diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 65f74dc..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: CI - -on: - - push - - pull_request - -jobs: - - build: - - strategy: - matrix: - python-version: - - "3.7" - - "3.8" - - "3.9" - - "3.10" - - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-20.04 - - steps: - - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install bandit flake8 pylint - - - name: Lint with flake8 - run: make flake8 - - # - name: Lint with pylint - # run: make pylint - - - name: Lint with bandit - run: make bandit diff --git a/.gitignore b/.gitignore index 5e4d717..47d34c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,23 @@ -.pylint.err -.pylint.out *.pyc *.pyo - -*.zip -*.bak -*.lis -*.dst -*.so - +*.ui toxygen/toxcore tests/tests -toxygen/libs +tests/libs tests/.cache tests/__pycache__ -tests/avatars toxygen/libs .idea *~ -#* *.iml -*.junk - *.so *.log toxygen/build toxygen/dist *.spec -dist +dist/ toxygen/avatars toxygen/__pycache__ /*.egg-info /*.egg -html -Toxygen.egg-info -*.tox -.cache -*.db -*~ -Makefile + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 524302e..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# -*- mode: yaml; indent-tabs-mode: nil; tab-width: 2; coding: utf-8-unix -*- ---- - -default_language_version: - python: python3.11 -default_stages: [pre-commit] -fail_fast: true -repos: -- repo: local - hooks: - - id: pylint - name: pylint - entry: env PYTHONPATH=/mnt/o/var/local/src/toxygen.git/toxygen toxcore_pylint.bash - language: system - types: [python] - args: - [ - "--source-roots=/mnt/o/var/local/src/toxygen.git/toxygen", - "-rn", # Only display messages - "-sn", # Don't display the score - "--rcfile=/usr/local/etc/testforge/pylint.rc", # Link to your config file - "-E" - ] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index e79a8ae..0000000 --- a/.pylintrc +++ /dev/null @@ -1,4 +0,0 @@ -[pre-commit-hook] -command=env PYTHONPATH=/mnt/o/var/local/src/toxygen.git/toxygen /usr/local/bin/toxcore_pylint.bash -params= -E --exit-zero -limit=8 diff --git a/.rsync b/.rsync deleted file mode 100644 index e69de29..0000000 diff --git a/.rsync.sh b/.rsync.sh deleted file mode 100644 index 06ad4db..0000000 --- a/.rsync.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -#find * -name \*.py | xargs grep -l '[ ]*$' | xargs sed -i -e 's/[ ]*$//' -rsync "$@" -vaxL --include \*.py \ - --exclude Toxygen.egg-info --exclude build \ - --exclude \*.pyc --exclude .pyl\* --exclude \*.so --exclude \*~ \ - --exclude __pycache__ --exclude \*.egg-info --exclude \*.new \ - ./ ../toxygen.git/|grep -v /$ diff --git a/.travis.yml b/.travis.yml index a4011e1..5bd30bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,14 @@ language: python python: - - "3.5" - - "3.6" -os: - - linux -dist: trusty -notifications: - email: false + - "3.4" before_install: - - sudo apt-get update - sudo apt-get install -y checkinstall build-essential - sudo apt-get install portaudio19-dev - - sudo apt-get install libsecret-1-dev - - sudo apt-get install libconfig-dev libvpx-dev check -qq install: - - pip install sip - - pip install pyqt5 + - pip install PySide --no-index --find-links https://parkin.github.io/python-wheelhouse/; + - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - 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 - - tar xzf opus-1.0.3.tar.gz - - cd opus-1.0.3 - - ./configure - - make -j3 - - sudo make install - - cd .. # Libsodium - git clone git://github.com/jedisct1/libsodium.git - cd libsodium @@ -38,16 +19,13 @@ before_script: - sudo ldconfig - cd .. # Toxcore - - git clone https://github.com/ingvar1995/toxcore.git --branch=ngc_rebase + - git clone https://github.com/irungentoo/toxcore.git - cd toxcore - - mkdir _build && cd _build - - cmake .. + - autoreconf -if + - ./configure - 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 +script: py.test tests/travis.py diff --git a/MANIFEST.in b/MANIFEST.in index 89e57c6..9bf65b8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -12,8 +12,9 @@ include toxygen/smileys/starwars/*.png include toxygen/smileys/starwars/config.json include toxygen/smileys/ksk/*.png include toxygen/smileys/ksk/config.json -include toxygen/styles/*.qss +include toxygen/styles/style.qss include toxygen/translations/*.qm include toxygen/libs/libtox.dll include toxygen/libs/libsodium.a -include toxygen/bootstrap/nodes.json +include toxygen/libs/libtox64.dll +include toxygen/libs/libsodium64.a \ No newline at end of file diff --git a/README.md b/README.md index 8a15a09..5b75a6d 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,63 @@ # Toxygen -Toxygen is powerful cross-platform [Tox](https://tox.chat/) client -for Tox and IRC/weechat written in pure Python3. +Toxygen is cross-platform [Tox](https://tox.chat/) client written in pure Python3 + +[![Release](https://img.shields.io/github/release/xveduk/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/releases/latest) +[![Stars](https://img.shields.io/github/stars/xveduk/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/stargazers) +[![Open issues](https://img.shields.io/github/issues/xveduk/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/issues) +[![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](https://raw.githubusercontent.com/toxygen-project/toxygen/master/LICENSE.md) ### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) -### Supported OS: Linux and Windows (only Linux is tested at the moment) +### Supported OS: -### Features: +- Windows +- Linux +- OS X -- PyQt5, PyQt6, and maybe PySide2, PySide6 via qtpy -- IRC via weechat /relay -- NGC groups -- 1v1 messages -- File transfers -- Audio calls -- Video calls -- Group chats -- Plugins support -- Desktop sharing -- Chat history -- Emoticons -- Stickers -- Screenshots -- Save file encryption -- Profile import and export -- Faux offline messaging -- Faux offline file transfers -- Inline images -- Message splitting -- Proxy support - runs over tor, without DNS leaks -- Avatars -- Multiprofile -- Multilingual -- Sound notifications -- Contact aliases -- Contact blocking -- Typing notifications -- Changing nospam -- File resuming -- Read receipts -- uses gevent +### Features + +- [x] 1v1 messages +- [x] File transfers +- [x] Audio +- [x] Plugins support +- [x] Chat history +- [x] Emoticons +- [x] Stickers +- [x] Screenshots +- [x] Name lookups (toxme.io support) +- [x] Save file encryption +- [x] Profile import and export +- [x] Faux offline messaging +- [x] Faux offline file transfers +- [x] Inline images +- [x] Message splitting +- [x] Proxy support +- [x] Avatars +- [x] Multiprofile +- [x] Multilingual +- [x] Sound notifications +- [x] Contact aliases +- [x] Contact blocking +- [x] Typing notifications +- [x] Changing nospam +- [x] File resuming +- [x] Read receipts +- [ ] Video +- [ ] Desktop sharing +- [ ] Group chats + +### Downloads +[Releases](https://github.com/toxygen-project/toxygen/releases) + +[Download last stable version](https://github.com/toxygen-project/toxygen/archive/master.zip) + +[Download develop version](https://github.com/toxygen-project/toxygen/archive/develop.zip) ### Screenshots *Toxygen on Ubuntu and Windows* ![Ubuntu](/docs/ubuntu.png) ![Windows](/docs/windows.png) -Windows was working but is not currently being tested. AV is working -but the video is garbled: we're unsure of naming the AV devices -from the commandline. We need to get a working echobot that supports SOCKS5; -we were working on one in https://git.plastiras.org/emdee/toxygen_wrapper - -## Forked - -This hard-forked from the dead https://github.com/toxygen-project/toxygen -```next_gen``` branch. - -See ToDo.md to the current ToDo list. - -## IRC Weechat - -You can have a [weechat](https://github.com/weechat/qweechat) -console so that you can have IRC and jabber in a window as well as Tox. -There's a copy of qweechat in https://git.plastiras.org/emdee/qweechat -that you must install first, which was backported to PyQt5 now to qtpy -(PyQt5 PyQt6 and PySide2 and PySide6) and integrated into toxygen. -Follow the normal instructions for adding a ```relay``` to -[weechat](https://github.com/weechat/weechat) -``` -/relay add weechat 9000 -/relay start weechat -``` -or -``` -weechat -r '/relay add weechat 9000;/relay start weechat' -``` -and use the Plugins -> Weechat Console to start weechat under Toxygen. -Then use the File/Connect menu item of the Console to connect to weechat. - -Weechat has a Jabber plugin to enable XMPP: -``` -/python load jabber.el -/help jabber -``` -so you can have Tox, IRC and XMPP in the same application! See docs/ToxygenWeechat.md - -## Install - -To install read the requirements.txt and look at the comments; there -are things that need installing by hand or decisions to be made -on supported alternatives. - -https://git.plastiras.org/emdee/toxygen_wrapper needs installing as it is a -dependency. Just download and install it from -https://git.plastiras.org/emdee/toxygen_wrapper The same with -https://git.plastiras.org/emdee/qweechat - -This is being ported to Qt6 using qtpy https://github.com/spyder-ide/qtpy -It now runs on PyQt5 and PyQt6, and may run on PySide2 and PySide6 - YMMV. -You will be able to choose between them by setting the environment variable -```QT_API``` to one of: ```pyqt5 pyqt6 pyside2 pyside6```. -It's currently tested mainly on PyQt5. - -To install it, look in the Makefile for the install target and type -``` -make install -``` -You should set the PIP_EXE_MSYS and PYTHON_EXE_MSYS variables and it does -``` - ${PIP_EXE_MSYS} --python ${PYTHON_EXE_MSYS} install \ - --no-deps \ - --target ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/ \ - --upgrade . -``` -and installs into PREFIX which is usually /usr/local - -## Updates - -Up-to-date code is on https://git.plastiras.org/emdee/toxygen - -Tox works over Tor, and the c-toxcore library can leak DNS requests -due to a 6-year old known security issue: -https://github.com/TokTok/c-toxcore/issues/469 but toxygen looksup -addresses before calling c-toxcore. This also allows us to use onion -addresses in the DHTnodes.json file. Still for anonymous communication -we recommend having a TCP and UDP firewall in place. - -Although Tox works with multi-user group chat, there are no checks -against impersonation of a screen nickname, so you may not be chatting -with the person you think. For the Toxic client, the (closed) issue is: -https://github.com/JFreegman/toxic/issues/622#issuecomment-1922116065 -Solving this might best be done with a solution to MultiDevice q.v. - -The Tox project does not follow semantic versioning of its main structures -in C so the project may break the underlying ctypes wrapper at any time; -it's not possible to use Tox version numbers to tell what the API will be. -The last git version this code was tested with is -``1623e3ee5c3a5837a92f959f289fcef18bfa9c959``` of Feb 12 10:06:37 2024. -In which case you may need to go into the tox.py file in -https://git.plastiras.org/emdee/toxygen_wrapper to fix it yourself. - -## MultiDevice - -Work on this project is suspended until the -[MultiDevice](https://git.plastiras.org/emdee/tox_profile/wiki/MultiDevice-Announcements-POC) problem is solved. Fork me! +### Docs +[Check /docs/ for more info](/docs/) diff --git a/ToDo.md b/ToDo.md deleted file mode 100644 index 9b4266f..0000000 --- a/ToDo.md +++ /dev/null @@ -1,70 +0,0 @@ -# Toxygen ToDo List - -## Bugs - -1. There is an agravating bug where new messages are not put in the - current window, and a messages waiting indicator appears. You have - to focus out of the window and then back in the window. this may be - fixed already - -2. The tray icon is flaky and has been disabled - look in app.py - for bSHOW_TRAY - -## Fix history - -## Fix Audio - -The code is in there but it's not working. It looks like audio input -is working but not output. The code is all in there; I may have broken -it trying to wire up the ability to set the audio device from the -command line. - -## Fix Video - -The code is in there but it's not working. I may have broken it -trying to wire up the ability to set the video device from the command -line. - -## NGC Groups - -1. peer_id There has been a change of API on a field named - ```group.peer_id``` The code is broken in places because I have not - seen the path to change from the old API ro the new one. - - -## Plugin system - -1. Needs better documentation and checking. - -2. There's something broken in the way some of them plug into Qt menus. - -3. Should the plugins be in toxygen or a separate repo? - -4. There needs to be a uniform way for plugins to wire into callbacks. - -## check toxygen_wrapper - -1. I've broken out toxygen_wrapper to be standalone, - https://git.plastiras.org/emdee/toxygen_wrapper but the tox.py - needs each call double checking. - -2. https://git.plastiras.org/emdee/toxygen_wrapper needs packaging - and making a dependency. - -## Migration - -Migrate PyQt5 to qtpy - done, but I'm not sure qtpy supports PyQt6. -https://github.com/spyder-ide/qtpy/ - -Maybe migrate gevent to asyncio, and migrate to -[qasync](https://github.com/CabbageDevelopment/qasync) -(see https://git.plastiras.org/emdee/phantompy ). - -(Also look at https://pypi.org/project/asyncio-gevent/ but it's dead). - -## Standards - -There's a standard for Tox clients that this has not been tested against: -https://tox.gitbooks.io/tox-client-standard/content/general_requirements/general_requirements.html -https://github.com/Tox/Tox-Client-Standard - diff --git a/_Bugs/segv.err b/_Bugs/segv.err deleted file mode 100644 index 6f1294e..0000000 --- a/_Bugs/segv.err +++ /dev/null @@ -1,130 +0,0 @@ -0 -TRAC> network.c#1748:net_connect connecting socket 58 to 127.0.0.1:9050 -TRAC> Messenger.c#2709:do_messenger Friend num in DHT 2 != friend num in msger 14 -TRAC> Messenger.c#2723:do_messenger F[--: 0] D3385007C28852C5398393E3338E6AABE5F86EF249BF724E7404233207D4D927 -TRAC> Messenger.c#2723:do_messenger F[--: 1] 98984E104B8A97CC43AF03A27BE159AC1F4CF35FADCC03D6CD5F8D67B5942A56 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 185.87.49.189:3389 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 185.87.49.189:3389 (0: OK) | 010001b95731bd0d...3d -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 37.221.66.161:443 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 37.221.66.161:443 (0: OK) | 01000125dd42a101...bb -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 172.93.52.70:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 139.162.110.188:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 37.59.63.150:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 37.97.185.116:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 85.143.221.42:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 104.244.74.69:38445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 168.119.209.10:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 81.169.136.229:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 91.219.59.156:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 46.101.197.175:3389 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 198.199.98.108:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 188.225.9.167:33445 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 5.19.249.240:38296 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> network.c#789:loglogdata [05 = ] T=> 3= 94.156.35.247:3389 (0: OK) | 0000000000000000...00 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 172.93.52.70:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 172.93.52.70:33445 (0: OK) | 010001ac5d344682...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 139.162.110.188:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 139.162.110.188:33445 (0: OK) | 0100018ba26ebc82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 37.59.63.150:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 37.59.63.150:33445 (0: OK) | 010001253b3f9682...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 130.133.110.14:33445 (0: OK) | 01000182856e0e82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 37.97.185.116:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 37.97.185.116:33445 (0: OK) | 0100012561b97482...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 85.143.221.42:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 85.143.221.42:33445 (0: OK) | 010001558fdd2a82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 104.244.74.69:38445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 104.244.74.69:38445 (0: OK) | 01000168f44a4596...2d -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 49.12.229.145:3389 (0: OK) | 010001310ce5910d...3d -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 168.119.209.10:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 168.119.209.10:33445 (0: OK) | 010001a877d10a82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 81.169.136.229:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 81.169.136.229:33445 (0: OK) | 01000151a988e582...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 91.219.59.156:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 91.219.59.156:33445 (0: OK) | 0100015bdb3b9c82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 46.101.197.175:3389 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 46.101.197.175:3389 (0: OK) | 0100012e65c5af0d...3d -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 198.199.98.108:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 198.199.98.108:33445 (0: OK) | 010001c6c7626c82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 130.133.110.14:33445 (0: OK) | 01000182856e0e82...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 49.12.229.145:3389 (0: OK) | 010001310ce5910d...3d -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 188.225.9.167:33445 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 188.225.9.167:33445 (0: OK) | 010001bce109a782...a5 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 5.19.249.240:38296 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 5.19.249.240:38296 (0: OK) | 0100010513f9f095...98 -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> network.c#789:loglogdata [05 = ] =>T 2= 94.156.35.247:3389 (0: OK) | 0000000000000000...00 -TRAC> network.c#789:loglogdata [05 = ] T=> 10= 94.156.35.247:3389 (0: OK) | 0100015e9c23f70d...3d -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes -app.contacts.contacts_manager INFO update_groups_numbers len(groups)={len(groups)} - -Thread 76 "ToxIterateThrea" received signal SIGSEGV, Segmentation fault. -[Switching to Thread 0x7ffedcb6b640 (LWP 2950427)] diff --git a/_Bugs/tox.abilinski.com.ping b/_Bugs/tox.abilinski.com.ping deleted file mode 100644 index 920f606..0000000 --- a/_Bugs/tox.abilinski.com.ping +++ /dev/null @@ -1,11 +0,0 @@ - ping tox.abilinski.com -ping: socket: Address family not supported by protocol -PING tox.abilinski.com (172.103.226.229) 56(84) bytes of data. -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=1 ttl=48 time=86.6 ms -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=2 ttl=48 time=83.1 ms -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=3 ttl=48 time=82.9 ms -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=4 ttl=48 time=83.4 ms -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=5 ttl=48 time=102 ms -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=6 ttl=48 time=87.4 ms -64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=7 ttl=48 time=84.9 ms -^C diff --git a/docs/ToxygenWeechat.md b/docs/ToxygenWeechat.md deleted file mode 100644 index 1694669..0000000 --- a/docs/ToxygenWeechat.md +++ /dev/null @@ -1,171 +0,0 @@ -## Toxygen Weechat - -You can have a [weechat](https://github.com/weechat/qweechat) -console so that you can have IRC and jabber in a window as well as Tox. -There's a copy of qweechat in ```thirdparty/qweechat``` backported to -PyQt5 and integrated into toxygen. Follow the normal instructions for -adding a ```relay``` to [weechat](https://github.com/weechat/weechat) -``` -/relay add ipv4.ssl.weechat 9000 -/relay start ipv4.ssl.weechat -``` -or -``` -/set relay.network.ipv6 off -/set relay.network.password password -/relay add weechat 9000 -/relay start weechat -``` -and use the Plugins/Weechat Console to start weechat under Toxygen. -Then use the File/Connect menu item of the Console to connect to weechat. - -Weechat has a Jabber plugin to enable XMPP: -``` -/python load jabber.el -/help jabber -``` -so you can have Tox, IRC and XMPP in the same application! - -### Creating servers for IRC over Tor - -Create a proxy called tor -``` -/proxy add tor socks5 127.0.0.1 9050 -``` - -It should now show up in the list of proxies. -``` -/proxy list -``` - -``` -/nick NickName -``` - -## TLS certificates - -[Create a Self-signed Certificate](https://www.oftc.net/NickServ/CertFP/) - -Choose a NickName you will identify as. - -Create a directory for your certificates ~/.config/weechat/ssl/ -and make a subdirectory for each server ~/.config/weechat/ssl/irc.oftc.net/ - -Change to the server directory and use openssl to make a keypair and answer the questions: -``` -openssl req -nodes -newkey rsa:2048 -keyout NickName.key -x509 -days 3650 -out NickName.cer -chmod 400 NickName.key -``` -We now combine certificate and key to a single file NickName.pem -``` -cat NickName.cer NickName.key > NickName.pem -chmod 400 NickName.pem -``` - -Do this for each server you want to connect to, or just use one for all of them. - -### Libera TokTok channel - -The main discussion forum for Tox is the #TokTok channel on libera. - -https://mox.sh/sysadmin/secure-irc-connection-to-freenode-with-tor-and-weechat/ -We have to create an account without Tor, this is a requirement to use TOR: -Connect to irc.libera.chat without Tor and register -``` -/msg NickServ identify NickName password -/msg NickServ REGISTER mypassword mycoolemail@example.com -/msg NickServ SET PRIVATE ON -``` -You'll get an email with a registration code. -Confirm registration after getting the mail with the code: -``` -/msg NickServ VERIFY REGISTER NickName code1235678 -``` - -Libera has an onion server so we can map an address in tor. Add this -to your /etc/tor/torrc -``` -MapAddress palladium.libera.chat libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion -``` -Or without the MapAddress just use -libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion -as the server address below, but set tls_verify to off. - -Define the server in weechat -https://www.weechat.org/files/doc/stable/weechat_user.en.html#irc_sasl_authentication -``` -/server remove libera -/server add libera palladium.libera.chat/6697 -tls -tls_verify -/set irc.server.libera.ipv6 off -/set irc.server.libera.proxy tor -/set irc.server.libera.username NickName -/set irc.server.libera.password password -/set irc.server.libera.nicks NickName -/set irc.server.libera.tls on -/set irc.server.libera.tls_cert "${weechat_config_dir}/ssl/libera.chat/NickName.pem" -``` - -``` -/set irc.server.libera.sasl_mechanism ecdsa-nist256p-challenge -/set irc.server.libera.sasl_username "NickName" -/set irc.server.libera.sasl_key "${weechat_config_dir}/ssl/libera.chat/NickName.pem" -``` - -Disconnect and connect back to the server. -``` -/disconnect libera -/connect libera -``` - -/msg nickserv identify password NickName - - -### oftc.net - -To use oftc.net over tor, you need to authenticate by SSL certificates. - - -Define the server in weechat -``` -/server remove irc.oftc.net -/server add OFTC irc.oftc.net/6697 -tls -tls_verify -/set irc.server.OFTC.ipv6 off -/set irc.server.OFTC.proxy tor -/set irc.server.OFTC.username NickName -/set irc.server.OFTC.nicks NickName -/set irc.server.OFTC.tls on -/set irc.server.OFTC.tls_cert "${weechat_config_dir}/ssl/irc.oftc.chat/NickName.pem" - -# Disconnect and connect back to the server. -/disconnect OFTC -/connect OFTC -``` -You must be identified in order to validate using certs -``` -/msg nickserv identify password NickName -``` -To allow NickServ to identify you based on this certificate you need -to associate the certificate fingerprint with your nick. To do this -issue the command cert add to Nickserv (try /msg nickserv helpcert). -``` -/msg nickserv cert add -``` - -### Privacy - -[Add somes settings bellow to weechat](https://szorfein.github.io/weechat/tor/configure-weechat/). -Detail from [faq](https://weechat.org/files/doc/weechat_faq.en.html#security). - -``` -/set irc.server_default.msg_part "" -/set irc.server_default.msg_quit "" -/set irc.ctcp.clientinfo "" -/set irc.ctcp.finger "" -/set irc.ctcp.source "" -/set irc.ctcp.time "" -/set irc.ctcp.userinfo "" -/set irc.ctcp.version "" -/set irc.ctcp.ping "" -/plugin unload xfer -/set weechat.plugin.autoload "*,!xfer" -``` diff --git a/docs/compile.md b/docs/compile.md index b4f6810..5b2b5fe 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -2,18 +2,9 @@ You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/) -Use Dockerfile and build script from `build` directory: +Install PyInstaller: +``pip3 install pyinstaller`` -1. Build image: -``` -docker build -t toxygen . -``` +``pyinstaller --windowed --icon images/icon.ico main.py`` -2. Run container: -``` -docker run -it toxygen bash -``` - -3. Execute `build.sh` script: - -```./build.sh``` +Don't forget to copy /images/, /sounds/, /translations/, /styles/, /smileys/, /stickers/, /plugins/ (and /libs/libtox.dll, /libs/libsodium.a on Windows) to /dist/main/ diff --git a/docs/contact.md b/docs/contact.md index 5eb2fa6..c66da1c 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -1,6 +1,5 @@ # Contact us: -1) https://git.plastiras.org/emdee/toxygen/issues +1) Using GitHub - open issue -2) Use Toxygen Tox Group (NGC) - -ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA +2) Use Toxygen Tox Group - add bot kalina@toxme.io (or 12EDB939AA529641CE53830B518D6EB30241868EE0E5023C46A372363CAEC91C2C948AEFE4EB) diff --git a/docs/contributing.md b/docs/contributing.md index b2cebf4..8b1e7fa 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,25 +1,20 @@ -# Issues +#Issues Help us find all bugs in Toxygen! Please provide following info: - OS - Toxygen version -- Toxygen executable info - python executable (.py), precompiled binary, from package etc. +- Toxygen executable info - .py or precompiled binary - Steps to reproduce the bug -Want to see new feature in Toxygen? -[Ask for it!](https://git.plastiras.org/emdee/toxygen/issues) +Want to see new feature in Toxygen? [Ask for it!](https://github.com/xveduk/toxygen/issues) -# Pull requests +#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://git.plastiras.org/emdee/toxygen/issues) -or implement features from our TODO list. +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/xveduk/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. +#Translations -# Translations - -Help us translate Toxygen! Translation can be created using pylupdate (``pylupdate5 toxygen.pro``) and QT Linguist. +Help us translate Toxygen! Translation can be created using pyside-lupdate (``pyside-lupdate toxygen.pro``) and QT Linguist. \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index 7d2b773..bf79c67 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,51 +1,86 @@ # How to install Toxygen +## Use precompiled binary: +[Check our releases page](https://github.com/xveduk/toxygen/releases) + +## Using pip3 + +### Windows + +``pip3.4 install toxygen`` + +Run app using ``toxygen`` command. + ### Linux -1. Install [c-toxcore](https://github.com/TokTok/c-toxcore/) +1. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/) 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](https://git.plastiras.org/emdee/toxygen/) -6. Run toxygen using ``toxygen`` command. +3. Install PySide: ``sudo apt-get install python3-pyside`` +4. Install toxygen: +``sudo pip3.4 install toxygen`` +5. Run toxygen using ``toxygen`` command. + +### OS X + +1. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system +2. Install PortAudio: +``brew install portaudio`` +3. Install toxygen: +``pip3 install toxygen`` +4. Run toxygen using ``toxygen`` command. + +## Packages + +Coming soon. ## From source code (recommended for developers) ### Windows -Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly recommended to use 64-bit Python. +1. [Download and install latest Python 3.4](https://www.python.org/downloads/windows/) +2. [Install PySide](https://pypi.python.org/pypi/PySide/1.2.4#installing-pyside-on-a-windows-system) (recommended) or [PyQt4](https://riverbankcomputing.com/software/pyqt/download) +3. Install PyAudio: ``pip3.4 install pyaudio`` +4. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip) +5. Unpack archive +6. Download latest libtox.dll build, download latest libsodium.a build, put it into \src\libs\ +7. Run \toxygen\main.py. -1. [Download and install latest Python 3 64-bit](https://www.python.org/downloads/windows/) -2. Install PyQt5: ``pip install pyqt5`` -3. Install PyAudio: ``pip install pyaudio`` -4. Install numpy: ``pip install numpy`` -5. Install OpenCV: ``pip install opencv-python`` -6. git clone --depth=1 https://git.plastiras.org/emdee/toxygen/ -7. I don't know -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: ``python3.4 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`` -3. Install [toxcore](https://github.com/TokTok/c-toxcore) with toxav support) -4. Install PyAudio: ``sudo apt-get install portaudio19-dev python3-pyaudio`` (or ``sudo pip3 install pyaudio``) -5. Install toxygen_wrapper https://git.plastiras.org/emdee/toxygen_wrapper -6. Install the rest of the requirements: ``sudo pip3 install -m requirements.txt`` -7. git clone --depth=1 [toxygen](https://git.plastiras.org/emdee/toxygen/) -8. Look in the Makefile for the install target and type -`` -make install -`` -You should set the PIP_EXE_MSYS and PYTHON_EXE_MSYS variables and it does -`` - ${PIP_EXE_MSYS} --python ${PYTHON_EXE_MSYS} install \ - --target ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/ \ - --upgrade . -`` -9. Run app: -``python3 ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/bin/toxygen`` +2. Install PySide: ``sudo apt-get install python3-pyside`` or install [PyQt4](https://riverbankcomputing.com/software/pyqt/download) (``sudo apt-get install python3-pyqt4``). +3. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/) +4. Install PyAudio: +``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``pip3 install pyaudio``) +5. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip) +6. Unpack archive +7. Run app: +``python3.4 main.py`` +Optional: install toxygen using setup.py: ``python3.4 setup.py install`` + +### OS X + +1. [Download and install latest Python 3.4](https://www.python.org/downloads/mac-osx/) +2. [Install PySide](https://pypi.python.org/pypi/PySide/1.2.4#installing-pyside-on-a-mac-os-x-system) (recommended) or [PyQt4](https://riverbankcomputing.com/software/pyqt/download) +3. Install PortAudio: +``brew install portaudio`` +4. Install PyAudio: ``pip3 install pyaudio`` +5. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system +6. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip) +7. Unpack archive +8. Run \toxygen\main.py. + +Optional: install toxygen using setup.py: ``python3 setup.py install`` diff --git a/docs/plugin_api.md b/docs/plugin_api.md index 32a27f8..f523270 100644 --- a/docs/plugin_api.md +++ b/docs/plugin_api.md @@ -1,6 +1,6 @@ -# Plugins API +#Plugins API -In Toxygen plugin is single python module (.py file) and directory with data associated with it. +In Toxygen plugin is single python (supported Python 3.0 - 3.4) 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. @@ -18,7 +18,7 @@ All plugin's data should be stored in following structure: ``` Plugin MUST override: -- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxES instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save. +- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxEncryptSave instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save. Plugin can override following methods: - get_description - this method should return plugin description. @@ -45,13 +45,13 @@ Import statement will not work in case you import module that wasn't previously About GUI: -GUI is available via PyQt5. Plugin can have no GUI at all. +It's strictly recommended to support both PySide and PyQt4 in GUI. Plugin can have no GUI at all. Exceptions: Plugin's methods MUST NOT raise exceptions. -# Examples +#Examples -You can find examples in [official repo](https://git.plastiras.org/emdee/toxygen_plugins) +You can find examples in [official repo](https://github.com/ingvar1995/toxygen_plugins) diff --git a/docs/plugins.md b/docs/plugins.md index 98fbac8..ee73415 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,22 +1,22 @@ -# Plugins +#Plugins -Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.5 - 3.6 module (.py file) and directory with plugin's data which provide some additional functionality. +Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 module (.py file) and directory with plugin's data which provide some additional functionality. -# How to write plugin +#How to write plugin Check [Plugin API](/docs/plugin_api.md) for more info -# How to install plugin +#How to install plugin Toxygen comes without preinstalled plugins. -1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins => Import plugin) -2. Restart Toxygen or choose Plugins => Reload plugins in menu. +1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins -> Import plugin) +2. Restart Toxygen -## Note: /src/plugins/ should contain plugin_super_class.py and __init__.py +##Note: /src/plugins/ should contain plugin_super_class.py and __init__.py -# Plugins list +#Plugins list WARNING: It is unsecure to install plugin not from this list! -[Main repo](https://github.com/toxygen-project/toxygen_plugins) \ No newline at end of file +[Main repo](https://github.com/ingvar1995/toxygen_plugins) \ No newline at end of file diff --git a/docs/smileys_and_stickers.md b/docs/smileys_and_stickers.md index 8705ba8..53a360b 100644 --- a/docs/smileys_and_stickers.md +++ b/docs/smileys_and_stickers.md @@ -1,4 +1,4 @@ -# Smileys +#Smileys Toxygen support smileys. Smiley is small picture which replaces some symbol or combination of symbols. If you want to create your own smiley pack, create directory in src/smileys/. This directory must contain images with smileys and config.json. Example of config.json: @@ -6,8 +6,8 @@ Toxygen support smileys. Smiley is small picture which replaces some symbol or c Animated smileys (.gif) are supported too. -# Stickers +#Stickers -Sticker is inline image. If you want to create your own sticker pack, create directory in src/stickers/ and place your stickers there. +Sticker is inline image. If you want to create your own smiley pack, create directory in src/stickers/ and place your stickers there. -Users can import smileys and stickers using menu: Settings -> Interface +Users can import plugins and stickers packs using menu: Settings -> Interface \ No newline at end of file diff --git a/docs/todo.md b/docs/todo.md deleted file mode 100644 index 9b4266f..0000000 --- a/docs/todo.md +++ /dev/null @@ -1,70 +0,0 @@ -# Toxygen ToDo List - -## Bugs - -1. There is an agravating bug where new messages are not put in the - current window, and a messages waiting indicator appears. You have - to focus out of the window and then back in the window. this may be - fixed already - -2. The tray icon is flaky and has been disabled - look in app.py - for bSHOW_TRAY - -## Fix history - -## Fix Audio - -The code is in there but it's not working. It looks like audio input -is working but not output. The code is all in there; I may have broken -it trying to wire up the ability to set the audio device from the -command line. - -## Fix Video - -The code is in there but it's not working. I may have broken it -trying to wire up the ability to set the video device from the command -line. - -## NGC Groups - -1. peer_id There has been a change of API on a field named - ```group.peer_id``` The code is broken in places because I have not - seen the path to change from the old API ro the new one. - - -## Plugin system - -1. Needs better documentation and checking. - -2. There's something broken in the way some of them plug into Qt menus. - -3. Should the plugins be in toxygen or a separate repo? - -4. There needs to be a uniform way for plugins to wire into callbacks. - -## check toxygen_wrapper - -1. I've broken out toxygen_wrapper to be standalone, - https://git.plastiras.org/emdee/toxygen_wrapper but the tox.py - needs each call double checking. - -2. https://git.plastiras.org/emdee/toxygen_wrapper needs packaging - and making a dependency. - -## Migration - -Migrate PyQt5 to qtpy - done, but I'm not sure qtpy supports PyQt6. -https://github.com/spyder-ide/qtpy/ - -Maybe migrate gevent to asyncio, and migrate to -[qasync](https://github.com/CabbageDevelopment/qasync) -(see https://git.plastiras.org/emdee/phantompy ). - -(Also look at https://pypi.org/project/asyncio-gevent/ but it's dead). - -## Standards - -There's a standard for Tox clients that this has not been tested against: -https://tox.gitbooks.io/tox-client-standard/content/general_requirements/general_requirements.html -https://github.com/Tox/Tox-Client-Standard - diff --git a/docs/ubuntu.png b/docs/ubuntu.png old mode 100644 new mode 100755 index 67951a5..cd80444 Binary files a/docs/ubuntu.png and b/docs/ubuntu.png differ diff --git a/docs/windows.png b/docs/windows.png old mode 100644 new mode 100755 index f13f4c0..d4ed323 Binary files a/docs/windows.png and b/docs/windows.png differ diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 37d424a..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,56 +0,0 @@ -[project] -name = "toxygen" -description = "examples of using stem" -authors = [{ name = "emdee", email = "emdee@spm.plastiras.org" } ] -requires-python = ">=3.7" -keywords = ["stem", "python3", "tox"] -classifiers = [ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - "Development Status :: 4 - Beta", - - # Indicate who your project is intended for - "Intended Audience :: Developers", - - # Specify the Python versions you support here. - "Programming Language :: Python :: 3", - "License :: OSI Approved", - "Operating System :: POSIX :: BSD :: FreeBSD", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: Implementation :: CPython", -] -# -dynamic = ["version", "readme", "dependencies"] # cannot be dynamic ['license'] - -[project.gui-scripts] -toxygen = "toxygen.__main__:main" - -[project.optional-dependencies] -weechat = ["weechat"] - -#[project.license] -#file = "LICENSE.md" - -[project.urls] -repository = "https://git.plastiras.org/emdee/toxygen" - -[build-system] -requires = ["setuptools >= 61.0"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.dynamic] -version = {attr = "toxygen.app.__version__"} -readme = {file = ["README.md", "ToDo.txt"]} -dependencies = {file = ["requirements.txt"]} - -[tool.setuptools] -packages = ["toxygen"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 216e1a4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -# the versions are the current ones tested - may work with earlier versions -# choose one of PyQt5 PyQt6 PySide2 PySide6 -# for now PyQt5 and PyQt6 is working, and most of the testing is PyQt5 -# usually this is installed by your OS package manager and pip may not -# detect the right version, so we leave these commented -# PyQt5 >= 5.15.10 -# this is not on pypi yet but is required - get it from -# https://git.plastiras.org/emdee/toxygen_wrapper -# toxygen_wrapper == 1.0.0 -QtPy >= 2.4.1 -PyAudio >= 0.2.13 -numpy >= 1.26.1 -opencv_python >= 4.8.0 -pillow >= 10.2.0 -gevent >= 23.9.1 -pydenticon >= 0.3.1 -greenlet >= 2.0.2 -sounddevice >= 0.3.15 -# this is optional -coloredlogs >= 15.0.1 -# this is optional -# qtconsole >= 5.4.3 -# this is not on pypi yet but is optional for qweechat - get it from -# https://git.plastiras.org/emdee/qweechat -# qweechat_wrapper == 0.0.1 - diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d7ffc22..0000000 --- a/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -[metadata] -classifiers = - License :: OSI Approved - License :: OSI Approved :: BSD 1-clause - Intended Audience :: Web Developers - Operating System :: Microsoft :: Windows - Operating System :: POSIX :: BSD :: FreeBSD - Operating System :: POSIX :: Linux - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: Implementation :: CPython - -[options] -zip_safe = false -python_requires = ~=3.7 -include_package_data = - "*" = ["*.ui", "*.txt", "*.png", "*.ico", "*.gif", "*.wav"] - -[options.entry_points] -console_scripts = - toxygen = toxygen.__main__:iMain - -[easy_install] -zip_ok = false - -[flake8] -jobs = 1 -max-line-length = 88 -ignore = - E111 - E114 - E128 - E225 - E261 - E302 - E305 - E402 - E501 - E502 - E541 - E701 - E702 - E704 - E722 - E741 - F508 - F541 - W503 - W601 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4eb163e --- /dev/null +++ b/setup.py @@ -0,0 +1,71 @@ +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 sys + + +version = program_version + '.0' + +MODULES = [] + +if system() in ('Windows', 'Darwin'): + MODULES = ['PyAudio', 'PySide'] +else: + try: + import pyaudio + except ImportError: + MODULES = ['PyAudio'] + + +class InstallScript(install): + """This class configures Toxygen after installation""" + + def run(self): + install.run(self) + try: + if system() == 'Windows': + call(["toxygen", "--configure"]) + else: + call(["toxygen", "--clean"]) + except: + try: + params = list(filter(lambda x: x.startswith('--prefix='), sys.argv)) + if params: + path = params[0][len('--prefix='):] + if path[-1] not in ('/', '\\'): + path += '/' + path += 'bin/toxygen' + if system() == 'Windows': + call([path, "--configure"]) + else: + call([path, "--clean"]) + except: + pass + +setup(name='Toxygen', + version=version, + description='Toxygen - Tox client', + long_description='Toxygen is powerful Tox client written in Python3', + url='https://github.com/xveduk/toxygen/', + keywords='toxygen tox messenger', + author='Ingvar', + maintainer='Ingvar', + license='GPL3', + packages=['toxygen', 'toxygen.plugins', 'toxygen.styles'], + install_requires=MODULES, + include_package_data=True, + classifiers=[ + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + entry_points={ + 'console_scripts': ['toxygen=toxygen.main:main'], + }, + cmdclass={ + 'install': InstallScript, + }, + ) diff --git a/setup.py.dst b/setup.py.dst deleted file mode 100644 index a3f543d..0000000 --- a/setup.py.dst +++ /dev/null @@ -1,53 +0,0 @@ -import sys -import os -from setuptools import setup -from setuptools.command.install import install - -version = '1.0.0' - -MODULES = open('requirements.txt', 'rt').readlines() - -def get_packages(): - directory = os.path.join(os.path.dirname(__file__), 'tox_wrapper') - for root, dirs, files in os.walk(directory): - packages = map(lambda d: 'toxygen.' + d, dirs) - packages = ['toxygen'] + list(packages) - return packages - -class InstallScript(install): - """This class configures Toxygen after installation""" - - def run(self): - install.run(self) - -setup(name='Toxygen', - version=version, - description='Toxygen - Tox client', - long_description='Toxygen is powerful Tox client written in Python3', - url='https://git.plastiras.org/emdee/toxygen/', - keywords='toxygen Tox messenger', - author='Ingvar', - maintainer='', - license='GPL3', - packages=get_packages(), - install_requires=MODULES, - include_package_data=True, - classifiers=[ - 'Programming Language :: Python :: 3 :: Only', - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - 'Programming Language :: Python :: 3.11', - ], - entry_points={ - 'console_scripts': ['toxygen=toxygen.main:main'] - }, - package_data={"": ["*.ui"],}, - cmdclass={ - 'install': InstallScript, - }, - zip_safe=False - ) diff --git a/tests/tests.py b/tests/tests.py index e3c9b6b..c9f5ca6 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,18 +1,70 @@ -from toxygen.middleware.tox_factory import * +from toxygen.bootstrap import node_generator +from toxygen.profile import * +from toxygen.settings import ProfileHelper +from toxygen.tox_dns import tox_dns +import toxygen.toxencryptsave as encr -# TODO: add new tests +class TestProfile: + + def test_search(self): + arr = ProfileHelper.find_profiles() + assert len(arr) >= 2 + + def test_open(self): + data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile() + assert data + class TestTox: + def test_loading(self): + data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile() + settings = Settings.get_default_settings() + tox = tox_factory(data, settings) + for data in node_generator(): + tox.bootstrap(*data) + del tox + def test_creation(self): - name = 'Toxygen User' - status_message = 'Toxing on Toxygen' + name = b'Toxygen User' + status_message = b'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() == name - assert tox.self_get_status_message() == status_message + assert tox.self_get_name() == str(name, 'utf-8') + assert tox.self_get_status_message() == str(status_message, 'utf-8') + + def test_friend_list(self): + data = ProfileHelper(Settings.get_default_path(), 'bob').open_profile() + settings = Settings.get_default_settings() + tox = tox_factory(data, settings) + s = tox.self_get_friend_list() + size = tox.self_get_friend_list_size() + assert size <= 2 + assert len(s) <= 2 + del tox + + +class TestDNS: + + def test_dns(self): + bot_id = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5' + tox_id = tox_dns('groupbot@toxme.io') + assert tox_id == bot_id + + +class TestEncryption: + + def test_encr_decr(self): + with open(settings.Settings.get_default_path() + '/alice.tox', 'rb') as fl: + data = fl.read() + lib = encr.ToxEncryptSave() + lib.set_password('easypassword') + copy_data = data[:] + data = lib.pass_encrypt(data) + data = lib.pass_decrypt(data) + assert copy_data == data diff --git a/tests/travis.py b/tests/travis.py index af8f83f..474d961 100644 --- a/tests/travis.py +++ b/tests/travis.py @@ -1,4 +1,4 @@ class TestToxygen: def test_main(self): - import toxygen.__main__ # check for syntax errors + import toxygen.main diff --git a/toxygen/.pylint.sh b/toxygen/.pylint.sh deleted file mode 100755 index c2e645c..0000000 --- a/toxygen/.pylint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -ROLE=logging -/var/local/bin/pydev_pylint.bash -E -f text *py [a-nr-z]*/*py >.pylint.err -/var/local/bin/pydev_pylint.bash *py [a-nr-z]*/*py >.pylint.out - -sed -e "/Module 'os' has no/d" \ - -e "/Undefined variable 'app'/d" \ - -e '/tests\//d' \ - -e "/Instance of 'Curl' has no /d" \ - -e "/No name 'path' in module 'os' /d" \ - -e "/ in module 'os'/d" \ - -e "/.bak\//d" \ - -i .pylint.err .pylint.out diff --git a/toxygen/__init__.py b/toxygen/__init__.py index 4671c45..70180be 100644 --- a/toxygen/__init__.py +++ b/toxygen/__init__.py @@ -1,3 +1,8 @@ import os import sys +path = os.path.dirname(os.path.realpath(__file__)) # curr dir + +sys.path.insert(0, os.path.join(path, 'styles')) +sys.path.insert(0, os.path.join(path, 'plugins')) +sys.path.insert(0, path) diff --git a/toxygen/__main__.py b/toxygen/__main__.py deleted file mode 100644 index 406726f..0000000 --- a/toxygen/__main__.py +++ /dev/null @@ -1,378 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import sys -import os -import logging -import signal -import time -import warnings -import faulthandler - -from gevent import monkey; monkey.patch_all(); del monkey # noqa - -faulthandler.enable() -warnings.filterwarnings('ignore') - -import toxygen_wrapper.tests.support_testing as ts -try: - from trepan.interfaces import server as Mserver - from trepan.api import debug -except Exception as e: - print('trepan3 TCP server NOT enabled.') -else: - import signal - try: - signal.signal(signal.SIGUSR1, ts.trepan_handler) - print('trepan3 TCP server enabled on port 6666.') - except: pass - -import app -from user_data.settings import * -from user_data.settings import Settings -from user_data import settings -import utils.util as util -with ts.ignoreStderr(): - import pyaudio - -__maintainer__ = 'Ingvar' -__version__ = '1.0.0' # was 0.5.0+ - -path = os.path.dirname(os.path.realpath(__file__)) # curr dir -sys.path.insert(0, os.path.join(path, 'styles')) -sys.path.insert(0, os.path.join(path, 'plugins')) -# sys.path.insert(0, os.path.join(path, 'third_party')) -sys.path.insert(0, path) - -sleep = time.sleep - -os.environ['QT_API'] = os.environ.get('QT_API', 'pyqt5') - -def reset() -> None: - Settings.reset_auto_profile() - -def clean() -> None: - """Removes libs folder""" - directory = util.get_libs_directory() - util.remove(directory) - -def print_toxygen_version() -> None: - print('toxygen ' + __version__) - -def setup_default_audio(): - # need: - audio = ts.get_audio() - # unfinished - global oPYA - oPYA = pyaudio.PyAudio() - audio['output_devices'] = dict() - i = oPYA.get_device_count() - while i > 0: - i -= 1 - if oPYA.get_device_info_by_index(i)['maxOutputChannels'] == 0: - continue - audio['output_devices'][i] = oPYA.get_device_info_by_index(i)['name'] - i = oPYA.get_device_count() - audio['input_devices'] = dict() - while i > 0: - i -= 1 - if oPYA.get_device_info_by_index(i)['maxInputChannels'] == 0: - continue - audio['input_devices'][i] = oPYA.get_device_info_by_index(i)['name'] - return audio - -def setup_video(oArgs): - video = setup_default_video() - # this is messed up - no video_input in oArgs - # parser.add_argument('--video_input', type=str,) - print(video) - if not video or not video['output_devices']: - video['device'] = -1 - if not hasattr(oArgs, 'video_input'): - video['device'] = video['output_devices'][0] - elif oArgs.video_input == '-1': - video['device'] = video['output_devices'][-1] - else: - video['device'] = oArgs.video_input - return video - -def setup_audio(oArgs): - global oPYA - audio = setup_default_audio() - for k,v in audio['input_devices'].items(): - if v == 'default' and 'input' not in audio: - audio['input'] = k - if v == getattr(oArgs, 'audio_input'): - audio['input'] = k - LOG.debug(f"Setting audio['input'] {k} = {v} {k}") - break - for k,v in audio['output_devices'].items(): - if v == 'default' and 'output' not in audio: - audio['output'] = k - if v == getattr(oArgs, 'audio_output'): - audio['output'] = k - LOG.debug(f"Setting audio['output'] {k} = {v} " +str(k)) - break - - if hasattr(oArgs, 'mode') and getattr(oArgs, 'mode') > 1: - audio['enabled'] = True - audio['audio_enabled'] = True - audio['video_enabled'] = True - elif hasattr(oArgs, 'mode') and getattr(oArgs, 'mode') > 0: - audio['enabled'] = True - audio['audio_enabled'] = False - audio['video_enabled'] = True - else: - audio['enabled'] = False - audio['audio_enabled'] = False - audio['video_enabled'] = False - - return audio - - i = getattr(oArgs, 'audio_output') - if i >= 0: - try: - elt = oPYA.get_device_info_by_index(i) - if i >= 0 and ( 'maxOutputChannels' not in elt or \ - elt['maxOutputChannels'] == 0): - LOG.warn(f"Audio output device has no output channels: {i}") - oArgs.audio_output = -1 - except OSError as e: - LOG.warn("Audio output device error looking for maxOutputChannels: " \ - +str(i) +' ' +str(e)) - oArgs.audio_output = -1 - - if getattr(oArgs, 'audio_output') < 0: - LOG.info("Choose an output device:") - i = oPYA.get_device_count() - while i > 0: - i -= 1 - if oPYA.get_device_info_by_index(i)['maxOutputChannels'] == 0: - continue - LOG.info(str(i) \ - +' ' +oPYA.get_device_info_by_index(i)['name'] \ - +' ' +str(oPYA.get_device_info_by_index(i)['defaultSampleRate']) - ) - return 0 - - i = getattr(oArgs, 'audio_input') - if i >= 0: - try: - elt = oPYA.get_device_info_by_index(i) - if i >= 0 and ( 'maxInputChannels' not in elt or \ - elt['maxInputChannels'] == 0): - LOG.warn(f"Audio input device has no input channels: {i}") - setattr(oArgs, 'audio_input', -1) - except OSError as e: - LOG.warn("Audio input device error looking for maxInputChannels: " \ - +str(i) +' ' +str(e)) - setattr(oArgs, 'audio_input', -1) - if getattr(oArgs, 'audio_input') < 0: - LOG.info("Choose an input device:") - i = oPYA.get_device_count() - while i > 0: - i -= 1 - if oPYA.get_device_info_by_index(i)['maxInputChannels'] == 0: - continue - LOG.info(str(i) \ - +' ' +oPYA.get_device_info_by_index(i)['name'] - +' ' +str(oPYA.get_device_info_by_index(i)['defaultSampleRate']) - ) - return 0 - -def setup_default_video(): - default_video = ["-1"] - default_video.extend(ts.get_video_indexes()) - LOG.info(f"Video input choices: {default_video}") - video = {'device': -1, 'width': 320, 'height': 240, 'x': 0, 'y': 0} - video['output_devices'] = default_video - return video - -def main_parser(_=None, iMode=2): - if not os.path.exists('/proc/sys/net/ipv6'): - bIpV6 = 'False' - else: - bIpV6 = 'True' - lIpV6Choices=[bIpV6, 'False'] - - audio = setup_default_audio() - default_video = setup_default_video()['output_devices'] - - parser = ts.oMainArgparser() - 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', type=str, default='', - help='Add specified Tox ID to friends') - parser.add_argument('--auto_accept_path', '--auto-accept-path', type=str, - default=os.path.join(os.environ['HOME'], 'Downloads'), - help="auto_accept_path") -# parser.add_argument('--mode', type=int, default=iMode, -# help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0') - parser.add_argument('--font', type=str, default="Courier", - help='Message font') - parser.add_argument('--message_font_size', type=int, default=15, - help='Font size in pixels') - parser.add_argument('--local_discovery_enabled',type=str, - default='False', choices=['True','False'], - help='Look on the local lan') - parser.add_argument('--compact_mode',type=str, - default='True', choices=['True','False'], - help='Compact mode') - parser.add_argument('--allow_inline',type=str, - default='False', choices=['True','False'], - help='Dis/Enable allow_inline') - parser.add_argument('--notifications',type=str, - default='True', choices=['True','False'], - help='Dis/Enable notifications') - parser.add_argument('--sound_notifications',type=str, - default='True', choices=['True','False'], - help='Enable sound notifications') - parser.add_argument('--calls_sound',type=str, - default='True', choices=['True','False'], - help='Enable calls_sound') - parser.add_argument('--core_logging',type=str, - default='False', choices=['True','False'], - help='Dis/Enable Toxcore notifications') - parser.add_argument('--save_history',type=str, - default='True', choices=['True','False'], - help='En/Disable save history') - parser.add_argument('--update', type=int, default=0, - choices=[0,0], - help='Update program (broken)') - parser.add_argument('--video_input', type=str, - default=-1, - choices=default_video, - help="Video input device number - /dev/video?") - parser.add_argument('--audio_input', type=str, - default=oPYA.get_default_input_device_info()['name'], - choices=audio['input_devices'].values(), - help="Audio input device name - aplay -L for help") - parser.add_argument('--audio_output', type=str, - default=oPYA.get_default_output_device_info()['index'], - choices=audio['output_devices'].values(), - help="Audio output device number - -1 for help") - parser.add_argument('--theme', type=str, default='default', - choices=['dark', 'default'], - help='Theme - style of UI') -# parser.add_argument('--sleep', type=str, default='time', -# # could expand this to tk, gtk, gevent... -# choices=['qt','gevent','time'], -# help='Sleep method - one of qt, gevent , time') - supported_languages = settings.supported_languages() - parser.add_argument('--language', type=str, default='English', - choices=supported_languages, - help='Languages') - parser.add_argument('profile', type=str, nargs='?', default=None, - help='Path to Tox profile') - return parser - -# clean out the unchanged settings so these can override the profile -lKEEP_SETTINGS = ['uri', - 'profile', - 'loglevel', - 'logfile', - 'mode', - - # dunno - 'audio_input', - 'audio_output', - 'audio', - 'video', - - 'ipv6_enabled', - 'udp_enabled', - 'local_discovery_enabled', - 'trace_enabled', - - 'theme', - 'network', - 'message_font_size', - 'font', - 'save_history', - 'language', - 'update', - 'proxy_host', - 'proxy_type', - 'proxy_port', - 'core_logging', - 'audio', - 'video' - ] # , 'nodes_json' - -class A(): pass - -def main(lArgs=None) -> int: - global oPYA - from argparse import Namespace - if lArgs is None: - lArgs = sys.argv[1:] - parser = main_parser() - default_ns = parser.parse_args([]) - oArgs = parser.parse_args(lArgs) - - if oArgs.version: - print_toxygen_version() - return 0 - - if oArgs.clean: - clean() - return 0 - - if oArgs.reset: - reset() - return 0 - - # if getattr(oArgs, 'network') in ['newlocal', 'localnew']: oArgs.network = 'new' - - # clean out the unchanged settings so these can override the profile - for key in default_ns.__dict__.keys(): - if key in lKEEP_SETTINGS: continue - if not hasattr(oArgs, key): continue - if getattr(default_ns, key) == getattr(oArgs, key): - delattr(oArgs, key) - - ts.clean_booleans(oArgs) - - aArgs = A() - for key in oArgs.__dict__.keys(): - setattr(aArgs, key, getattr(oArgs, key)) - - #setattr(aArgs, 'video', setup_video(oArgs)) - aArgs.video = setup_video(oArgs) - assert 'video' in aArgs.__dict__ - - #setattr(aArgs, 'audio', setup_audio(oArgs)) - aArgs.audio = setup_audio(oArgs) - assert 'audio' in aArgs.__dict__ - oArgs = aArgs - - oApp = app.App(__version__, oArgs) - # for pyqtconsole - try: - setattr(__builtins__, 'app', oApp) - except Exception as e: - pass - i = oApp.iMain() - return i - -if __name__ == '__main__': - iRet = 0 - try: - iRet = main(sys.argv[1:]) - except KeyboardInterrupt: - iRet = 0 - except SystemExit as e: - iRet = e - except Exception as e: - import traceback - sys.stderr.write(f"Exception from main {e}" \ - +'\n' + traceback.format_exc() +'\n' ) - iRet = 1 - - # Exception ignored in: - # File "/usr/lib/python3.9/threading.py", line 1428, in _shutdown - # lock.acquire() - # gevent.exceptions.LoopExit as e: - # This operation would block forever - sys.stderr.write('Calling sys.exit' +'\n') - with ts.ignoreStdout(): - sys.exit(iRet) diff --git a/toxygen/app.py b/toxygen/app.py deleted file mode 100644 index 4326849..0000000 --- a/toxygen/app.py +++ /dev/null @@ -1,1050 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import os -import sys -import traceback -import logging -from random import shuffle -import threading -from time import sleep, time -from copy import deepcopy - -# used only in loop -import gevent - -from qtpy import QtWidgets, QtGui, QtCore -from qtpy.QtCore import QTimer -from qtpy.QtWidgets import QApplication - -__version__ = "1.0.0" - -try: - import coloredlogs - if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: - os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' - # https://pypi.org/project/coloredlogs/ -except ImportError as e: - coloredlogs = False - -try: - # https://github.com/pyqtconsole/pyqtconsole - from pyqtconsole.console import PythonConsole -except Exception as e: - PythonConsole = None - -try: - import qdarkstylexxx -except ImportError: - qdarkstyle = None - -from middleware import threads -import middleware.callbacks as callbacks -import updater.updater as updater -from middleware.tox_factory import tox_factory -import toxygen_wrapper.toxencryptsave as tox_encrypt_save -import user_data.toxes -from user_data import settings -from user_data.settings import get_user_config_path, merge_args_into_settings -from user_data.settings import Settings -from user_data.profile_manager import ProfileManager - -from plugin_support.plugin_support import PluginLoader - -import ui.password_screen as password_screen -from ui.login_screen import LoginScreen -from ui.main_screen import MainWindow -from ui import tray - -import utils.ui as util_ui -import utils.util as util -from av.calls_manager import CallsManager -from common.provider import Provider -from contacts.contact_provider import ContactProvider -from contacts.contacts_manager import ContactsManager -from contacts.friend_factory import FriendFactory -from contacts.group_factory import GroupFactory -from contacts.group_peer_factory import GroupPeerFactory -from contacts.profile import Profile -from file_transfers.file_transfers_handler import FileTransfersHandler -from file_transfers.file_transfers_messages_service import FileTransfersMessagesService -from groups.groups_service import GroupsService -from history.database import Database -from history.history import History -from messenger.messenger import Messenger -from network.tox_dns import ToxDns -from smileys.smileys import SmileyLoader -from ui.create_profile_screen import CreateProfileScreen -from ui.items_factories import MessagesItemsFactory, ContactItemsFactory -from ui.widgets_factory import WidgetsFactory -from user_data.backup_service import BackupService -import styles.style # TODO: dynamic loading - -import toxygen_wrapper.tests.support_testing as ts - -global LOG -LOG = logging.getLogger('app') - -IDLE_PERIOD = 0.10 -iNODES=8 -bSHOW_TRAY=False - -def setup_logging(oArgs) -> None: - global LOG - logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S', - fmt='%(levelname)s:%(name)s %(message)s') - logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' - logging._defaultFormatter.default_msec_format = '' - - if coloredlogs: - aKw = dict(level=oArgs.loglevel, - logger=LOG, - fmt='%(name)s %(levelname)s %(message)s') - aKw['stream'] = sys.stdout - coloredlogs.install(**aKw) - - else: - aKw = dict(level=oArgs.loglevel, - format='%(name)s %(levelname)-4s %(message)s') - aKw['stream'] = sys.stdout - logging.basicConfig(**aKw) - - if oArgs.logfile: - oFd = open(oArgs.logfile, 'wt') - setattr(oArgs, 'log_oFd', oFd) - oHandler = logging.StreamHandler(stream=oFd) - LOG.addHandler(oHandler) - - LOG.setLevel(oArgs.loglevel) - LOG.trace = lambda l: LOG.log(0, repr(l)) - LOG.info(f"Setting loglevel to {oArgs.loglevel}") - - if oArgs.loglevel < 20: - # opencv debug - sys.OpenCV_LOADER_DEBUG = True - -#? with ignoreStderr(): for png -# silence logging PyQt5.uic.uiparser -logging.getLogger('PyQt5.uic').setLevel(logging.ERROR) -logging.getLogger('PyQt5.uic.uiparser').setLevel(logging.ERROR) -logging.getLogger('PyQt5.uic.properties').setLevel(logging.ERROR) - -global iI -iI = 0 - -sSTYLE = """ -.QWidget {font-family Helvetica;} -.QCheckBox { font-family Helvetica;} -.QComboBox { font-family Helvetica;} -.QGroupBox { font-family Helvetica;} -.QLabel {font-family Helvetica;} -.QLineEdit { font-family Helvetica;} -.QListWidget { font-family Helvetica;} -.QListWidgetItem { font-family Helvetica;} -.QMainWindow {font-family Helvetica;} -.QMenu {font-family Helvetica;} -.QMenuBar {font-family Helvetica;} -.QPlainText {font-family Courier; weight: 75;} -.QPlainTextEdit {font-family Courier;} -.QPushButton {font-family Helvetica;} -.QRadioButton { font-family Helvetica; } -.QText {font-family Courier; weight: 75; } -.QTextBrowser {font-family Courier; weight: 75; } -.QTextSingleLine {font-family Courier; weight: 75; } -.QToolBar { font-weight: bold; } -""" -class App: - - def __init__(self, version, oArgs): - global LOG - self._args = oArgs - self.oArgs = oArgs - self._path = path_to_profile = oArgs.profile - uri = oArgs.uri - logfile = oArgs.logfile - loglevel = oArgs.loglevel - - setup_logging(oArgs) - # sys.stderr.write( 'Command line args: ' +repr(oArgs) +'\n') - LOG.info("Command line: " +' '.join(sys.argv[1:])) - LOG.debug(f'oArgs = {oArgs}') - LOG.info("Starting toxygen version " +version) - - self._version = version - self._tox = None - self._app = self._settings = self._profile_manager = None - self._plugin_loader = self._messenger = None - self._tox = self._ms = self._init = self._main_loop = self._av_loop = None - self._uri = self._toxes = self._tray = None - self._file_transfer_handler = self._contacts_provider = None - self._friend_factory = self._calls_manager = None - self._contacts_manager = self._smiley_loader = None - self._group_peer_factory = self._tox_dns = self._backup_service = None - self._group_factory = self._groups_service = self._profile = None - if uri is not None and uri.startswith('tox:'): - self._uri = uri[4:] - self._history = None - self.bAppExiting = False - - # Public methods - - def set_trace(self) -> None: - """unused""" - LOG.debug('pdb.set_trace ') - sys.stdin = sys.__stdin__ - sys.stdout = sys.__stdout__ - import pdb; pdb.set_trace() - - def ten(self, i=0) -> None: - """unused""" - global iI - iI += 1 - if logging.getLogger('app').getEffectiveLevel() != 10: - sys.stderr.write('CHANGED '+str(logging.getLogger().level+'\n')) - LOG.setLevel(10) - LOG.root.setLevel(10) - logging.getLogger('app').setLevel(10) - #sys.stderr.write(f"ten '+str(iI)+' {i}"+' '+repr(LOG) +'\n') - #LOG.debug('ten '+str(iI)) - - def iMain(self) -> int: - """ - Main function of app. loads login screen if needed and starts main screen - """ - self._app = QApplication([]) - self._load_icon() - - # is this still needed? - if util.get_platform() == 'Linux' and \ - hasattr(QtCore.Qt, 'AA_X11InitThreads'): - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) - - self._load_base_style() - - encrypt_save = tox_encrypt_save.ToxEncryptSave() - self._toxes = user_data.toxes.ToxES(encrypt_save) - try: - # this throws everything as errors - if not self._select_and_load_profile(): - return 2 - if hasattr(self._args, 'update') and self._args.update: - if self._try_to_update(): return 3 - - self._load_app_styles() - if self._args.language != 'English': - # (Pdb) Fatal Python error: Segmentation fault - self._load_app_translations() - self._create_dependencies() - - self._start_threads(True) - - if self._uri is not None: - self._ms.add_contact(self._uri) - except Exception as e: - LOG.error(f"Error loading profile: {e}") - sys.stderr.write(' iMain(): ' +f"Error loading profile: {e}" \ - +'\n' + traceback.format_exc()+'\n') - util_ui.message_box(str(e), - util_ui.tr('Error loading profile')) - return 4 - - self._app.lastWindowClosed.connect(self._app.quit) - try: - self._execute_app() - self.quit() - retval = 0 - except KeyboardInterrupt: - retval = 0 - except Exception: - retval = 1 - - return retval - - # App executing - - def _execute_app(self) -> None: - LOG.debug("_execute_app") - - while True: - try: - self._app.exec_() - except Exception as ex: - LOG.error('Unhandled exception: ' + str(ex)) - else: - break - - def quit(self, retval=0) -> None: - LOG.debug("quit") - self._stop_app() - - # failsafe: segfaults on exit - maybe it's Qt - if hasattr(self, '_tox'): - if self._tox and hasattr(self._tox, 'kill'): - LOG.debug(f"quit: Killing {self._tox}") - self._tox.kill() - del self._tox - - if hasattr(self, '_app'): - self._app.quit() - del self._app.quit - del self._app - - sys.stderr.write('quit raising SystemExit' +'\n') - # hanging on gevents - # Thread 1 "python3.9" received signal SIGSEGV, Segmentation fault. - #44 0x00007ffff7fb2f93 in () at /usr/lib/python3.9/site-packages/greenlet/_greenlet.cpython-39-x86_64-linux-gnu.so - #45 0x00007ffff7fb31ef in () at /usr/lib/python3.9/site-packages/greenlet/_greenlet.cpython-39-x86_64-linux-gnu.so - #46 0x00007ffff452165c in hb_shape_plan_create_cached2 () at /usr/lib64/libharfbuzz.so.0 - - raise SystemExit(retval) - - def _stop_app(self) -> None: - LOG.debug("_stop_app") - self._save_profile() - self._history.save_history() - - self._plugin_loader.stop() - try: - self._stop_threads(is_app_closing=True) - except (Exception, RuntimeError): - # RuntimeError: cannot join current thread - pass - # I think there are threads still running here leading to a SEGV - # File "/usr/lib/python3.11/threading.py", line 1401 in run - # File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner - # File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap - - if hasattr(self, '_tray') and self._tray: - self._tray.hide() - self._settings.close() - - self.bAppExiting = True - LOG.debug(f"stop_app: Killing {self._tox}") - self._kill_toxav() - self._kill_tox() - del self._tox - - oArgs = self._args - if hasattr(oArgs, 'log_oFd'): - LOG.debug(f"Closing {oArgs.log_oFd}") - oArgs.log_oFd.close() - delattr(oArgs, 'log_oFd') - - # App loading - - def _load_base_style(self) -> None: - if self._args.theme in ['', 'default']: return - - if qdarkstyle: - LOG.debug("_load_base_style qdarkstyle " +self._args.theme) - # QDarkStyleSheet - if self._args.theme == 'light': - from qdarkstyle.light.palette import LightPalette - style = qdarkstyle.load_stylesheet(palette=LightPalette) - else: - from qdarkstyle.dark.palette import DarkPalette - style = qdarkstyle.load_stylesheet(palette=DarkPalette) - else: - LOG.debug("_load_base_style qss " +self._args.theme) - name = self._args.theme + '.qss' - with open(util.join_path(util.get_styles_directory(), name)) as fl: - style = fl.read() - style += '\n' +sSTYLE - self._app.setStyleSheet(style) - - def _load_app_styles(self) -> None: - LOG.debug(f"_load_app_styles {list(settings.built_in_themes().keys())}") - # application color scheme - if self._settings['theme'] in ['', 'default']: return - for theme in settings.built_in_themes().keys(): - if self._settings['theme'] != theme: - continue - if qdarkstyle: - LOG.debug("_load_base_style qdarkstyle " +self._args.theme) - # QDarkStyleSheet - if self._args.theme == 'light': - from qdarkstyle.light.palette import LightPalette - style = qdarkstyle.load_stylesheet(palette=LightPalette) - else: - from qdarkstyle.dark.palette import DarkPalette - style = qdarkstyle.load_stylesheet(palette=DarkPalette) - else: - theme_path = settings.built_in_themes()[theme] - file_path = util.join_path(util.get_styles_directory(), theme_path) - if not os.path.isfile(file_path): - LOG.warn('_load_app_styles: no theme file ' + file_path) - continue - with open(file_path) as fl: - style = fl.read() - LOG.debug('_load_app_styles: loading theme file ' + file_path) - style += '\n' +sSTYLE - self._app.setStyleSheet(style) - LOG.info('_load_app_styles: loaded theme ' +self._args.theme) - break - - def _load_login_screen_translations(self) -> None: - LOG.debug("_load_login_screen_translations") - 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) -> None: - LOG.debug("_load_icon") - icon_file = os.path.join(util.get_images_directory(), 'icon.png') - self._app.setWindowIcon(QtGui.QIcon(icon_file)) - - @staticmethod - def _get_languages() -> tuple: - LOG.debug("_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) -> None: - LOG.debug("_load_app_translations") - lang = settings.supported_languages()[self._settings['language']] - translator = QtCore.QTranslator() - translator.load(os.path.join(util.get_translations_directory(), lang)) - self._app.installTranslator(translator) - self._app.translator = translator - - def _select_and_load_profile(self) -> bool: - LOG.debug("_select_and_load_profile: " +repr(self._path)) - - if self._path is not None: - # toxygen was started with path to profile - try: - assert os.path.exists(self._path), f"FNF {self._path}" - self._load_existing_profile(self._path) - except Exception as e: - LOG.error('_load_existing_profile failed: ' + str(e)) - title = 'Loading the profile failed ' - if self._path: - title += os.path.basename(self._path) - text = 'Loading the profile failed - \n' +str(e) - if 'Dis' == 'Abled': - text += '\nLoading the profile failed - \n' \ - +str(e) +'\nContinue with a default profile?' - reply = util_ui.question(text, title) - if not reply: - LOG.debug('_load_existing_profile not continuing ') - raise - LOG.debug('_load_existing_profile continuing ') - # drop through - else: - util_ui.message_box(text, title) - raise - else: - auto_profile = Settings.get_auto_profile() - if auto_profile is None: # no default profile - LOG.debug('_select_and_load_profile no default profile ') - result = self._select_profile() - if result is None: - LOG.debug('no selected profile ') - return False - if result.is_new_profile(): # create new profile - if not self._create_new_profile(result.profile_path): - LOG.warn('no new profile ') - return False - LOG.debug('created new profile ') - else: # load existing profile - self._load_existing_profile(result.profile_path) - # drop through - self._path = result.profile_path - else: # default profile - LOG.debug('loading default profile ') - self._path = auto_profile - self._load_existing_profile(auto_profile) - - if settings.is_active_profile(self._path): # profile is in use - LOG.warn(f"_select_and_load_profile active: {self._path}") - profile_name = util.get_profile_name_from_path(self._path) - title = util_ui.tr('Profile {}').format(profile_name) - text = util_ui.tr( - '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 - - # is self._path right - was pathless - self._settings.set_active_profile(self._path) - - return True - - # Threads - - def _start_threads(self, initial_start=True) -> None: - LOG.debug(f"_start_threads before: {threading.enumerate()}") - # init thread - self._init = threads.InitThread(self._tox, - self._plugin_loader, - self._settings, - self, - initial_start) - self._init.start() - def te(): return [t.name for t in threading.enumerate()] - LOG.debug(f"_start_threads init: {te()}") - - # starting threads for tox iterate and toxav iterate - self._main_loop = threads.ToxIterateThread(self._tox, app=self) - self._main_loop.start() - - self._av_loop = threads.ToxAVIterateThread(self._tox.AV) - self._av_loop.start() - - if initial_start: - threads.start_file_transfer_thread() - LOG.debug(f"_start_threads after: {[t.name for t in threading.enumerate()]}") - - def _stop_threads(self, is_app_closing=True) -> None: - LOG.debug("_stop_threads") - self._init.stop_thread(1.0) - - self._av_loop.stop_thread() - self._main_loop.stop_thread() - - if is_app_closing: - threads.stop_file_transfer_thread() - - def iterate(self, n=100) -> None: - interval = self._tox.iteration_interval() - for i in range(n): - self._tox.iterate() - # Cooperative yield, allow gevent to monitor file handles via libevent - gevent.sleep(interval / 1000.0) -#? sleep(interval / 1000.0) - - # Profiles - - def _select_profile(self): - LOG.debug("_select_profile") - if self._args.language != 'English': - self._load_login_screen_translations() - ls = LoginScreen() - profiles = ProfileManager.find_profiles() - ls.update_select(profiles) - ls.show() - self._app.exec_() - return ls.result - - def _load_existing_profile(self, profile_path) -> None: - profile_path = profile_path.replace('.json', '.tox') - LOG.info("_load_existing_profile " +repr(profile_path)) - assert os.path.exists(profile_path), profile_path - self._profile_manager = ProfileManager(self._toxes, profile_path, app=self) - data = self._profile_manager.open_profile() - if self._toxes.is_data_encrypted(data): - LOG.debug("_entering password") - data = self._enter_password(data) - LOG.debug("_entered password") - json_file = profile_path.replace('.tox', '.json') - if os.path.exists(json_file): - LOG.debug("creating _settings from: " +json_file) - self._settings = Settings(self._toxes, json_file, self) - else: - self._settings = Settings.get_default_settings() - - self._tox = self._create_tox(data, self._settings) - LOG.debug("created _tox") - - def _create_new_profile(self, profile_name) -> bool: - LOG.info("_create_new_profile " + profile_name) - result = self._get_create_profile_screen_result() - if result is None: - return False - if result.save_into_default_folder: - profile_path = util.join_path(get_user_config_path(), profile_name + '.tox') - else: - profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') - if os.path.isfile(profile_path): - 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' - assert self._args - self._path = profile_path - if result.password: - self._toxes.set_password(result.password) - self._settings = Settings(self._toxes, - self._path.replace('.tox', '.json'), - app=self) - self._tox = self._create_tox(None, - self._settings) - self._tox.self_set_name(name if name else 'Toxygen User') - self._tox.self_set_status_message('Toxing on Toxygen') - - self._profile_manager = ProfileManager(self._toxes, profile_path) - try: - self._save_profile() - except Exception as ex: - #? print(ex) - LOG.error('Profile creation exception: ' + str(ex)) - text = util_ui.tr('Profile saving error! Does Toxygen have permission to write to this directory?') - util_ui.message_box(text, util_ui.tr('Error')) - - 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): - LOG.debug("_get_create_profile_screen_result") - cps = CreateProfileScreen() - cps.show() - self._app.exec_() - - return cps.result - - def _save_profile(self, data=None) -> None: - LOG.debug("_save_profile") - data = data or self._tox.get_savedata() - self._profile_manager.save_profile(data) - - # Other private methods - - def _enter_password(self, data): - """ - Show password screen - """ - LOG.debug("_enter_password") - p = password_screen.PasswordScreen(self._toxes, data) - p.show() - self._app.lastWindowClosed.connect(self._app.quit) - self._app.exec_() - if p.result is not None: - return p.result - self._force_exit(0) - return None - - def _reset(self) -> None: - LOG.debug("_reset") - """ - 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() - try: - # create new tox instance - self._tox = self._create_tox(data, self._settings) - assert self._tox - self._start_threads(False) - - tox_savers = [self._friend_factory, self._group_factory, - self._plugin_loader, self._contacts_manager, - self._contacts_provider, self._messenger, - self._file_transfer_handler, - self._groups_service, self._profile] - for tox_saver in tox_savers: - tox_saver.set_tox(self._tox) - - 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() - except BaseException as e: - LOG.error(f"_reset : {e}") - LOG.debug('_reset: ' \ - +'\n' + traceback.format_exc()) - title = util_ui.tr('Reset Error') - text = util_ui.tr('Error:') + str(e) - util_ui.message_box(text, title) - - def _create_dependencies(self) -> None: - LOG.info(f"_create_dependencies toxygen version {self._version}") - if hasattr(self._args, 'update') and self._args.update: - self._backup_service = BackupService(self._settings, - self._profile_manager) - self._smiley_loader = SmileyLoader(self._settings) - self._tox_dns = ToxDns(self._settings) - self._ms = MainWindow(self._settings, self._tray, self) - - db_path = self._path.replace('.tox', '.db') - db = Database(db_path, self._toxes) - if os.path.exists(db_path) and hasattr(db, 'open'): - db.open() - - assert self._tox - - contact_items_factory = ContactItemsFactory(self._settings, self._ms) - self._friend_factory = FriendFactory(self._profile_manager, - self._settings, - self._tox, - db, - contact_items_factory) - self._group_factory = GroupFactory(self._profile_manager, - self._settings, - self._tox, - db, - contact_items_factory) - self._group_peer_factory = GroupPeerFactory(self._tox, - self._profile_manager, - db, - contact_items_factory) - self._contacts_provider = ContactProvider(self._tox, - self._friend_factory, - self._group_factory, - self._group_peer_factory, - app=self) - 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._history = history - self._calls_manager = CallsManager(self._tox.AV, - self._settings, - self._ms, - self._contacts_manager, - self) - self._messenger = Messenger(self._tox, - self._plugin_loader, self._ms, self._contacts_manager, - self._contacts_provider, messages_items_factory, self._profile, - self._calls_manager) - file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, - 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) - if bSHOW_TRAY: - 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) - - if bSHOW_TRAY: # broken - # the tray icon does not die with the app - self._tray.show() - self._ms.show() - - # FixMe: - self._log = lambda line: LOG.log(self._args.loglevel, - self._ms.status(line)) - # self._ms._log = self._log # was used in callbacks.py - - if False: - self.status_handler = logging.Handler() - self.status_handler.setLevel(logging.INFO) # self._args.loglevel - self.status_handler.handle = self._ms.status - - self._init_callbacks() - LOG.info("_create_dependencies toxygen version " +self._version) - - def _try_to_update(self): - LOG.debug("_try_to_update") - updating = updater.start_update_if_needed(self._version, self._settings) - if updating: - LOG.info("Updating toxygen version " +self._version) - self._save_profile() - self._settings.close() - self._kill_toxav() - self._kill_tox() - return updating - - def _create_tox(self, data, settings_): - LOG.info("_create_tox calling tox_factory") - assert self._args - retval = tox_factory(data=data, settings=settings_, - args=self._args, app=self) - LOG.debug("_create_tox succeeded") - self._tox = retval - return retval - - def _force_exit(self, retval=0) -> None: - LOG.debug("_force_exit") - sys.exit(0) - - def _init_callbacks(self, ms=None) -> None: - LOG.debug("_init_callbacks") - # this will block if you are not connected - callbacks.init_callbacks(self._tox, self._profile, self._settings, - self._plugin_loader, self._contacts_manager, - self._calls_manager, - self._file_transfer_handler, self._ms, - self._tray, - self._messenger, self._groups_service, - self._contacts_provider, self._ms) - - def _init_profile(self) -> None: - LOG.debug("_init_profile") - if not self._profile.has_avatar(): - self._profile.reset_avatar(self._settings['identicons']) - - def _kill_toxav(self) -> None: -# LOG_debug("_kill_toxav") - self._calls_manager.set_toxav(None) - self._tox.AV.kill() - - def _kill_tox(self) -> None: -# LOG.debug("_kill_tox") - self._tox.kill() - - def loop(self, n) -> None: - """ - Im guessing - there are 4 sleeps - time, tox, and Qt gevent - """ - interval = self._tox.iteration_interval() - for i in range(n): - self._tox.iterate() - #? QtCore.QThread.msleep(interval) - # Cooperative yield, allow gevent to monitor file handles via libevent - gevent.sleep(interval / 1000.0) - # NO? - QtCore.QCoreApplication.processEvents() - - def _test_tox(self) -> None: - self.test_net(iMax=8) - self._ms.log_console() - - def test_net(self, lElts=None, oThread=None, iMax=4) -> None: - - # bootstrap - LOG.debug('test_net: Calling generate_nodes: udp') - lNodes = ts.generate_nodes(oArgs=self._args, - ipv='ipv4', - udp_not_tcp=True) - self._settings['current_nodes_udp'] = lNodes - if not lNodes: - LOG.warn('empty generate_nodes udp') - LOG.debug('test_net: Calling generate_nodes: tcp') - lNodes = ts.generate_nodes(oArgs=self._args, - ipv='ipv4', - udp_not_tcp=False) - self._settings['current_nodes_tcp'] = lNodes - if not lNodes: - LOG.warn('empty generate_nodes tcp') - - # if oThread and oThread._stop_thread: return - LOG.debug("test_net network=" +self._args.network +' iMax=' +str(iMax)) - if self._args.network not in ['local', 'localnew', 'newlocal']: - b = ts.bAreWeConnected() - if b is None: - i = os.system('ip route|grep ^def') - if i > 0: - b = False - else: - b = True - if not b: - LOG.warn("No default route for network " +self._args.network) - text = 'You have no default route - are you connected?' - reply = util_ui.question(text, "Are you connected?") - if not reply: return - iMax = 1 - else: - LOG.debug("Have default route for network " +self._args.network) - - lUdpElts = self._settings['current_nodes_udp'] - if self._args.proxy_type <= 0 and not lUdpElts: - title = 'test_net Error' - text = 'Error: ' + str('No UDP nodes') - util_ui.message_box(text, title) - return - lTcpElts = self._settings['current_nodes_tcp'] - if self._args.proxy_type > 0 and not lTcpElts: - title = 'test_net Error' - text = 'Error: ' + str('No TCP nodes') - util_ui.message_box(text, title) - return - LOG.debug(f"test_net {self._args.network} lenU={len(lUdpElts)} lenT={len(lTcpElts)} iMax={iMax}") - i = 0 - while i < iMax: - # if oThread and oThread._stop_thread: return - i = i + 1 - LOG.debug(f"bootstrapping status proxy={self._args.proxy_type} # {i}") - if self._args.proxy_type == 0: - self._test_bootstrap(lUdpElts) - else: - self._test_bootstrap([lUdpElts[0]]) - LOG.debug(f"relaying status # {i}") - self._test_relays(self._settings['current_nodes_tcp']) - status = self._tox.self_get_connection_status() - if status > 0: - LOG.info(f"Connected # {i}" +' : ' +repr(status)) - break - LOG.trace(f"Connected status #{i}: {status}") - - def _test_env(self) -> None: - _settings = self._settings - if 'proxy_type' not in _settings or _settings['proxy_type'] == 0 or \ - not _settings['proxy_host'] or not _settings['proxy_port']: - env = dict( prot = 'ipv4') - lElts = self._settings['current_nodes_udp'] - elif _settings['proxy_type'] == 2: - env = dict(prot = 'socks5', - https_proxy='', \ - socks_proxy='socks5://' \ - +_settings['proxy_host'] +':' \ - +str(_settings['proxy_port'])) - lElts = self._settings['current_nodes_tcp'] - elif _settings['proxy_type'] == 1: - env = dict(prot = 'https', - socks_proxy='', \ - https_proxy='http://' \ - +_settings['proxy_host'] +':' \ - +str(_settings['proxy_port'])) - lElts = _settings['current_nodes_tcp'] -# LOG.debug(f"test_env {len(lElts)}") - return env - - def _test_bootstrap(self, lElts=None) -> None: - if lElts is None: - lElts = self._settings['current_nodes_udp'] - LOG.debug(f"_test_bootstrap #Elts={len(lElts)}") - if not lElts: - return - shuffle(lElts) - ts.bootstrap_udp(lElts[:iNODES], [self._tox]) - LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) - - def _test_relays(self, lElts=None) -> None: - if lElts is None: - lElts = self._settings['current_nodes_tcp'] - shuffle(lElts) - LOG.debug(f"_test_relays {len(lElts)}") - ts.bootstrap_tcp(lElts[:iNODES], [self._tox]) - - def _test_nmap(self, lElts=None) -> None: - LOG.debug("_test_nmap") - if not self._tox: return - title = 'Extended Test Suite' - text = 'Run the Extended Test Suite?\nThe program may freeze for 1-10 minutes.' - i = os.system('ip route|grep ^def >/dev/null') - if i > 0: - text += '\nYou have no default route - are you connected?' - reply = util_ui.question(text, title) - if not reply: return - - if self._args.proxy_type == 0: - sProt = "udp4" - else: - sProt = "tcp4" - if lElts is None: - if self._args.proxy_type == 0: - lElts = self._settings['current_nodes_udp'] - else: - lElts = self._settings['current_nodes_tcp'] - shuffle(lElts) - try: - ts.bootstrap_iNmapInfo(lElts, self._args, sProt) - except Exception as e: - LOG.error(f"test_nmap ' +' : {e}") - LOG.error('_test_nmap(): ' \ - +'\n' + traceback.format_exc()) - title = 'Test Suite Error' - text = 'Error: ' + str(e) - util_ui.message_box(text, title) - - # LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) - self._ms.log_console() - - def _test_main(self) -> None: - from toxygen_toxygen_wrapper.toxygen_wrapper.tests.tests_wrapper import main as tests_main - LOG.debug("_test_main") - if not self._tox: return - title = 'Extended Test Suite' - text = 'Run the Extended Test Suite?\nThe program may freeze for 20-60 minutes.' - reply = util_ui.question(text, title) - if reply: - if hasattr(self._args, 'proxy_type') and self._args.proxy_type: - lArgs = ['--proxy_host', self._args.proxy_host, - '--proxy_port', str(self._args.proxy_port), - '--proxy_type', str(self._args.proxy_type), ] - else: - lArgs = list() - try: - tests_main(lArgs) - except Exception as e: - LOG.error(f"_test_socks(): {e}") - LOG.error('_test_socks(): ' \ - +'\n' + traceback.format_exc()) - title = 'Extended Test Suite Error' - text = 'Error:' + str(e) - util_ui.message_box(text, title) - self._ms.log_console() - -#? unused -class GEventProcessing: - """Interoperability class between Qt/gevent that allows processing gevent - tasks during Qt idle periods.""" - def __init__(self, idle_period=IDLE_PERIOD): - # Limit the IDLE handler's frequency while still allow for gevent - # to trigger a microthread anytime - self._idle_period = idle_period - # IDLE timer: on_idle is called whenever no Qt events left for - # processing - self._timer = QTimer() - self._timer.timeout.connect(self.process_events) - self._timer.start(0) - def __enter__(self) -> None: - pass - - def __exit__(self, *exc_info) -> None: - self._timer.stop() - - def process_events(self, idle_period=None) -> None: - if idle_period is None: - idle_period = self._idle_period - # Cooperative yield, allow gevent to monitor file handles via libevent - gevent.sleep(idle_period) - #? QtCore.QCoreApplication.processEvents() diff --git a/toxygen/av/__init__.py b/toxygen/av/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/av/call.py b/toxygen/av/call.py deleted file mode 100644 index 73caa25..0000000 --- a/toxygen/av/call.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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/av/calls.py b/toxygen/av/calls.py deleted file mode 100644 index 9b40fc1..0000000 --- a/toxygen/av/calls.py +++ /dev/null @@ -1,587 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import time -import threading -import logging -import itertools - -from toxygen_wrapper.toxav_enums import * -from toxygen_wrapper.tests import support_testing as ts -from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -with ts.ignoreStderr(): - import pyaudio -from av import screen_sharing -from av.call import Call -import common.tox_save -from middleware.threads import BaseQThread - -from utils import ui as util_ui -from middleware.threads import invoke_in_main_thread -# from middleware.threads import BaseThread - -sleep = time.sleep - -global LOG -LOG = logging.getLogger('app.'+__name__) - -TIMER_TIMEOUT = 30.0 -iFPS = 25 - -class AudioThread(BaseQThread): - def __init__(self, av, name=''): - super().__init__() - self.av = av - self._name = name - - def join(self, ito=ts.iTHREAD_TIMEOUT): - LOG_DEBUG(f"AudioThread join {self}") - # dunno - - def run(self) -> None: - LOG_DEBUG('AudioThread run: ') - # maybe not needed - while not self._stop_thread: - self.av.send_audio() - sleep(100.0 / 1000.0) - -class VideoThread(BaseQThread): - def __init__(self, av, name=''): - super().__init__() - self.av = av - self._name = name - - def join(self, ito=ts.iTHREAD_TIMEOUT): - LOG_DEBUG(f"VideoThread join {self}") - # dunno - - def run(self) -> None: - LOG_DEBUG('VideoThread run: ') - # maybe not needed - while not self._stop_thread: - self.av.send_video() - sleep(100.0 / 1000.0) - -class AV(common.tox_save.ToxAvSave): - - def __init__(self, toxav, settings): - super().__init__(toxav) - self._toxav = toxav - self._settings = settings - self._running = True - s = settings - if 'video' not in s: - LOG.warn("AV.__init__ 'video' not in s" ) - LOG.debug(f"AV.__init__ {s}" ) - elif 'device' not in s['video']: - LOG.warn("AV.__init__ 'device' not in s.video" ) - LOG.debug(f"AV.__init__ {s['video']}" ) - - self._calls = {} # dict: key - friend number, value - Call instance - - self._audio = None - self._audio_stream = None - self._audio_thread = None - self._audio_running = False - self._out_stream = None - - self._audio_channels = 1 - self._audio_duration = 60 - self._audio_rate_pa = 48000 - self._audio_rate_tox = 48000 - self._audio_rate_pa = 48000 - self._audio_krate_tox_audio = self._audio_rate_tox // 1000 - self._audio_krate_tox_video = 5000 - self._audio_sample_count_pa = self._audio_rate_pa * self._audio_channels * self._audio_duration // 1000 - self._audio_sample_count_tox = self._audio_rate_tox * self._audio_channels * self._audio_duration // 1000 - - self._video = None - self._video_thread = None - self._video_running = None - - self._video_width = 320 - self._video_height = 240 - - # was iOutput = self._settings._args.audio['output'] - iInput = self._settings['audio']['input'] - self.lPaSampleratesI = ts.lSdSamplerates(iInput) - iOutput = self._settings['audio']['output'] - self.lPaSampleratesO = ts.lSdSamplerates(iOutput) - - global oPYA - oPYA = self._audio = pyaudio.PyAudio() - - def stop(self) -> None: - LOG_DEBUG(f"AV.CA stop {self._video_thread}") - self._running = False - self.stop_audio_thread() - self.stop_video_thread() - - def __contains__(self, friend_number:int) -> bool: - return friend_number in self._calls - - # Calls - - def __call__(self, friend_number, audio, video): - """Call friend with specified number""" - if friend_number in self._calls: - LOG.warn(f"__call__ already has {friend_number}") - return - if self._audio_krate_tox_audio not in ts.lToxSampleratesK: - LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") - - try: - self._toxav.call(friend_number, - self._audio_krate_tox_audio if audio else 0, - self._audio_krate_tox_video if video else 0) - except Exception as e: - LOG.warn(f"_toxav.call already has {friend_number}") - return - self._calls[friend_number] = Call(audio, video) - threading.Timer(TIMER_TIMEOUT, - lambda: self.finish_not_started_call(friend_number)).start() - - def accept_call(self, friend_number, audio_enabled, video_enabled): - # obsolete - self.call_accept_call(friend_number, audio_enabled, video_enabled) - - def call_accept_call(self, friend_number, audio_enabled, video_enabled) -> None: - # called from CM.accept_call in a try: - LOG.debug(f"call_accept_call from F={friend_number} R={self._running}" + - f" A={audio_enabled} V={video_enabled}") - # import pdb; pdb.set_trace() - gets into q Qt exec_ problem - # ts.trepan_handler() - - if self._audio_krate_tox_audio not in ts.lToxSampleratesK: - LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") - if self._running: - self._calls[friend_number] = Call(audio_enabled, video_enabled) - # audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. - # video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. - try: - self._toxav.answer(friend_number, - self._audio_krate_tox_audio if audio_enabled else 0, - self._audio_krate_tox_video if video_enabled else 0) - except Exception as e: - LOG.error(f"AV accept_call error from {friend_number} {self._running} {e}") - raise - if video_enabled: - # may raise - self.start_video_thread() - if audio_enabled: - LOG.debug(f"calls accept_call calling start_audio_thread F={friend_number}") - # may raise - self.start_audio_thread() - - def finish_call(self, friend_number, by_friend=False) -> None: - LOG.debug(f"finish_call {friend_number}") - if friend_number in self._calls: - del self._calls[friend_number] - try: - # AttributeError: 'int' object has no attribute 'out_audio' - if not len(list(filter(lambda c: c.out_audio, self._calls))): - self.stop_audio_thread() - if not len(list(filter(lambda c: c.out_video, self._calls))): - self.stop_video_thread() - except Exception as e: - LOG.error(f"finish_call FixMe: {e}") - # dunno - self.stop_audio_thread() - self.stop_video_thread() - if not by_friend: - LOG.debug(f"finish_call before call_control {friend_number}") - self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) - LOG.debug(f"finish_call after call_control {friend_number}") - - def finish_not_started_call(self, friend_number:int) -> None: - if friend_number in self: - call = self._calls[friend_number] - if not call.is_active: - self.finish_call(friend_number) - - def toxav_call_state_cb(self, friend_number, state) -> None: - """ - New call state - """ - LOG.debug(f"toxav_call_state_cb {friend_number}") - call = self._calls[friend_number] - call.is_active = True - - call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A'] > 0 - call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V'] > 0 - - if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio: - self.start_audio_thread() - - if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video: - self.start_video_thread() - - def is_video_call(self, number) -> bool: - return number in self and self._calls[number].in_video - - # Threads - - def start_audio_thread(self, bSTREAM_CALLBACK=False) -> None: - """ - Start audio sending - from a callback - """ - # called from call_accept_call in an try: from CM.accept_call - global oPYA - # was iInput = self._settings._args.audio['input'] - iInput = self._settings['audio']['input'] - if self._audio_thread is not None: - LOG_WARN(f"start_audio_thread device={iInput}") - return - LOG_DEBUG(f"start_audio_thread device={iInput}") - lPaSamplerates = ts.lSdSamplerates(iInput) - if not(len(lPaSamplerates)): - e = f"No sample rates for device: audio[input]={iInput}" - LOG_WARN(f"start_audio_thread {e}") - #?? dunno - cancel call? - no let the user do it - # return - # just guessing here in case that's a false negative - lPaSamplerates = [round(oPYA.get_device_info_by_index(iInput)['defaultSampleRate'])] - if lPaSamplerates and self._audio_rate_pa in lPaSamplerates: - pass - elif lPaSamplerates: - LOG_WARN(f"Setting audio_rate to: {lPaSamplerates[0]}") - self._audio_rate_pa = lPaSamplerates[0] - elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iInput): - self._audio_rate_pa = oPYA.get_device_info_by_index(iInput)['defaultSampleRate'] - LOG_WARN(f"setting to defaultSampleRate") - else: - LOG_WARN(f"{self._audio_rate_pa} not in {lPaSamplerates}") - # a float is in here - must it be int? - if type(self._audio_rate_pa) == float: - self._audio_rate_pa = round(self._audio_rate_pa) - try: - if self._audio_rate_pa not in lPaSamplerates: - LOG_WARN(f"PAudio sampling rate was {self._audio_rate_pa} changed to {lPaSamplerates[0]}") - LOG_DEBUG(f"lPaSamplerates={lPaSamplerates}") - self._audio_rate_pa = lPaSamplerates[0] - else: - LOG_DEBUG( f"start_audio_thread framerate: {self._audio_rate_pa}" \ - +f" device: {iInput}" - +f" supported: {lPaSamplerates}") - - if bSTREAM_CALLBACK: - # why would you not call a thread? - self._audio_stream = oPYA.open(format=pyaudio.paInt16, - rate=self._audio_rate_pa, - channels=self._audio_channels, - input=True, - input_device_index=iInput, - frames_per_buffer=self._audio_sample_count_pa * 10, - stream_callback=self.send_audio_data) - self._audio_running = True - self._audio_stream.start_stream() - while self._audio_stream.is_active(): - sleep(0.1) - self._audio_stream.stop_stream() - self._audio_stream.close() - else: - LOG_DEBUG( f"start_audio_thread starting thread {self._audio_rate_pa}") - self._audio_stream = oPYA.open(format=pyaudio.paInt16, - rate=self._audio_rate_pa, - channels=self._audio_channels, - input=True, - input_device_index=iInput, - frames_per_buffer=self._audio_sample_count_pa * 10) - self._audio_running = True - self._audio_thread = AudioThread(self, - name='_audio_thread') - self._audio_thread.start() - LOG_DEBUG( f"start_audio_thread started thread name='_audio_thread'") - - except Exception as e: - LOG_ERROR(f"Starting self._audio.open {e}") - LOG_DEBUG(repr(dict(format=pyaudio.paInt16, - rate=self._audio_rate_pa, - channels=self._audio_channels, - input=True, - input_device_index=iInput, - frames_per_buffer=self._audio_sample_count_pa * 10))) - # catcher in place in calls_manager? yes accept_call - # calls_manager._call.toxav_call_state_cb(friend_number, mask) - invoke_in_main_thread(util_ui.message_box, - str(e), - util_ui.tr("Starting self._audio.open")) - return - else: - LOG_DEBUG(f"start_audio_thread {self._audio_stream}") - - def stop_audio_thread(self) -> None: - LOG_DEBUG(f"stop_audio_thread {self._audio_stream}") - - if self._audio_thread is None: - return - self._audio_running = False - self._audio_thread._stop_thread = True - - self._audio_thread = None - self._audio_stream = None - self._audio = None - - if self._out_stream is not None: - self._out_stream.stop_stream() - self._out_stream.close() - self._out_stream = None - - def start_video_thread(self) -> None: - if self._video_thread is not None: - return - s = self._settings - if 'video' not in s: - LOG.warn("AV.__init__ 'video' not in s" ) - LOG.debug(f"start_video_thread {s}" ) - raise RuntimeError("start_video_thread not 'video' in s)" ) - if 'device' not in s['video']: - LOG.error("start_video_thread not 'device' in s['video']" ) - LOG.debug(f"start_video_thread {s['video']}" ) - raise RuntimeError("start_video_thread not 'device' ins s['video']" ) - self._video_width = s['video']['width'] - self._video_height = s['video']['height'] - - # dunno - if s['video']['device'] == -1: - self._video = screen_sharing.DesktopGrabber(s['video']['x'], - s['video']['y'], - s['video']['width'], - s['video']['height']) - else: - with ts.ignoreStdout(): import cv2 - if s['video']['device'] == 0: - # webcam - self._video = cv2.VideoCapture(s['video']['device'], cv2.DSHOW) - else: - self._video = cv2.VideoCapture(s['video']['device']) - self._video.set(cv2.CAP_PROP_FPS, iFPS) - self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width) - self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height) -# self._video.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) - if self._video is None: - LOG.error("start_video_thread " \ - +f" device: {s['video']['device']}" \ - +f" supported: {s['video']['width']} {s['video']['height']}") - return - LOG.info("start_video_thread " \ - +f" device: {s['video']['device']}" \ - +f" supported: {s['video']['width']} {s['video']['height']}") - - self._video_running = True - self._video_thread = VideoThread(self, - name='_video_thread') - self._video_thread.start() - - def stop_video_thread(self) -> None: - LOG_DEBUG(f"stop_video_thread {self._video_thread}") - if self._video_thread is None: - return - - self._video_thread._stop_thread = True - self._video_running = False - i = 0 - while i < ts.iTHREAD_JOINS: - self._video_thread.join(ts.iTHREAD_TIMEOUT) - try: - if not self._video_thread.is_alive(): break - except: - break - i = i + 1 - else: - LOG.warn("self._video_thread.is_alive BLOCKED") - self._video_thread = None - self._video = None - # Incoming chunks - - def audio_chunk(self, samples, channels_count, rate) -> None: - """ - Incoming chunk - """ - # from callback - if self._out_stream is None: - # was iOutput = self._settings._args.audio['output'] - iOutput = self._settings['audio']['output'] - if self.lPaSampleratesO and rate in self.lPaSampleratesO: - LOG_DEBUG(f"Using rate {rate} in self.lPaSampleratesO") - elif self.lPaSampleratesO: - LOG_WARN(f"{rate} not in {self.lPaSampleratesO}") - LOG_WARN(f"Setting audio_rate to: {self.lPaSampleratesO[0]}") - rate = self.lPaSampleratesO[0] - elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iOutput): - rate = round(oPYA.get_device_info_by_index(iOutput)['defaultSampleRate']) - LOG_WARN(f"Setting rate to {rate} empty self.lPaSampleratesO") - else: - LOG_WARN(f"Using rate {rate} empty self.lPaSampleratesO") - if type(rate) == float: - rate = round(rate) - # test output device? - # [Errno -9985] Device unavailable - try: - with ts.ignoreStderr(): - self._out_stream = oPYA.open(format=pyaudio.paInt16, - channels=channels_count, - rate=rate, - output_device_index=iOutput, - output=True) - except Exception as e: - LOG_ERROR(f"Error playing audio_chunk creating self._out_stream output_device_index={iOutput} {e}") - invoke_in_main_thread(util_ui.message_box, - str(e), - util_ui.tr("Error Chunking audio")) - # dunno - self.stop() - return - - iOutput = self._settings['audio']['output'] -#trace LOG_DEBUG(f"audio_chunk output_device_index={iOutput} rate={rate} channels={channels_count}") - try: - self._out_stream.write(samples) - except Exception as e: - # OSError: [Errno -9999] Unanticipated host error - LOG_WARN(f"audio_chunk output_device_index={iOutput} {e}") - - # AV sending - - def send_audio_data(self, data, count, *largs, **kwargs) -> None: - # callback - pcm = data - # :param sampling_rate: Audio sampling rate used in this frame. - try: - if self._toxav is None: - LOG_ERROR("_toxav not initialized") - return - if self._audio_rate_tox not in ts.lToxSamplerates: - LOG_WARN(f"ToxAudio sampling rate was {self._audio_rate_tox} changed to {ts.lToxSamplerates[0]}") - self._audio_rate_tox = ts.lToxSamplerates[0] - - for friend_num in self._calls: - if self._calls[friend_num].out_audio: - # app.av.calls ERROR Error send_audio audio_send_frame: This client is currently not in a call with the friend. - self._toxav.audio_send_frame(friend_num, - pcm, - count, - self._audio_channels, - self._audio_rate_tox) - - except Exception as e: - LOG.error(f"Error send_audio_data audio_send_frame: {e}") - LOG.debug(f"send_audio_data self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}") - self.stop_audio_thread() - invoke_in_main_thread(util_ui.message_box, - str(e), - util_ui.tr("Error send_audio_data audio_send_frame")) - #? stop ? endcall? - - def send_audio(self) -> None: - """ - This method sends audio to friends - """ - i=0 - count = self._audio_sample_count_tox - LOG_DEBUG(f"send_audio stream={self._audio_stream}") - while self._audio_running: - try: - pcm = self._audio_stream.read(count, exception_on_overflow=False) - if not pcm: - sleep(0.1) - else: - self.send_audio_data(pcm, count) - except: - LOG_DEBUG(f"error send_audio {i}") - else: - LOG_TRACE(f"send_audio {i}") - i += 1 - sleep(0.01) - - def send_video(self) -> None: - """ - This method sends video to friends - """ -# LOG_DEBUG(f"send_video thread={threading.current_thread().name}" -# +f" self._video_running={self._video_running}" -# +f" device: {self._settings['video']['device']}" ) - while self._video_running: - try: - result, frame = self._video.read() - if not result: - LOG_WARN(f"send_video video_send_frame _video.read result={result}") - break - if frame is None: - LOG_WARN(f"send_video video_send_frame _video.read result={result} frame={frame}") - continue - - LOG_TRACE(f"send_video video_send_frame _video.read result={result}") - height, width, channels = frame.shape - friends = [] - for friend_num in self._calls: - if self._calls[friend_num].out_video: - friends.append(friend_num) - if len(friends) == 0: - LOG_WARN(f"send_video video_send_frame no friends") - else: - LOG_TRACE(f"send_video video_send_frame {friends}") - friend_num = friends[0] - try: - y, u, v = self.convert_bgr_to_yuv(frame) - self._toxav.video_send_frame(friend_num, width, height, y, u, v) - except Exception as e: - LOG_WARN(f"send_video video_send_frame ERROR {e}") - pass - - except Exception as e: - LOG_ERROR(f"send_video video_send_frame {e}") - pass - - sleep( 1.0/iFPS) - - def convert_bgr_to_yuv(self, frame) -> tuple: - """ - :param frame: input bgr frame - :return y, u, v: y, u, v values of frame - - How this function works: - OpenCV creates YUV420 frame from BGR - This frame has following structure and size: - width, height - dim of input frame - width, height * 1.5 - dim of output frame - - width - ------------------------- - | | - | Y | height - | | - ------------------------- - | | | - | U even | U odd | height // 4 - | | | - ------------------------- - | | | - | V even | V odd | height // 4 - | | | - ------------------------- - - width // 2 width // 2 - - Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable() - Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes - """ - with ts.ignoreStdout(): - import cv2 - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) - - y = frame[:self._video_height, :] - y = list(itertools.chain.from_iterable(y)) - - import numpy as np - # was np.int - u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) - u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2] - u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:] - u = list(itertools.chain.from_iterable(u)) - v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) - v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2] - v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:] - v = list(itertools.chain.from_iterable(v)) - - return bytes(y), bytes(u), bytes(v) diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py deleted file mode 100644 index d0d6683..0000000 --- a/toxygen/av/calls_manager.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import sys -import threading -import traceback -import logging - -from qtpy import QtCore - -import av.calls -from messenger.messages import * -from ui import av_widgets -import common.event as event -import utils.ui as util_ui -from toxygen_wrapper.tests import support_testing as ts - -global LOG -LOG = logging.getLogger('app.'+__name__) - -class CallsManager: - - def __init__(self, toxav, settings, main_screen, contacts_manager, app=None): - self._callav = av.calls.AV(toxav, settings) # object with data about calls - self._call = self._callav - self._call_widgets = {} # dict of incoming call widgets - self._incoming_calls = set() - self._settings = settings - self._main_screen = main_screen - self._contacts_manager = contacts_manager - self._call_started_event = event.Event() # friend_number, audio, video, is_outgoing - self._call_finished_event = event.Event() # friend_number, is_declined - self._app = app - - def set_toxav(self, toxav) -> None: - self._callav.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) -> None: - """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._callav and self._contacts_manager.is_active_online(): # start call - if not self._settings['audio']['enabled']: - return - self._callav(num, audio, video) - self._main_screen.active_call() - self._call_started_event(num, audio, video, True) - elif num in self._callav: # finish or cancel call if you call with active friend - self.stop_call(num, False) - - def incoming_call(self, audio, video, friend_number) -> None: - """ - Incoming call from friend. - """ - LOG.debug(f"CM incoming_call {friend_number}") - # if not self._settings['audio']['enabled']: return - friend = self._contacts_manager.get_friend_by_number(friend_number) - self._call_started_event(friend_number, audio, video, False) - self._incoming_calls.add(friend_number) - if friend_number == self._contacts_manager.get_active_number(): - self._main_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) -> None: - """ - Accept incoming call with audio or video - Called from a thread - """ - - LOG.debug(f"CM accept_call from friend_number={friend_number} {audio} {video}") - sys.stdout.flush() - - try: - self._main_screen.active_call() - # failsafe added somewhere this was being left up - self.close_call(friend_number) - QtCore.QCoreApplication.processEvents() - - self._callav.call_accept_call(friend_number, audio, video) - LOG.debug(f"accept_call _call.accept_call CALLED f={friend_number}") - except Exception as e: - # - LOG.error(f"accept_call _call.accept_call ERROR for {friend_number} {e}") - LOG.debug(traceback.print_exc()) - self._main_screen.call_finished() - if hasattr(self._main_screen, '_settings') and \ - 'audio' in self._main_screen._settings and \ - 'input' in self._main_screen._settings['audio']: - iInput = self._settings['audio']['input'] - iOutput = self._settings['audio']['output'] - iVideo = self._settings['video']['device'] - LOG.debug(f"iInput={iInput} iOutput={iOutput} iVideo={iVideo}") - elif hasattr(self._main_screen, '_settings') and \ - hasattr(self._main_screen._settings, 'audio') and \ - 'input' not in self._main_screen._settings['audio']: - LOG.warn(f"'audio' not in {self._main_screen._settings}") - elif hasattr(self._main_screen, '_settings') and \ - hasattr(self._main_screen._settings, 'audio') and \ - 'input' not in self._main_screen._settings['audio']: - LOG.warn(f"'audio' not in {self._main_screen._settings}") - else: - LOG.warn(f"_settings not in self._main_screen") - util_ui.message_box(str(e), - util_ui.tr('ERROR Accepting call from {friend_number}')) - finally: - # does not terminate call - just the av_widget - LOG.debug(f"CM.accept_call close av_widget") - self.close_call(friend_number) - LOG.debug(f" closed self._call_widgets[{friend_number}]") - - def close_call(self, friend_number:int) -> None: - # refactored out from above because the accept window not getting - # taken down in some accept audio calls - LOG.debug(f"close_call {friend_number}") - try: - if friend_number in self._call_widgets: - self._call_widgets[friend_number].close() - del self._call_widgets[friend_number] - if friend_number in self._incoming_calls: - self._incoming_calls.remove(friend_number) - except Exception as e: - # RuntimeError: wrapped C/C++ object of type IncomingCallWidget has been deleted - - LOG.warn(f" closed self._call_widgets[{friend_number}] {e}") - # invoke_in_main_thread(QtCore.QCoreApplication.processEvents) - QtCore.QCoreApplication.processEvents() - - - def stop_call(self, friend_number, by_friend) -> None: - """ - Stop call with friend - """ - LOG.debug(f"CM.stop_call friend={friend_number}") - if friend_number in self._incoming_calls: - self._incoming_calls.remove(friend_number) - is_declined = True - else: - is_declined = False - if friend_number in self._call_widgets: - LOG.debug(f"CM.stop_call _call_widgets close") - self.close_call(friend_number) - - LOG.debug(f"CM.stop_call _main_screen.call_finished") - self._main_screen.call_finished() - self._callav.finish_call(friend_number, by_friend) # finish or decline call - is_video = self._callav.is_video_call(friend_number) - if is_video: - def destroy_window(): - #??? FixMe - with ts.ignoreStdout(): import cv2 - cv2.destroyWindow(str(friend_number)) - LOG.debug(f"CM.stop_call destroy_window") - threading.Timer(2.0, destroy_window).start() - - LOG.debug(f"CM.stop_call _call_finished_event") - self._call_finished_event(friend_number, is_declined) - - def friend_exit(self, friend_number:int) -> None: - if friend_number in self._callav: - self._callav.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/av/screen_sharing.py b/toxygen/av/screen_sharing.py deleted file mode 100644 index e0f783b..0000000 --- a/toxygen/av/screen_sharing.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import QtWidgets - -class DesktopGrabber: - - def __init__(self, x, y, width, height): - self._x = x - self._y = y - self._width = width - self._height = height - self._width -= width % 4 - self._height -= height % 4 - self._screen = QtWidgets.QApplication.primaryScreen() - - def read(self) -> tuple: - pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height) - image = pixmap.toImage() - s = image.bits().asstring(self._width * self._height * 4) - import numpy as np - arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4)) - - return True, arr diff --git a/toxygen/avwidgets.py b/toxygen/avwidgets.py new file mode 100644 index 0000000..a01b52d --- /dev/null +++ b/toxygen/avwidgets.py @@ -0,0 +1,139 @@ +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +import widgets +import profile +import 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__() + self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint) + self.resize(QtCore.QSize(500, 270)) + self.avatar_label = QtGui.QLabel(self) + self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) + self.avatar_label.setScaledContents(False) + self.name = widgets.DataLabel(self) + self.name.setGeometry(QtCore.QRect(90, 20, 300, 25)) + font = QtGui.QFont() + font.setFamily(settings.Settings.get_instance['font']) + font.setPointSize(16) + font.setBold(True) + self.name.setFont(font) + self.call_type = widgets.DataLabel(self) + self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25)) + self.call_type.setFont(font) + self.accept_audio = QtGui.QPushButton(self) + self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150)) + self.accept_video = QtGui.QPushButton(self) + self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150)) + self.decline = QtGui.QPushButton(self) + self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150)) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_audio.png') + icon = QtGui.QIcon(pixmap) + self.accept_audio.setIcon(icon) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_video.png') + icon = QtGui.QIcon(pixmap) + self.accept_video.setIcon(icon) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/decline_call.png') + icon = QtGui.QIcon(pixmap) + self.decline.setIcon(icon) + self.accept_audio.setIconSize(QtCore.QSize(150, 150)) + self.accept_video.setIconSize(QtCore.QSize(140, 140)) + self.decline.setIconSize(QtCore.QSize(140, 140)) + self.accept_audio.setStyleSheet("QPushButton { border: none }") + self.accept_video.setStyleSheet("QPushButton { border: none }") + self.decline.setStyleSheet("QPushButton { border: none }") + self.setWindowTitle(text) + self.name.setText(name) + self.call_type.setText(text) + pr = profile.Profile.get_instance() + self.accept_audio.clicked.connect(lambda: pr.accept_call(friend_number, True, False) or self.stop()) + # self.accept_video.clicked.connect(lambda: pr.start_call(friend_number, True, True)) + self.decline.clicked.connect(lambda: pr.stop_call(friend_number, False) or self.stop()) + + class SoundPlay(QtCore.QThread): + + def __init__(self): + QtCore.QThread.__init__(self) + + def run(self): + class AudioFile: + chunk = 1024 + + def __init__(self, fl): + self.stop = False + self.fl = fl + self.wf = wave.open(self.fl, 'rb') + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wf.getsampwidth()), + channels=self.wf.getnchannels(), + rate=self.wf.getframerate(), + output=True) + + def play(self): + while not self.stop: + data = self.wf.readframes(self.chunk) + while data and not self.stop: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + self.wf = wave.open(self.fl, 'rb') + + def close(self): + self.stream.close() + self.p.terminate() + + self.a = AudioFile(curr_directory() + '/sounds/call.wav') + self.a.play() + self.a.close() + + if settings.Settings.get_instance()['calls_sound']: + self.thread = SoundPlay() + self.thread.start() + else: + self.thread = None + + def stop(self): + if self.thread is not None: + self.thread.a.stop = True + self.thread.wait() + self.close() + + def set_pixmap(self, pixmap): + self.avatar_label.setPixmap(pixmap) + + +class AudioMessageRecorder(widgets.CenteredWidget): + + def __init__(self, friend_number, name): + super(AudioMessageRecorder, self).__init__() + self.label = QtGui.QLabel(self) + self.label.setGeometry(QtCore.QRect(10, 20, 250, 20)) + text = QtGui.QApplication.translate("MenuWindow", "Send audio message to friend {}", None, QtGui.QApplication.UnicodeUTF8) + self.label.setText(text.format(name)) + self.record = QtGui.QPushButton(self) + self.record.setGeometry(QtCore.QRect(20, 100, 150, 150)) + + self.record.setText(QtGui.QApplication.translate("MenuWindow", "Start recording", None, + QtGui.QApplication.UnicodeUTF8)) + self.record.clicked.connect(self.start_or_stop_recording) + self.recording = False + self.friend_num = friend_number + + def start_or_stop_recording(self): + if not self.recording: + self.recording = True + self.record.setText(QtGui.QApplication.translate("MenuWindow", "Stop recording", None, + QtGui.QApplication.UnicodeUTF8)) + else: + self.close() + + diff --git a/toxygen/bootstrap.py b/toxygen/bootstrap.py new file mode 100644 index 0000000..89534b7 --- /dev/null +++ b/toxygen/bootstrap.py @@ -0,0 +1,83 @@ +import random + + +class Node: + def __init__(self, ip, port, tox_key, rand): + self._ip, self._port, self._tox_key, self.rand = ip, port, tox_key, rand + + def get_data(self): + return bytes(self._ip, 'utf-8'), self._port, self._tox_key + + +def node_generator(): + nodes = [] + ips = [ + "144.76.60.215", "23.226.230.47", "195.154.119.113", "biribiri.org", + "46.38.239.179", "178.62.250.138", "130.133.110.14", "104.167.101.29", + "205.185.116.116", "198.98.51.198", "80.232.246.79", "108.61.165.198", + "212.71.252.109", "194.249.212.109", "185.25.116.107", "192.99.168.140", + "46.101.197.175", "95.215.46.114", "5.189.176.217", "148.251.23.146", + "104.223.122.15", "78.47.114.252", "d4rk4.ru", "81.4.110.149", + "95.31.20.151", "104.233.104.126", "51.254.84.212", "home.vikingmakt.com.br", + "5.135.59.163", "185.58.206.164", "188.244.38.183", "mrflibble.c4.ee", + "82.211.31.116", "128.199.199.197", "103.230.156.174", "91.121.66.124", + "92.54.84.70", "tox1.privacydragon.me" + ] + ports = [ + 33445, 33445, 33445, 33445, + 33445, 33445, 33445, 33445, + 33445, 33445, 33445, 33445, + 33445, 33445, 33445, 33445, + 443, 33445, 5190, 2306, + 33445, 33445, 1813, 33445, + 33445, 33445, 33445, 33445, + 33445, 33445, 33445, 33445, + 33445, 33445, 33445, 33445, + 33445, 33445 + ] + ids = [ + "04119E835DF3E78BACF0F84235B300546AF8B936F035185E2A8E9E0A67C8924F", + "A09162D68618E742FFBCA1C2C70385E6679604B2D80EA6E84AD0996A1AC8A074", + "E398A69646B8CEACA9F0B84F553726C1C49270558C57DF5F3C368F05A7D71354", + "F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67", + "F5A1A38EFB6BD3C2C8AF8B10D85F0F89E931704D349F1D0720C3C4059AF2440A", + "788236D34978D1D5BD822F0A5BEBD2C53C64CC31CD3149350EE27D4D9A2F9B6B", + "461FA3776EF0FA655F1A05477DF1B3B614F7D6B124F7DB1DD4FE3C08B03B640F", + "5918AC3C06955962A75AD7DF4F80A5D7C34F7DB9E1498D2E0495DE35B3FE8A57", + "A179B09749AC826FF01F37A9613F6B57118AE014D4196A0E1105A98F93A54702", + "1D5A5F2F5D6233058BF0259B09622FB40B482E4FA0931EB8FD3AB8E7BF7DAF6F", + "CF6CECA0A14A31717CC8501DA51BE27742B70746956E6676FF423A529F91ED5D", + "8E7D0B859922EF569298B4D261A8CCB5FEA14FB91ED412A7603A585A25698832", + "C4CEB8C7AC607C6B374E2E782B3C00EA3A63B80D4910B8649CCACDD19F260819", + "3CEE1F054081E7A011234883BC4FC39F661A55B73637A5AC293DDF1251D9432B", + "DA4E4ED4B697F2E9B000EEFE3A34B554ACD3F45F5C96EAEA2516DD7FF9AF7B43", + "6A4D0607A296838434A6A7DDF99F50EF9D60A2C510BBF31FE538A25CB6B4652F", + "CD133B521159541FB1D326DE9850F5E56A6C724B5B8E5EB5CD8D950408E95707", + "5823FB947FF24CF83DDFAC3F3BAA18F96EA2018B16CC08429CB97FA502F40C23", + "2B2137E094F743AC8BD44652C55F41DFACC502F125E99E4FE24D40537489E32F", + "7AED21F94D82B05774F697B209628CD5A9AD17E0C073D9329076A4C28ED28147", + "0FB96EEBFB1650DDB52E70CF773DDFCABE25A95CC3BB50FC251082E4B63EF82A", + "1C5293AEF2114717547B39DA8EA6F1E331E5E358B35F9B6B5F19317911C5F976", + "53737F6D47FA6BD2808F378E339AF45BF86F39B64E79D6D491C53A1D522E7039", + "9E7BD4793FFECA7F32238FA2361040C09025ED3333744483CA6F3039BFF0211E", + "9CA69BB74DE7C056D1CC6B16AB8A0A38725C0349D187D8996766958584D39340", + "EDEE8F2E839A57820DE3DA4156D88350E53D4161447068A3457EE8F59F362414", + "AEC204B9A4501412D5F0BB67D9C81B5DB3EE6ADA64122D32A3E9B093D544327D", + "188E072676404ED833A4E947DC1D223DF8EFEBE5F5258573A236573688FB9761", + "2D320F971EF2CA18004416C2AAE7BA52BF7949DB34EA8E2E21AF67BD367BE211", + "24156472041E5F220D1FA11D9DF32F7AD697D59845701CDD7BE7D1785EB9DB39", + "15A0F9684E2423F9F46CFA5A50B562AE42525580D840CC50E518192BF333EE38", + "FAAB17014F42F7F20949F61E55F66A73C230876812A9737F5F6D2DCE4D9E4207", + "AF97B76392A6474AF2FD269220FDCF4127D86A42EF3A242DF53A40A268A2CD7C", + "B05C8869DBB4EDDD308F43C1A974A20A725A36EACCA123862FDE9945BF9D3E09", + "5C4C7A60183D668E5BD8B3780D1288203E2F1BAE4EEF03278019E21F86174C1D", + "4E3F7D37295664BBD0741B6DBCB6431D6CD77FC4105338C2FC31567BF5C8224A", + "5625A62618CB4FCA70E147A71B29695F38CC65FF0CBD68AD46254585BE564802", + "31910C0497D347FF160D6F3A6C0E317BAFA71E8E03BC4CBB2A185C9D4FB8B31E" + ] + for i in range(len(ips)): + nodes.append(Node(ips[i], ports[i], ids[i], random.randint(0, 1000000))) + arr = sorted(nodes, key=lambda x: x.rand)[:4] + for elem in arr: + yield elem.get_data() + diff --git a/toxygen/bootstrap/__init__.py b/toxygen/bootstrap/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py deleted file mode 100644 index 6d64783..0000000 --- a/toxygen/bootstrap/bootstrap.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import random -import logging - -from qtpy import QtCore -try: - import certifi - from io import BytesIO -except ImportError: - certifi = None - -from user_data.settings import get_user_config_path -from utils.util import * - -from toxygen_wrapper.tests.support_testing import _get_nodes_path -from toxygen_wrapper.tests.support_http import download_url -import toxygen_wrapper.tests.support_testing as ts - -global LOG -LOG = logging.getLogger('app.'+'bootstrap') - -def download_nodes_list(settings, oArgs) -> str: - if not settings['download_nodes_list']: - return '' - if not ts.bAreWeConnected(): - return '' - url = settings['download_nodes_url'] - path = _get_nodes_path(oArgs=oArgs) - # dont download blindly so we can edit the file and not block on startup - if os.path.isfile(path): - with open(path, 'rt') as fl: - result = fl.read() - return result - LOG.debug("downloading list of nodes") - result = download_url(url, settings._app._settings) - if not result: - LOG.warn("failed downloading list of nodes") - return '' - LOG.info("downloaded list of nodes") - _save_nodes(result, settings._app) - return result - -def _save_nodes(nodes, app) -> None: - if not nodes: - return - with open(_get_nodes_path(app._args), 'wb') as fl: - LOG.info("Saving nodes to " +_get_nodes_path(app._args)) - fl.write(nodes) diff --git a/toxygen/bootstrap/nodes.json b/toxygen/bootstrap/nodes.json deleted file mode 100644 index 5314998..0000000 --- a/toxygen/bootstrap/nodes.json +++ /dev/null @@ -1 +0,0 @@ -{"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 new file mode 100644 index 0000000..d5b9784 --- /dev/null +++ b/toxygen/callbacks.py @@ -0,0 +1,357 @@ +try: + from PySide import QtCore +except ImportError: + from PyQt4 import QtCore +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 + + +# ----------------------------------------------------------------------------------------------------------------- +# 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 = QtGui.QApplication.translate("Callback", "File from", None, QtGui.QApplication.UnicodeUTF8) + 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 + """ + plugin = PluginLoader.get_instance() + invoke_in_main_thread(plugin.callback_lossless, friend_number, data, length) + + +def lossy_packet(tox, friend_number, data, length, user_data): + """ + Incoming lossy packet + """ + plugin = PluginLoader.get_instance() + invoke_in_main_thread(plugin.callback_lossy, friend_number, data, length) + + +# ----------------------------------------------------------------------------------------------------------------- +# 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.chunk( + bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), + audio_channels_count, + rate) + + +# ----------------------------------------------------------------------------------------------------------------- +# 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) + + tox.callback_friend_lossless_packet(lossless_packet, 0) + tox.callback_friend_lossy_packet(lossy_packet, 0) + diff --git a/toxygen/calls.py b/toxygen/calls.py new file mode 100644 index 0000000..16cef47 --- /dev/null +++ b/toxygen/calls.py @@ -0,0 +1,144 @@ +import pyaudio +import time +import threading +import settings +from toxav_enums import * +# TODO: play sound until outgoing call will be started or cancelled and add timeout +# TODO: add widget for call + +CALL_TYPE = { + 'NONE': 0, + 'AUDIO': 1, + 'VIDEO': 2 +} + + +class AV: + + def __init__(self, toxav): + self._toxav = toxav + self._running = True + + self._calls = {} # dict: key - friend number, value - call type + + self._audio = None + self._audio_stream = None + self._audio_thread = None + self._audio_running = False + self._out_stream = None + + self._audio_rate = 8000 + self._audio_channels = 1 + self._audio_duration = 60 + self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000 + + def __contains__(self, friend_number): + return friend_number in self._calls + + def __call__(self, friend_number, audio, video): + """Call friend with specified number""" + self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0) + self._calls[friend_number] = CALL_TYPE['AUDIO'] + self.start_audio_thread() + + def finish_call(self, friend_number, by_friend=False): + + if not by_friend: + self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) + if friend_number in self._calls: + del self._calls[friend_number] + if not len(self._calls): + self.stop_audio_thread() + + def stop(self): + self._running = False + self.stop_audio_thread() + + def start_audio_thread(self): + """ + Start audio sending + """ + if self._audio_thread is not None: + return + + self._audio_running = True + + self._audio = pyaudio.PyAudio() + self._audio_stream = self._audio.open(format=pyaudio.paInt16, + rate=self._audio_rate, + channels=self._audio_channels, + input=True, + input_device_index=settings.Settings.get_instance().audio['input'], + frames_per_buffer=self._audio_sample_count * 10) + + self._audio_thread = threading.Thread(target=self.send_audio) + self._audio_thread.start() + + def stop_audio_thread(self): + + if self._audio_thread is None: + return + + self._audio_running = False + + self._audio_thread.join() + + self._audio_thread = None + self._audio_stream = None + self._audio = None + + if self._out_stream is not None: + self._out_stream.stop_stream() + self._out_stream.close() + self._out_stream = None + + def chunk(self, samples, channels_count, rate): + """ + Incoming chunk + """ + + if self._out_stream is None: + self._out_stream = self._audio.open(format=pyaudio.paInt16, + channels=channels_count, + rate=rate, + output_device_index=settings.Settings.get_instance().audio['output'], + output=True) + self._out_stream.write(samples) + + def send_audio(self): + """ + This method sends audio to friends + """ + + while self._audio_running: + try: + pcm = self._audio_stream.read(self._audio_sample_count) + if pcm: + for friend in self._calls: + if self._calls[friend] & 1: + try: + self._toxav.audio_send_frame(friend, pcm, self._audio_sample_count, + self._audio_channels, self._audio_rate) + except: + pass + except: + pass + + time.sleep(0.01) + + def accept_call(self, friend_number, audio_enabled, video_enabled): + + if self._running: + self._calls[friend_number] = int(video_enabled) * 2 + int(audio_enabled) + self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0) + self.start_audio_thread() + + def toxav_call_state_cb(self, friend_number, state): + """ + New call state + """ + if self._running: + + if state & TOXAV_FRIEND_CALL_STATE['ACCEPTING_A']: + self._calls[friend_number] |= 1 + diff --git a/toxygen/common/__init__.py b/toxygen/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/common/event.py b/toxygen/common/event.py deleted file mode 100644 index f51a51f..0000000 --- a/toxygen/common/event.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 deleted file mode 100644 index 687fd9a..0000000 --- a/toxygen/common/provider.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 deleted file mode 100644 index 45563b2..0000000 --- a/toxygen/common/tox_save.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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/contact.py b/toxygen/contact.py new file mode 100644 index 0000000..1dbfcdf --- /dev/null +++ b/toxygen/contact.py @@ -0,0 +1,113 @@ +import os +from settings import * +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +from toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE + + +class Contact: + """ + Class encapsulating TOX contact + Properties: name (alias of contact or name), status_message, status (connection status) + widget - widget for update + """ + + def __init__(self, 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._name, self._status_message = name, status_message + self._status, self._widget = None, widget + self._widget.name.setText(name) + self._widget.status_message.setText(status_message) + self._tox_id = tox_id + self.load_avatar() + + # ----------------------------------------------------------------------------------------------------------------- + # name - current name or alias of user + # ----------------------------------------------------------------------------------------------------------------- + + def get_name(self): + return self._name + + def set_name(self, value): + self._name = str(value, 'utf-8') + self._widget.name.setText(self._name) + self._widget.name.repaint() + + name = property(get_name, set_name) + + # ----------------------------------------------------------------------------------------------------------------- + # Status message + # ----------------------------------------------------------------------------------------------------------------- + + def get_status_message(self): + return self._status_message + + def set_status_message(self, value): + self._status_message = str(value, 'utf-8') + self._widget.status_message.setText(self._status_message) + self._widget.status_message.repaint() + + status_message = property(get_status_message, set_status_message) + + # ----------------------------------------------------------------------------------------------------------------- + # Status + # ----------------------------------------------------------------------------------------------------------------- + + def get_status(self): + return self._status + + def set_status(self, value): + self._status = value + self._widget.connection_status.update(value) + + status = property(get_status, set_status) + + # ----------------------------------------------------------------------------------------------------------------- + # TOX ID. WARNING: for friend it will return public key, for profile - full address + # ----------------------------------------------------------------------------------------------------------------- + + def get_tox_id(self): + return self._tox_id + + tox_id = property(get_tox_id) + + # ----------------------------------------------------------------------------------------------------------------- + # Avatars + # ----------------------------------------------------------------------------------------------------------------- + + def load_avatar(self): + """ + Tries to load avatar of contact or uses default avatar + """ + avatar_path = '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + os.chdir(ProfileHelper.get_path() + 'avatars/') + if not os.path.isfile(avatar_path): # load default image + avatar_path = 'avatar.png' + os.chdir(curr_directory() + '/images/') + 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() + + 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): + os.remove(avatar_path) + self.load_avatar() + + def set_avatar(self, avatar): + avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + with open(avatar_path, 'wb') as f: + f.write(avatar) + self.load_avatar() + + def get_pixmap(self): + return self._widget.avatar_label.pixmap() diff --git a/toxygen/contacts/__init__.py b/toxygen/contacts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/contacts/basecontact.py b/toxygen/contacts/basecontact.py deleted file mode 100644 index b4b33f1..0000000 --- a/toxygen/contacts/basecontact.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from user_data.settings import * -from qtpy import QtCore, QtGui -from toxygen_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: - """ - Class encapsulating TOX contact - Properties: name (alias of contact or name), status_message, status (connection status) - widget - widget for update, tox id (or public key) - Base class for all contacts. - """ - - def __init__(self, profile_manager, name, status_message, widget, tox_id, kind=''): - """ - :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 - :param kind: one of ['bot', 'friend', 'group', 'invite', 'grouppeer', ''] - """ - self._profile_manager = profile_manager - self._name, self._status_message = name, status_message - self._kind = kind - 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() - - # Name - current name or alias of user - - def get_name(self): - return self._name - - def set_name(self, value): - 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 - - def get_status_message(self): - return self._status_message - - def set_status_message(self, value): - 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 - - def get_status(self): - 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 - - def get_tox_id(self): - return self._tox_id - - tox_id = property(get_tox_id) - - # Avatars - - def load_avatar(self): - """ - Tries to load avatar of contact or uses default avatar - """ - try: - 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) - except Exception as e: - pass - - 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 = self.get_contact_avatar_path() - with open(avatar_path, 'wb') as f: - f.write(avatar) - self.load_avatar() - - 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): - # File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contacts_manager.py", line 252, in filtration_and_sorting - # contact.set_widget(item_widget) - # File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact.py", line 320, in set_widget - if not self._widget: - LOG.warn("BC.init_widget self._widget is NULL") - return - self._widget.name.setText(self._name) - self._widget.status_message.setText(self._status_message) - if hasattr(self._widget, 'kind'): - self._widget.kind.setText(self._kind) - 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 deleted file mode 100644 index bd46c32..0000000 --- a/toxygen/contacts/common.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import hashlib - -from pydenticon import Generator - -# 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:int): - 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/contacts/contact.py b/toxygen/contacts/contact.py deleted file mode 100644 index 70b9318..0000000 --- a/toxygen/contacts/contact.py +++ /dev/null @@ -1,320 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from history.database import TIMEOUT, \ - SAVE_MESSAGES, MESSAGE_AUTHOR - -from contacts import basecontact, common -from messenger.messages import * -from contacts.contact_menu import * -from file_transfers import file_transfers as ft -import re - -# LOG=util.log -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) - -class Contact(basecontact.BaseContact): - """ - Class encapsulating TOX contact - Properties: number, message getter, history etc. Base class for friend and gc classes - """ - - 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__(profile_manager, name, status_message, widget, tox_id) - self._number = number - self._new_messages = False - self._visible = True - self._alias = False - self._message_getter = message_getter - self._corr = [] - self._unsaved_messages = 0 - self._history_loaded = self._new_actions = False - self._curr_text = self._search_string = '' - self._search_index = 0 - - def __del__(self): - self.set_visibility(False) - del self._widget - if hasattr(self, '_message_getter'): - del self._message_getter - - # History support - - def load_corr(self, first_time=True): - """ - :param first_time: friend became active, load first part of messages - """ - 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): - """ - Get all chat history from db for current friend - """ - if self._message_getter is None: - return - data = list(self._message_getter.get_all()) - if data is not None and len(data): - data.reverse() - data = list(map(lambda p: self._get_text_message(p), data)) - self._corr = data + self._corr - self._history_loaded = True - - def get_corr_for_saving(self): - """ - Get data to save in db - :return: list of unsaved messages or [] - """ - 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[:] - - def append_message(self, message): - """ - :param message: text or file transfer message - """ - self._corr.append(message) - if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): - self._unsaved_messages += 1 - - def get_last_message_text(self): - 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].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 - - def get_unsent_messages(self): - """ - :return list of unsent messages - """ - 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 - """ -# and m.tox_message_id == tox_message_id, - messages = filter(lambda m: m.author is not None - and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], - self._corr) - # was message = list(...)[0] - return list(messages) - - def mark_as_sent(self, tox_message_id): - try: - 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: - # wrapped C/C++ object of type QLabel has been deleted - LOG.error(f"Mark as sent: {ex}") - - # Message deletion - - 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) - self._message_getter.delete_one() - self._search_index = 0 - - def delete_old_messages(self): - """ - Delete old messages (reduces RAM usage if messages saving is not enabled) - """ - def save_message(m): - if m.type == MESSAGE_TYPE['FILE_TRANSFER'] and (m.state not in ACTIVE_FILE_TRANSFERS): - return True - 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 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 - - def clear_corr(self, save_unsent=False): - """ - Clear messages list - """ - if hasattr(self, '_message_getter'): - del self._message_getter - self._search_index = 0 - # don't delete data about active file transfer - if not save_unsent: - 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 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()) - - # Chat history search - - def search_string(self, search_string): - self._search_string, self._search_index = search_string, 0 - return self.search_prev() - - def search_prev(self): - while True: - l = len(self._corr) - for i in range(self._search_index - 1, -l - 1, -1): - if self._corr[i].type not in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): - continue - message = self._corr[i].text - if re.search(self._search_string, message, re.IGNORECASE) is not None: - self._search_index = i - return i - self._search_index = -l - self.load_corr(False) - if len(self._corr) == l: - return None # not found - - def search_next(self): - if not self._search_index: - return None - for i in range(self._search_index + 1, 0): - if self._corr[i].type not in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): - continue - message = self._corr[i].text - if re.search(self._search_string, message, re.IGNORECASE) is not None: - self._search_index = i - return i - return None # not found - - # Current text - text from message area - - def get_curr_text(self): - return self._curr_text - - def set_curr_text(self, value): - self._curr_text = value - - curr_text = property(get_curr_text, set_curr_text) - - # Alias support - - def set_name(self, value): - """ - Set new name or ignore if alias exists - :param value: new name - """ - if not self._alias: - super().set_name(value) - - def set_alias(self, alias): - self._alias = bool(alias) - - def has_alias(self): - return self._alias - - # Visibility in friends' list - - def get_visibility(self): - return self._visible - - def set_visibility(self, value): - self._visible = value - - visibility = property(get_visibility, set_visibility) - - # Unread messages and other actions from friend - - def get_actions(self): - return self._new_actions - - def set_actions(self, value): - self._new_actions = value - self._widget.connection_status.update(self.status, value) - - actions = property(get_actions, set_actions) # unread messages, incoming files, av calls - - def get_messages(self): - return self._new_messages - - def inc_messages(self): - self._new_messages += 1 - self._new_actions = True - self._widget.connection_status.update(self.status, True) - self._widget.messages.update(self._new_messages) - - def reset_messages(self): - self._new_actions = False - self._new_messages = 0 - self._widget.messages.update(self._new_messages) - self._widget.connection_status.update(self.status, False) - - messages = property(get_messages) - - # Friend's or group's number (can be used in toxcore) - - def get_number(self): - return self._number - - def set_number(self, value): - 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 deleted file mode 100644 index 6f45ca6..0000000 --- a/toxygen/contacts/contact_menu.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from qtpy import QtWidgets - -import utils.ui as util_ui -from toxygen_wrapper.toxcore_enums_and_consts import * - -global LOG -import logging -LOG = logging.getLogger('app') - -# 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() - LOG.debug(f"_generate_groups_menu len(chats)={len(chats)} or self._contact.status={self._contact.status}") - if not len(chats) or self._contact.status is None: - #? return None - pass - 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 deleted file mode 100644 index 0c5a61d..0000000 --- a/toxygen/contacts/contact_provider.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import common.tox_save as tox_save - -global LOG -import logging -LOG = logging.getLogger(__name__) - -# callbacks can be called in any thread so were being careful -from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -class ContactProvider(tox_save.ToxSave): - - def __init__(self, tox, friend_factory, group_factory, group_peer_factory, app=None): - 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 - self._app = app - - # Friends - - def get_friend_by_number(self, friend_number:int): - try: - public_key = self._tox.friend_get_public_key(friend_number) - except Exception as e: - LOG_WARN(f"CP.get_friend_by_number NO {friend_number} {e} ") - return None - 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) - if friend is None: - LOG_WARN(f"CP.get_friend_by_public_key NULL {friend} ") - else: - self._add_to_cache(public_key, friend) - LOG_DEBUG(f"CP.get_friend_by_public_key ADDED {friend} ") - return friend - - def get_all_friends(self) -> list: - if self._app and self._app.bAppExiting: - return [] - try: - friend_numbers = self._tox.self_get_friend_list() - except Exception as e: - LOG_WARN(f"CP.get_all_friends EXCEPTION {e} ") - return [] - friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) - return list(friends) - - # Groups - - def get_all_groups(self): - """from callbacks""" - try: - len_groups = self._tox.group_get_number_groups() - group_numbers = range(len_groups) - except Exception as e: - return None - groups = list(map(lambda n: self.get_group_by_number(n), group_numbers)) - # failsafe in case there are bogus None groups? - fgroups = list(filter(lambda x: x, groups)) - if len(fgroups) != len_groups: - LOG_WARN(f"CP.are there are bogus None groups in libtoxcore? {len(fgroups)} != {len_groups}") - for group_num in group_numbers: - group = self.get_group_by_number(group_num) - if group is None: - LOG_ERROR(f"There are bogus None groups in libtoxcore {group_num}!") - # fixme: do something - groups = fgroups - return groups - - def get_group_by_number(self, group_number): - group = None - try: -# LOG_DEBUG(f"CP.CP.group_get_number {group_number} ") - # original code - chat_id = self._tox.group_get_chat_id(group_number) - if chat_id is None: - LOG_ERROR(f"get_group_by_number NULL chat_id ({group_number})") - elif chat_id == '-1': - LOG_ERROR(f"get_group_by_number <0 chat_id ({group_number})") - else: - LOG_INFO(f"CP.group_get_number {group_number} {chat_id}") - group = self.get_group_by_chat_id(chat_id) - if group is None or group == '-1': - LOG_WARN(f"CP.get_group_by_number leaving {group} ({group_number})") - #? iRet = self._tox.group_leave(group_number) - # invoke in main thread? - # self._contacts_manager.delete_group(group_number) - return group - except Exception as e: - LOG_WARN(f"CP.group_get_number {group_number} {e}") - return None - - def get_group_by_chat_id(self, chat_id): - group = self._get_contact_from_cache(chat_id) - if group is not None: - return group - group = self._group_factory.create_group_by_chat_id(chat_id) - if group is None: - LOG_ERROR(f"get_group_by_chat_id NULL chat_id={chat_id}") - else: - self._add_to_cache(chat_id, group) - - return group - - 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) - if group is None: - LOG_WARN(f"get_group_by_public_key NULL group public_key={public_key}") - else: - self._add_to_cache(public_key, group) - - return group - - # Group peers - - def get_all_group_peers(self): - return [] - - def get_group_peer_by_id(self, group, peer_id): - peer = group.get_peer_by_id(peer_id) - if peer is not None: - return self._get_group_peer(group, peer) - LOG_WARN(f"get_group_peer_by_id peer_id={peer_id}") - return None - - def get_group_peer_by_public_key(self, group, public_key): - peer = group.get_peer_by_public_key(public_key) - if peer is not None: - return self._get_group_peer(group, peer) - LOG_WARN(f"get_group_peer_by_public_key public_key={public_key}") - return None - - # 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 deleted file mode 100644 index 6f0dae8..0000000 --- a/toxygen/contacts/contacts_manager.py +++ /dev/null @@ -1,670 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import logging - -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 -from groups.group_peer import GroupChatPeer -from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE -import toxygen_wrapper.toxcore_enums_and_consts as enums - -# LOG=util.log -global LOG -LOG = logging.getLogger('app.'+__name__) - -UINT32_MAX = 2 ** 32 -1 - -def set_contact_kind(contact) -> None: - bInvite = len(contact.name) == enums.TOX_PUBLIC_KEY_SIZE * 2 and \ - contact.status_message == '' - bBot = not bInvite and contact.name.lower().endswith(' bot') - if type(contact) == Friend and bInvite: - contact._kind = 'invite' - elif type(contact) == Friend and bBot: - contact._kind = 'bot' - elif type(contact) == Friend: - contact._kind = 'friend' - elif type(contact) == GroupChat: - contact._kind = 'group' - elif type(contact) == GroupChatPeer: - contact._kind = 'grouppeer' - -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._ms = 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 _log(self, s) -> None: - try: - self._ms._log(s) - except: pass - - 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) -> None: - data = self._tox.get_savedata() - self._profile_manager.save_profile(data) - - def is_friend_active(self, friend_number:int) -> bool: - if not self.is_active_a_friend(): - return False - - return self.get_curr_contact().number == friend_number - - def is_group_active(self, group_number) -> bool: - if self.is_active_a_friend(): - return False - - return self.get_curr_contact().number == group_number - - def is_contact_active(self, contact) -> bool: - if self._active_contact == -1: -# LOG.debug("No self._active_contact") - return False - if self._active_contact >= len(self._contacts): - LOG.warn(f"ERROR _active_contact={self._active_contact} >= contacts len={len(self._contacts)}") - return False - if not self._contacts[self._active_contact]: - LOG.warn(f"ERROR NULL {self._contacts[self._active_contact]} {contact.tox_id}") - return False - - if not hasattr(contact, 'tox_id'): - LOG.warn(f"ERROR is_contact_active no contact.tox_id {type(contact)} contact={contact}") - return False - - return self._contacts[self._active_contact].tox_id == contact.tox_id - - # Reconnection support - - def reset_contacts_statuses(self) -> None: - 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 >= 0 and self._active_contact != value: - try: - current_contact.curr_text = self._screen.messageEdit.toPlainText() - except: - pass - - # IndexError: list index out of range - if value >= len(self._contacts): - LOG.warn("CM.set_active value too big: {{self._contacts}}") - return - 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 e: # no friend found. ignore - LOG.warn(f"CM.set_active EXCEPTION value:{value} len={len(self._contacts)} {e}") - # gulp 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, 6 kind - :param filter_str: show contacts which name contains this substring - """ - filter_str = filter_str.lower() - current_contact = self.get_curr_contact() - - for index, contact in enumerate(self._contacts): - if not contact._kind: - set_contact_kind(contact) - - if sorting > 6 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: - # AttributeError: 'NoneType' object has no attribute 'number' - for (i, contact) in enumerate(self._contacts): - if contact is None or not hasattr(contact, 'number'): - LOG.error(f"Contact {i} is None or not hasattr 'number'") - del self._contacts[i] - continue - 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) - elif sorting == 6: - self._contacts = sorted(self._contacts, key=lambda x: x._kind) - 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) - if not item_widget: - LOG_WARN("CM.filtration_and_sorting( item_widget is NULL") - continue - 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 peer is None: - LOG.warn(f'get_or_create_group_peer_contact group_number={group_number} peer_id={peer_id} peer={peer}') - return None - LOG.debug(f'get_or_create_group_peer_contact group_number={group_number} peer_id={peer_id} peer={peer}') - if not self.check_if_contact_exists(peer.public_key): - contact = self.add_group_peer(group, peer) - # dunno - return contact - # me - later wrong kind of object? - 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) - try: - self._tox.friend_delete(friend.number) - except Exception as e: - LOG.warn(f"'There was no friend with the given friend number {e}") - 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[:enums.TOX_PUBLIC_KEY_SIZE * 2] - if tox_id == self._tox.self_get_address()[:enums.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): - index = len(self._contacts) - group = self._contact_provider.get_group_by_number(group_number) - if group is None: - LOG.warn(f"CM.add_group: NULL group from group_number={group_number}") - elif type(group) == int and group < 0: - LOG.warn(f"CM.add_group: NO group from group={group} group_number={group_number}") - else: - LOG.info(f"CM.add_group: Adding group {group._name}") - self._contacts.append(group) - LOG.info(f"contacts_manager.add_group: saving profile") - self._save_profile() - group.reset_avatar(self._settings['identicons']) - LOG.info(f"contacts_manager.add_group: setting active") - 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 contact - contact._kind = 'grouppeer' - self._contacts.append(contact) - contact.reset_avatar(self._settings['identicons']) - self._save_profile() - return contact - - def remove_group_peer_by_id(self, group, peer_id): - peer = group.get_peer_by_id(peer_id) - if peer: # broken - if not self.check_if_contact_exists(peer.public_key): - return - contact = self.get_contact_by_tox_id(peer.public_key) - self.remove_group_peer(contact) - - def remove_group_peer(self, group_peer_contact): - contact = self.get_contact_by_tox_id(group_peer_contact.tox_id) - if contact: - self._cleanup_contact_data(contact) - num = self._contacts.index(contact) - self._delete_contact(num) - - def get_gc_peer_name(self, name): - group = self.get_curr_contact() - - 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, sToxPkOrId, message): - """ - Function tries to send request to contact with specified id - :param sToxPkOrId: id of new contact or tox dns 4 value - :param message: additional message - :return: True on success else error string - """ - retval = '' - try: - message = message or 'Hello! Add me to your contact list please' - if len(sToxPkOrId) == enums.TOX_PUBLIC_KEY_SIZE * 2: # public key - self.add_friend(sToxPkOrId) - title = 'Friend added' - text = 'Friend added without sending friend request' - else: - num = self._tox.friend_add(sToxPkOrId, message.encode('utf-8')) - if num < UINT32_MAX: - tox_pk = sToxPkOrId[:enums.TOX_PUBLIC_KEY_SIZE * 2] - self._add_friend(tox_pk) - self.update_filtration() - title = 'Friend added' - text = 'Friend added by sending friend request' - self.save_profile() - retval = True - else: - title = 'Friend failed' - text = 'Friend failed sending friend request' - retval = text - - except Exception as ex: # wrong data - title = 'Friend add exception' - text = 'Friend request exception with ' + str(ex) - self._log(text) - LOG.exception(text) - LOG.warn(f"DELETE {sToxPkOrId} ?") - retval = str(ex) - title = util_ui.tr(title) - text = util_ui.tr(text) - util_ui.message_box(text, title) - return retval - - 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 - LOG.error('Accept friend request failed! ' + str(ex)) - - def can_send_typing_notification(self): - return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() - - # 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() - LOG.info(f"update_groups_numbers len(groups)={len(groups)}") - # Thread 76 "ToxIterateThrea" received signal SIGSEGV, Segmentation fault. - for i in range(len(groups)): - chat_id = self._tox.group_get_chat_id(i) - if not chat_id: - LOG.warn(f"update_groups_numbers {i} chat_id") - continue - group = self.get_contact_by_tox_id(chat_id) - if not group: - LOG.warn(f"update_groups_numbers {i} group") - continue - 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) - # filter(lambda c: not c.has_avatar(), self._contacts) - for (i, contact) in enumerate(self._contacts): - if contact is None: - LOG.warn(f"_load_contacts NULL contact {i}") - LOG.info(f"_load_contacts deleting NULL {self._contacts[i]}") - del self._contacts[i] - #? self.save_profile() - continue - if contact.has_avatar(): continue - 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 Exception as e: - 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 deleted file mode 100644 index 24b04ad..0000000 --- a/toxygen/contacts/friend.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os - -from contacts import contact, common -from messenger.messages import * -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: - return -1 - - 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 deleted file mode 100644 index 31d5eec..0000000 --- a/toxygen/contacts/friend_factory.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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:int): - aliases = self._settings['friends_aliases'] - sToxPk = self._tox.friend_get_public_key(friend_number) - assert sToxPk, sToxPk - try: - alias = list(filter(lambda x: x[0] == sToxPk, aliases))[0][1] - except: - alias = '' - item = self._create_friend_item() - name = alias or self._tox.friend_get_name(friend_number) or sToxPk - status_message = self._tox.friend_get_status_message(friend_number) - message_getter = self._db.messages_getter(sToxPk) - friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, sToxPk) - 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 deleted file mode 100644 index c060e65..0000000 --- a/toxygen/contacts/group_chat.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from contacts import contact -from contacts.contact_menu import GroupMenuGenerator -import utils.util as util -from groups.group_peer import GroupChatPeer -from toxygen_wrapper import toxcore_enums_and_consts as constants -from common.tox_save import ToxSave -from groups.group_ban import GroupBan - -global LOG -import logging -LOG = logging.getLogger(__name__) -from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -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): - "called from callbacks" - if peer_id > self._peers_limit: - LOG_WARN(f"add_peer id={peer_id} > {self._peers_limit}") - return - - status_message = f"Private in {self.name}" - LOG_TRACE(f"GC.add_peer id={peer_id} status_message={status_message}") - 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, - status_message=status_message) - 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) - if peer: # broken - self._peers.remove(peer) - else: - LOG_WARN(f"remove_peer empty peers for {peer_id}") - - def get_peer_by_id(self, peer_id): - peers = list(filter(lambda p: p.id == peer_id, self._peers)) - if peers: - return peers[0] - else: - LOG_WARN(f"get_peer_by_id empty peers for {peer_id}") - return None - - def get_peer_by_public_key(self, public_key): - peers = list(filter(lambda p: p.public_key == public_key, self._peers)) - # DEBUGc: group_moderation #0 mod_id=4294967295 event_type=3 - # WARN_: get_peer_by_id empty peers for 4294967295 - if peers: - return peers[0] - else: - LOG_WARN(f"get_peer_by_public_key empty peers for {public_key}") - return None - - 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) - if peers_names: # broken - return list(peers_names) - else: - LOG_WARN(f"get_peers_names empty peers") - #? broken - return [] - - def get_peers(self): - return self._peers[:] - - peers = property(get_peers) - - def get_bans(self): - return [] -# ban_ids = self._tox.group_ban_get_list(self._number) -# bans = [] -# for ban_id in ban_ids: -# ban = GroupBan(ban_id, -# self._tox.group_ban_get_target(self._number, ban_id), -# self._tox.group_ban_get_time_set(self._number, ban_id)) -# bans.append(ban) -# -# return bans -# - bans = property(get_bans) - - # 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 deleted file mode 100644 index 4345c4b..0000000 --- a/toxygen/contacts/group_factory.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from contacts.group_chat import GroupChat -from common.tox_save import ToxSave -import toxygen_wrapper.toxcore_enums_and_consts as constants - -global LOG -import logging -LOG = logging.getLogger(__name__) - -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_chat_id(self, chat_id): - return self.create_group_by_public_key(chat_id) - - 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): - LOG.info(f"create_group_by_number {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()+100): - 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 deleted file mode 100644 index 3e6131c..0000000 --- a/toxygen/contacts/group_peer_contact.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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, status_message=None): - if status_message is None: status_message=str() - super().__init__(profile_manager, message_getter, peer_number, name, status_message, 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 deleted file mode 100644 index 1804b50..0000000 --- a/toxygen/contacts/group_peer_factory.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -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, - status_message=peer.status_message) - 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 deleted file mode 100644 index 3afcf2b..0000000 --- a/toxygen/contacts/profile.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from contacts import basecontact -import random -import threading -import common.tox_save as tox_save -from middleware.threads import invoke_in_main_thread - -iUMAXINT = 4294967295 -iRECONNECT = 50 - -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) - -class Profile(basecontact.BaseContact, tox_save.ToxSave): - """ - Profile of current toxygen user. - """ - def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action, app=None): - """ - :param tox: tox instance - :param screen: ref to main screen - """ - assert tox - 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 - self._app = app - - # Edit current user's data - - def change_status(self) -> None: - """ - 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) -> None: - 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(iRECONNECT, self._reconnect) - self._timer.start() - - def set_name(self, value) -> None: - if self.name == value: - return - super().set_name(value) - self._tox.self_set_name(self._name) - - def set_status_message(self, value) -> None: - 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, iUMAXINT)) # no spam - uint32 - self._tox_id = self._tox.self_get_address() - self._sToxId = self._tox.self_get_address() - return self._sToxId - - # Reset - - def restart(self) -> None: - """ - Recreate tox instance - """ - self.status = None - invoke_in_main_thread(self._reset_action) - - def _reconnect(self) -> None: - self._waiting_for_reconnection = False - if self._app and self._app.bAppExiting: - # dont do anything after the app has been shipped - # there's a segv that results - return - 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(iRECONNECT, self._reconnect) - self._timer.start() - -# Current thread 0x00007901a13ccb80 (most recent call first): -# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 826 in self_get_friend_list_size -# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 838 in self_get_friend_list -# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact_provider.py", line 45 in get_all_friends -# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/profile.py", line 90 in _reconnect -# File "/usr/lib/python3.11/threading.py", line 1401 in run -# File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner -# File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap -# - diff --git a/toxygen/file_transfers/file_transfers.py b/toxygen/file_transfers.py similarity index 62% rename from toxygen/file_transfers/file_transfers.py rename to toxygen/file_transfers.py index 5fa87f9..7b23ffc 100644 --- a/toxygen/file_transfers/file_transfers.py +++ b/toxygen/file_transfers.py @@ -1,25 +1,24 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os -from os import chdir, remove, rename +from toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL from os.path import basename, getsize, exists, dirname -from time import time +from os import remove, rename, chdir +from time import time, sleep +from tox import Tox +import settings +try: + from PySide import QtCore +except ImportError: + from PyQt4 import QtCore + QtCore.Signal = QtCore.pyqtSignal -from common.event import Event -from middleware.threads import invoke_in_main_thread -from toxygen_wrapper.tox import Tox -from toxygen_wrapper.toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL -from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE -FILE_TRANSFER_STATE = { +TOX_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, - 'UNSENT': 7 + 'OUTGOING_NOT_STARTED': 6 } ACTIVE_FILE_TRANSFERS = (0, 1, 4, 5, 6) @@ -30,119 +29,113 @@ DO_NOT_SHOW_ACCEPT_BUTTON = (2, 3, 4, 6) SHOW_PROGRESS_BAR = (0, 1, 4) - -def is_inline(file_name): - allowed_inlines = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png') - - return file_name in allowed_inlines or file_name.startswith('qTox_Image_') +ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png') -class FileTransfer: +class StateSignal(QtCore.QObject): + + signal = QtCore.Signal(int, float, int) # state, progress, time in sec + + +class TransferFinishedSignal(QtCore.QObject): + + signal = QtCore.Signal(int, int) # friend number, file number + + +class FileTransfer(QtCore.QObject): """ 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 = FILE_TRANSFER_STATE['RUNNING'] + self.state = TOX_FILE_TRANSFER_STATE['RUNNING'] self._file_number = file_number self._creation_time = None self._size = float(size) self._done = 0 - self._state_changed_event = Event() - self._finished_event = Event() - self._file_id = self._file = None + self._state_changed = StateSignal() + self._finished = TransferFinishedSignal() + self._file_id = None + + def set_tox(self, tox): + self._tox = tox def set_state_changed_handler(self, handler): - self._state_changed_event += lambda *args: invoke_in_main_thread(handler, *args) + self._state_changed.signal.connect(handler) def set_transfer_finished_handler(self, handler): - self._finished_event += lambda *args: invoke_in_main_thread(handler, *args) + self._finished.signal.connect(handler) - 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 - - friend_number = property(get_friend_number) - - def get_file_id(self): - return self._file_id -#? return self._tox.file_get_file_id(self._friend_number, self._file_number) - - 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 self._file is not None: - self._file.close() - self._signal() - - def cancelled(self): - if self._file is not None: - self._file.close() - 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.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.set_state(control) - - def _signal(self): + 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)) + self._state_changed.signal.emit(self.state, percentage, int(t)) - def _finished(self): - self._finished_event(self._friend_number, self._file_number) + def finished(self): + self._finished.signal.emit(self._friend_number, self._file_number) + def get_file_number(self): + return self._file_number + + def get_friend_number(self): + return self._friend_number + + def get_id(self): + return self._file_id + + def get_path(self): + return self._path + + def cancel(self): + self.send_control(TOX_FILE_CONTROL['CANCEL']) + if hasattr(self, '_file'): + self._file.close() + self.signal() + + def cancelled(self): + if hasattr(self, '_file'): + sleep(0.1) + self._file.close() + self.state = TOX_FILE_TRANSFER_STATE['CANCELLED'] + self.signal() + + 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() + + def send_control(self, control): + if self._tox.file_control(self._friend_number, self._file_number, control): + self.state = control + self.signal() + + def get_file_id(self): + return self._tox.file_get_file_id(self._friend_number, self._file_number) + +# ----------------------------------------------------------------------------------------------------------------- # Send file +# ----------------------------------------------------------------------------------------------------------------- class SendTransfer(FileTransfer): def __init__(self, path, tox, friend_number, kind=TOX_FILE_KIND['DATA'], file_id=None): if path is not None: - fl = open(path, 'rb') + self._file = open(path, 'rb') size = getsize(path) else: - fl = None size = 0 - super().__init__(path, tox, friend_number, size) - self._file = fl - self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + super(SendTransfer, self).__init__(path, tox, friend_number, size) + self.state = TOX_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() @@ -160,12 +153,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 self._file is not None: + if hasattr(self, '_file'): self._file.close() - self.state = FILE_TRANSFER_STATE['FINISHED'] - self._finished() + self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self.finished() + self.signal() class SendAvatar(SendTransfer): @@ -174,15 +167,12 @@ class SendAvatar(SendTransfer): """ def __init__(self, path, tox, friend_number): - LOG_DEBUG(f"SendAvatar path={path} friend_number={friend_number}") - if path is None or not os.path.exists(path): - avatar_hash = None + if path is None: + hash = None else: with open(path, 'rb') as fl: - data=fl.read() - LOG_DEBUG(f"SendAvatar data={data} type={type(data)}") - avatar_hash = tox.hash(data, None) - super().__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], avatar_hash) + hash = Tox.hash(fl.read()) + super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], hash) class SendFromBuffer(FileTransfer): @@ -191,8 +181,8 @@ class SendFromBuffer(FileTransfer): """ def __init__(self, tox, friend_number, data, file_name): - super().__init__(None, tox, friend_number, len(data)) - self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + super(SendFromBuffer, self).__init__(None, tox, friend_number, len(data)) + self.state = TOX_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')) @@ -200,8 +190,6 @@ 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() @@ -210,29 +198,31 @@ class SendFromBuffer(FileTransfer): self._tox.file_send_chunk(self._friend_number, self._file_number, position, data) self._done += size else: - self.state = FILE_TRANSFER_STATE['FINISHED'] - self._finished() - self._signal() + self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self.finished() + self.signal() class SendFromFileBuffer(SendTransfer): def __init__(self, *args): - super().__init__(*args) + super(SendFromFileBuffer, self).__init__(*args) def send_chunk(self, position, size): - super().send_chunk(position, size) + super(SendFromFileBuffer, self).send_chunk(position, size) if not size: - os.chdir(dirname(self._path)) - os.remove(self._path) + chdir(dirname(self._path)) + remove(self._path) +# ----------------------------------------------------------------------------------------------------------------- # Receive file +# ----------------------------------------------------------------------------------------------------------------- class ReceiveTransfer(FileTransfer): def __init__(self, path, tox, friend_number, size, file_number, position=0): - super().__init__(path, tox, friend_number, size, file_number) + super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number) self._file = open(self._path, 'wb') self._file_size = position self._file.truncate(position) @@ -241,12 +231,11 @@ class ReceiveTransfer(FileTransfer): self._done = position def cancel(self): - super().cancel() + super(ReceiveTransfer, self).cancel() remove(self._path) def total_size(self): self._missed.add(self._file_size) - return min(self._missed) def write_chunk(self, position, data): @@ -259,8 +248,8 @@ class ReceiveTransfer(FileTransfer): self._creation_time = time() if data is None: self._file.close() - self.state = FILE_TRANSFER_STATE['FINISHED'] - self._finished() + self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self.finished() else: data = bytearray(data) if self._file_size < position: @@ -275,7 +264,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): @@ -284,21 +273,19 @@ class ReceiveToBuffer(FileTransfer): """ def __init__(self, tox, friend_number, size, file_number): - super().__init__(None, tox, friend_number, size, file_number) + super(ReceiveToBuffer, self).__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 = FILE_TRANSFER_STATE['FINISHED'] - self._finished() + self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self.finished() else: data = bytes(data) l = len(data) @@ -308,7 +295,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): @@ -316,36 +303,40 @@ class ReceiveAvatar(ReceiveTransfer): Get friend's avatar. Doesn't need file transfer item """ MAX_AVATAR_SIZE = 512 * 1024 - def __init__(self, path, tox, friend_number, size, file_number): - full_path = path + '.tmp' - super().__init__(full_path, tox, friend_number, size, file_number) + + 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) if size > self.MAX_AVATAR_SIZE: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - remove(full_path) + remove(path + '.tmp') elif not size: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - remove(full_path) + if exists(path): + remove(path) + self._file.close() + remove(path + '.tmp') elif exists(path): - ihash = self.get_file_id() + hash = self.get_file_id() with open(path, 'rb') as fl: data = fl.read() existing_hash = Tox.hash(data) - if ihash == existing_hash: + if hash == existing_hash: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - remove(full_path) + remove(path + '.tmp') else: self.send_control(TOX_FILE_CONTROL['RESUME']) else: self.send_control(TOX_FILE_CONTROL['RESUME']) def write_chunk(self, position, data): - if data is None: + super(ReceiveAvatar, self).write_chunk(position, data) + if self.state: avatar_path = self._path[:-4] if exists(avatar_path): chdir(dirname(avatar_path)) remove(avatar_path) rename(self._path, avatar_path) - super().write_chunk(position, data) diff --git a/toxygen/file_transfers/__init__.py b/toxygen/file_transfers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py deleted file mode 100644 index a9085c2..0000000 --- a/toxygen/file_transfers/file_transfers_handler.py +++ /dev/null @@ -1,371 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import logging - -from messenger.messages import * -from file_transfers.file_transfers import SendAvatar, is_inline -from ui.contact_items import * -import utils.util as util -from common.tox_save import ToxSave - -from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -# LOG=util.log -global LOG -LOG = logging.getLogger('app.'+__name__) -log = lambda x: LOG.info(x) - -class FileTransfersHandler(ToxSave): - lBlockAvatars = [] - def __init__(self, tox, settings, contact_provider, file_transfers_message_service, profile): - super().__init__(tox) - self._settings = settings - 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) - self. lBlockAvatars = [] - - def stop(self) -> None: - 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) -> None: - # main thread - """ - 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) - if friend is None: - LOG.info(f'incoming_file_handler Friend NULL friend_number={friend_number}') - return - auto = self._settings['allow_auto_accept'] and friend.tox_id in self._settings['auto_accept_from_friends'] - inline = False # ?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: - LOG_INFO(f'incoming_file_handler paused friend_number={friend_number}') - (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: - LOG_INFO(f'incoming_file_handler small friend_number={friend_number}') - 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: - # accepted is really started - LOG_INFO(f'incoming_file_handler auto friend_number={friend_number}') - 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: - LOG_INFO(f'incoming_file_handler reject friend_number={friend_number}') - accepted = False - # FixME: need GUI ask - # accepted is really started - 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) -> None: - """ - Stop transfer - :param friend_number: number of friend - :param file_number: file number - :param already_cancelled: was cancelled by friend - """ - # callback - 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) -> None: - friend = self._get_friend_by_number(friend_number) - if friend is None: return None - friend.delete_one_unsent_file(message_id) - - def pause_transfer(self, friend_number, file_number, by_friend=False) -> None: - """ - 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) -> None: - """ - 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) -> None: - """ - :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 friend is None: return None - 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) -> None: - """ - 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) -> None: - 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) -> None: - friend = self._get_friend_by_number(friend_number) - if friend is None: - LOG_WARN("fsend_inline Error friend is None file_name: {file_name}") - return - 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: - LOG_WARN("fsend_inline Error friend.status is None file_name: {file_name}") - return - 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) -> 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 is None: return None - if friend.status is None and not is_resend: - self._file_transfers_message_service.add_unsent_file_message(friend, path, None) - return - elif friend.status is None and is_resend: - LOG_WARN('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) -> None: - """ - Incoming chunk - """ - self._file_transfers[(friend_number, file_number)].write_chunk(position, data) - - def outgoing_chunk(self, friend_number, file_number, position, size) -> None: - """ - Outgoing chunk - """ - self._file_transfers[(friend_number, file_number)].send_chunk(position, size) - - def transfer_finished(self, friend_number, file_number) -> None: - transfer = self._file_transfers[(friend_number, file_number)] - friend = self._get_friend_by_number(friend_number) - if friend is None: return None - t = type(transfer) - if t is ReceiveAvatar: - friend.load_avatar() - elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image - LOG.debug('inline') - inline = InlineImageMessage(transfer.data) - message_id = self._insert_inline_before[(friend_number, file_number)] - del self._insert_inline_before[(friend_number, file_number)] - if friend is None: return None - index = friend.insert_inline(message_id, inline) - self._file_transfers_message_service.add_inline_message(transfer, index) - del self._file_transfers[(friend_number, file_number)] - - def send_files(self, friend_number:int) -> None: - try: - friend = self._get_friend_by_number(friend_number) - if friend is None: return - friend.remove_invalid_unsent_files() - files = friend.get_unsent_files() - for fl in files: - data, path = fl.data, fl.path - if data is not None: - 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(): - # RuntimeError: dictionary changed size during iteration - (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[key] - if not os.path.exists(path): - del self._paused_file_transfers[key] - 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: - LOG_ERROR('send_files EXCEPTION in file sending: ' + str(ex)) - - def friend_exit(self, friend_number:int) -> None: - # RuntimeError: dictionary changed size during iteration - lMayChangeDynamically = self._file_transfers.copy() - for friend_num, file_num in lMayChangeDynamically: - if friend_num != friend_number: - continue - if (friend_num, file_num) not in self._file_transfers: - continue - ft = self._file_transfers[(friend_num, file_num)] - if type(ft) is SendTransfer: - try: - file_id = ft.file_id - except Exception as e: - LOG_WARN("friend_exit SendTransfer Error getting file_id: {e}") - # drop through - else: - self._paused_file_transfers[file_id] = [ft.path, friend_num, False, -1] - elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - try: - file_id = ft.file_id - except Exception as e: - LOG_WARN("friend_exit ReceiveTransfer Error getting file_id: {e}") - # drop through - else: - self._paused_file_transfers[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) -> None: - """ - :param friend_number: number of friend who should get new avatar - :param avatar_path: path to avatar or None if reset - """ - return - if (avatar_path, friend_number,) in self.lBlockAvatars: - return - if friend_number is None: - LOG_WARN(f"send_avatar friend_number NULL {friend_number}") - return - if avatar_path and type(avatar_path) != str: - LOG_WARN(f"send_avatar avatar_path type {type(avatar_path)}") - return - LOG_INFO(f"send_avatar avatar_path={avatar_path} friend_number={friend_number}") - try: - # self NOT missing - who's self? - sa = SendAvatar(avatar_path, self._tox, friend_number) - LOG_INFO(f"send_avatar avatar_path={avatar_path} sa={sa}") - self._file_transfers[(friend_number, sa.file_number)] = sa - except Exception as e: - # ArgumentError('This client is currently not connected to the friend.') - LOG_WARN(f"send_avatar EXCEPTION {e}") - self.lBlockAvatars.append( (avatar_path, friend_number,) ) - - def incoming_avatar(self, friend_number, file_number, size) -> None: - """ - 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) - if friend is None: return - 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, _) -> None: - # from a callback - 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:int) -> bool: - friend = self._get_friend_by_number(friend_number) - if friend is None: return None - - return friend.status is not None - - def _get_friend_by_number(self, friend_number:int): - 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 deleted file mode 100644 index 1b292ee..0000000 --- a/toxygen/file_transfers/file_transfers_messages_service.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import logging - -from messenger.messenger import * -import utils.util as util -from file_transfers.file_transfers import * - -global LOG -LOG = logging.getLogger('app.'+__name__) - -from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -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): - assert friend - author = MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND']) - # accepted is really started - 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): - assert friend - 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) -> None: - """callback""" - if not self._is_friend_active(transfer.friend_number): - return - if transfer is None or not hasattr(transfer, 'data') or \ - not transfer.data: - LOG_ERROR(f"add_inline_message empty data") - return - count = self._messages.count() - if count + index + 1 >= 0: - # assumes .data - self._create_inline_item(transfer, count + index + 1) - - def add_unsent_file_message(self, friend, file_path, data): - assert friend - 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:int) -> bool: - 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 new file mode 100644 index 0000000..4e57f0b --- /dev/null +++ b/toxygen/friend.py @@ -0,0 +1,251 @@ +import contact +from messages import * +from history import * +import util +import file_transfers as ft + + +class Friend(contact.Contact): + """ + Friend in list of friends. Can be hidden, properties 'has unread messages' and 'has alias' added + """ + + def __init__(self, message_getter, number, *args): + """ + :param message_getter: gets messages from db + :param number: number of friend. + """ + super(Friend, self).__init__(*args) + self._number = number + self._new_messages = False + self._visible = True + self._alias = False + self._message_getter = message_getter + self._corr = [] + self._unsaved_messages = 0 + self._history_loaded = self._new_actions = False + self._receipts = 0 + self._curr_text = '' + + def __del__(self): + self.set_visibility(False) + del self._widget + if hasattr(self, '_message_getter'): + del self._message_getter + + # ----------------------------------------------------------------------------------------------------------------- + # 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() + + def load_corr(self, first_time=True): + """ + :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 + 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 + + def load_all_corr(self): + 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)) + self._corr = data + self._corr + self._history_loaded = True + + def get_corr_for_saving(self): + """ + 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 [] + + def get_corr(self): + return self._corr[:] + + def append_message(self, message): + """ + :param message: text or file transfer message + """ + self._corr.append(message) + if message.get_type() <= 1: + 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)) + if messages: + return messages[-1].get_data()[0] + else: + return '' + + def get_unsent_messages(self): + """ + :return list of unsent messages + """ + messages = filter(lambda x: x.get_owner() == MESSAGE_OWNER['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)) + + def delete_message(self, time): + elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0] + tmp = list(filter(lambda x: x.get_type() <= 1, self._corr)) + if elem in tmp[-self._unsaved_messages:]: + self._unsaved_messages -= 1 + self._corr.remove(elem) + + def mark_as_sent(self): + try: + message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] + message.mark_as_sent() + except Exception as ex: + util.log('Mark as sent ex: ' + str(ex)) + + def clear_corr(self, save_unsent=False): + """ + Clear messages list + """ + if hasattr(self, '_message_getter'): + del self._message_getter + # 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._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)) + self._unsaved_messages = len(self.get_unsent_messages()) + + def get_curr_text(self): + return self._curr_text + + def set_curr_text(self, value): + self._curr_text = value + + curr_text = property(get_curr_text, set_curr_text) + + # ----------------------------------------------------------------------------------------------------------------- + # 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 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)) + + # ----------------------------------------------------------------------------------------------------------------- + # Alias support + # ----------------------------------------------------------------------------------------------------------------- + + def set_name(self, value): + """ + Set new name or ignore if alias exists + :param value: new name + """ + if not self._alias: + super(Friend, self).set_name(value) + + def set_alias(self, alias): + self._alias = bool(alias) + + # ----------------------------------------------------------------------------------------------------------------- + # Visibility in friends' list + # ----------------------------------------------------------------------------------------------------------------- + + def get_visibility(self): + return self._visible + + def set_visibility(self, value): + self._visible = value + + visibility = property(get_visibility, set_visibility) + + # ----------------------------------------------------------------------------------------------------------------- + # Unread messages from friend + # ----------------------------------------------------------------------------------------------------------------- + + def get_actions(self): + return self._new_actions + + def set_actions(self, value): + self._new_actions = value + self._widget.connection_status.update(self.status, value) + + actions = property(get_actions, set_actions) # unread messages, incoming files, av calls + + def get_messages(self): + return self._new_messages + + def inc_messages(self): + self._new_messages += 1 + self._new_actions = True + self._widget.connection_status.update(self.status, True) + self._widget.messages.update(self._new_messages) + + def reset_messages(self): + self._new_actions = False + self._new_messages = 0 + self._widget.messages.update(self._new_messages) + self._widget.connection_status.update(self.status, False) + + messages = property(get_messages) + + # ----------------------------------------------------------------------------------------------------------------- + # Friend's number (can be used in toxcore) + # ----------------------------------------------------------------------------------------------------------------- + + def get_number(self): + return self._number + + def set_number(self, value): + self._number = value + + number = property(get_number, set_number) diff --git a/toxygen/groups/__init__.py b/toxygen/groups/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/groups/group_ban.py b/toxygen/groups/group_ban.py deleted file mode 100644 index 2b17a25..0000000 --- a/toxygen/groups/group_ban.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 deleted file mode 100644 index 2332933..0000000 --- a/toxygen/groups/group_invite.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 deleted file mode 100644 index a96c751..0000000 --- a/toxygen/groups/group_peer.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -class GroupChatPeer: - """ - Represents peer in group chat. - """ - - def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False, status_message=None): - self._peer_id = peer_id - self._name = name - self._status = status - self._status_message = status_message - self._role = role - self._public_key = public_key - self._is_current_user = is_current_user - self._is_muted = is_muted - self._kind = 'grouppeer' - - # 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) - - def get_status_message(self): - return self._status_message - - status_message = property(get_status_message) - - # 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 deleted file mode 100644 index 0e52d2a..0000000 --- a/toxygen/groups/groups_service.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import logging - -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 toxygen_wrapper.toxcore_enums_and_consts as constants -from toxygen_wrapper.toxcore_enums_and_consts import * -from toxygen_wrapper.tox import UINT32_MAX - -global LOG -LOG = logging.getLogger('app.'+'gs') - -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 - # maybe just use self - self._tox = tox - - def set_tox(self, tox) -> None: - 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) -> None: - try: - group_number = self._tox.group_new(privacy_state, name, nick, status) - except Exception as e: - LOG.error(f"create_new_gc {e}") - return - 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) -> None: - try: - group_number = self._tox.group_join(chat_id, password, nick, status) - assert type(group_number) == int, group_number - assert group_number < UINT32_MAX, group_number - except Exception as e: - # gui - title = f"join_gc_by_id {chat_id}" - util_ui.message_box(title +'\n' +str(e), title) - LOG.error(f"_join_gc_via_id {e}") - return - LOG.debug(f"_join_gc_via_id {group_number}") - self._add_new_group_by_number(group_number) - group = self._get_group_by_number(group_number) - try: - assert group and hasattr(group, 'status') - except Exception as e: - # gui - title = f"join_gc_by_id {chat_id}" - util_ui.message_box(title +'\n' +str(e), title) - LOG.error(f"_join_gc_via_id {e}") - return - group.status = constants.TOX_USER_STATUS['NONE'] - self._contacts_manager.update_filtration() - - # Groups reconnect and leaving - - def leave_group(self, group_number) -> None: - if type(group_number) == int: - self._tox.group_leave(group_number) - self._contacts_manager.delete_group(group_number) - - def disconnect_from_group(self, group_number) -> None: - 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) -> None: - 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) -> None: - if self._tox.friend_get_connection_status(friend_number) == TOX_CONNECTION['NONE']: - title = f"Error in group_invite_friend {friend_number}" - e = f"Friend not connected friend_number={friend_number}" - util_ui.message_box(title +'\n' +str(e), title) - return - - try: - self._tox.group_invite_friend(group_number, friend_number) - except Exception as e: - title = f"Error in group_invite_friend {group_number} {friend_number}" - util_ui.message_box(title +'\n' +str(e), title) - - def process_group_invite(self, friend_number, group_name, invite_data) -> None: - friend = self._get_friend_by_number(friend_number) - # binary {invite_data} - LOG.debug(f"process_group_invite {friend_number} {group_name}") - 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) -> None: - pk = invite.friend_public_key - friend = self._get_friend_by_public_key(pk) - LOG.debug(f"accept_group_invite {name}") - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - self._tox.group_toggle_ignore(group.number, peer.id, ignore) - peer.is_muted = ignore - - def set_self_info(self, group, name, status) -> None: - 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) -> None: - return - 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) -> None: - self._tox.group_mod_ban_peer(group.number, peer_id, ban_type) - - def kick_peer(self, group, peer_id) -> None: - self._tox.group_mod_remove_peer(group.number, peer_id) - - def cancel_ban(self, group_number, ban_id) -> None: - self._tox.group_mod_remove_ban(group_number, ban_id) - - # Private methods - - def _add_new_group_by_number(self, group_number) -> None: - LOG.debug(f"_add_new_group_by_number group_number={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:int): - 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) -> None: - group.remove_all_peers_except_self() - self.generate_peers_list() - - def _delete_group_invite(self, invite) -> None: - if invite in self._group_invites: - self._group_invites.remove(invite) - - # status should be dropped - def _join_gc_via_invite(self, invite_data, friend_number, nick, status='', password='') -> None: - LOG.debug(f"_join_gc_via_invite friend_number={friend_number} nick={nick} datalen={len(invite_data)}") - if nick is None: - nick = '' - if invite_data is None: - invite_data = b'' - try: - # status should be dropped - group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, password=password) - except Exception as e: - LOG.error(f"_join_gc_via_invite ERROR {e}") - return - try: - self._add_new_group_by_number(group_number) - except Exception as e: - LOG.error(f"_join_gc_via_invite group_number={group_number} {e}") - return - - def _update_invites_button_state(self) -> None: - self._main_screen.update_gc_invites_button_state() - - def _get_widgets_factory(self) -> None: - return self._widgets_factory_provider.get_item() diff --git a/toxygen/groups/peers_list.py b/toxygen/groups/peers_list.py deleted file mode 100644 index 97641d9..0000000 --- a/toxygen/groups/peers_list.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from ui.group_peers_list import PeerItem, PeerTypeItem -from toxygen_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 new file mode 100644 index 0000000..ad18ee5 --- /dev/null +++ b/toxygen/history.py @@ -0,0 +1,182 @@ +# coding=utf-8 +from sqlite3 import connect +import settings +from os import chdir +import os.path +from toxencryptsave import ToxEncryptSave + + +PAGE_SIZE = 42 + +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 = ToxEncryptSave.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') + cursor = db.cursor() + cursor.execute('CREATE TABLE IF NOT EXISTS friends(' + ' tox_id TEXT PRIMARY KEY' + ')') + db.close() + + def save(self): + encr = ToxEncryptSave.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 = ToxEncryptSave.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') + 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: + db.rollback() + raise + finally: + db.close() + + def delete_friend_from_db(self, tox_id): + chdir(settings.ProfileHelper.get_path()) + db = connect(self._name + '.hstr') + try: + cursor = db.cursor() + cursor.execute('DELETE FROM friends WHERE tox_id=?;', (tox_id, )) + cursor.execute('DROP TABLE id' + tox_id + ';') + db.commit() + except: + db.rollback() + raise + finally: + db.close() + + def friend_exists_in_db(self, tox_id): + chdir(settings.ProfileHelper.get_path()) + db = connect(self._name + '.hstr') + 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') + try: + cursor = db.cursor() + cursor.executemany('INSERT INTO id' + tox_id + '(message, owner, unix_time, message_type) ' + 'VALUES (?, ?, ?, ?);', messages_iter) + db.commit() + except: + db.rollback() + raise + finally: + db.close() + + def update_messages(self, tox_id, unsent_time): + chdir(settings.ProfileHelper.get_path()) + db = connect(self._name + '.hstr') + 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: + db.rollback() + raise + finally: + db.close() + pass + + def delete_message(self, tox_id, time): + chdir(settings.ProfileHelper.get_path()) + db = connect(self._name + '.hstr') + try: + cursor = db.cursor() + cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time = ' + str(time) + ';') + db.commit() + except: + db.rollback() + raise + finally: + db.close() + + def delete_messages(self, tox_id): + chdir(settings.ProfileHelper.get_path()) + db = connect(self._name + '.hstr') + try: + cursor = db.cursor() + cursor.execute('DELETE FROM id' + tox_id + ';') + db.commit() + except: + db.rollback() + raise + finally: + db.close() + + def messages_getter(self, tox_id): + return History.MessageGetter(self._name, tox_id) + + class MessageGetter: + def __init__(self, name, tox_id): + chdir(settings.ProfileHelper.get_path()) + self._db = connect(name + '.hstr') + self._cursor = self._db.cursor() + self._cursor.execute('SELECT message, owner, unix_time, message_type FROM id' + tox_id + + ' ORDER BY unix_time DESC;') + + def get_one(self): + return self._cursor.fetchone() + + def get_all(self): + return self._cursor.fetchall() + + def get(self, count): + return self._cursor.fetchmany(count) + + def __del__(self): + self._db.close() diff --git a/toxygen/history/__init__.py b/toxygen/history/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/history/database.py b/toxygen/history/database.py deleted file mode 100644 index 7d8dd35..0000000 --- a/toxygen/history/database.py +++ /dev/null @@ -1,227 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from sqlite3 import connect -import os.path -import utils.util as util - -global LOG -import logging -LOG = logging.getLogger('h.database') - -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 = path - self._toxes = toxes - self._name = os.path.basename(path) - - def open(self): - path = self._path - toxes = self._toxes - if not os.path.exists(path): - LOG.warn('Db not found: ' +path) - return - try: - with open(path, 'rb') as fin: - data = fin.read() - except Exception as ex: - LOG.error('Db reading error: ' +path +' ' +str(ex)) - raise - try: - if toxes.is_data_encrypted(data): - data = toxes.pass_decrypt(data) - with open(path, 'wb') as fout: - fout.write(data) - except Exception as ex: - LOG.error('Db writing error: ' +path +' ' + str(ex)) - os.remove(path) - LOG.info('Db opened: ' +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) - LOG.info('Db exported: ' +new_path) - - 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() - return True - except Exception as e: - LOG.error("dd_friend_to_db " +self._name +f" Database exception! {e}") - db.rollback() - return False - finally: - db.close() - LOG.debug(f"add_friend_to_db {tox_id}") - - def delete_friend_from_db(self, tox_id): - db = self._connect() - try: - cursor = db.cursor() - cursor.execute('DROP TABLE id' + tox_id + ';') - db.commit() - return True - except Exception as e: - LOG.error("delete_friend_from_db " +self._name +f" Database exception! {e}") - db.rollback() - return False - finally: - db.close() - LOG.debug(f"delete_friend_from_db {tox_id}") - - 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() - return True - except Exception as e: - LOG.error("save_messages_to_db" +self._name +f" Database exception! {e}") - db.rollback() - return False - finally: - db.close() - LOG.debug(f"save_messages_to_db {tox_id}") - - 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() - return True - except Exception as e: - LOG.error("update_messages" +self._name +f" Database exception! {e}") - db.rollback() - return False - finally: - db.close() - LOG.debug(f"update_messages {tox_id}") - - 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() - return True - except Exception as e: - LOG.error("delete_message" +self._name +f" Database exception! {e}") - db.rollback() - return False - finally: - db.close() - LOG.debug(f"delete_message {tox_id}") - - def delete_messages(self, tox_id): - db = self._connect() - try: - cursor = db.cursor() - cursor.execute('DELETE FROM id' + tox_id + ';') - db.commit() - return True - except Exception as e: - LOG.error("delete_messages" +self._name +f" Database exception! {e}") - db.rollback() - return False - finally: - db.close() - LOG.debug(f"delete_messages {tox_id}") - - 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 deleted file mode 100644 index 971fa29..0000000 --- a/toxygen/history/history.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from history.history_logs_generators import * - -global LOG -import logging -LOG = logging.getLogger('app.db') - -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 - """ - # me a mistake? was _db not _history - if self._settings['save_history']: - 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) - assert history - with open(file_name, 'wt') as fl: - fl.write(history) - LOG.info(f"wrote history to {file_name}") - - 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 deleted file mode 100644 index 91c0a28..0000000 --- a/toxygen/history/history_logs_generators.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import utils.util as util -from messenger.messages import * - - -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/images/accept.png b/toxygen/images/accept.png old mode 100644 new mode 100755 index eedb818..aaa1388 Binary files a/toxygen/images/accept.png and b/toxygen/images/accept.png differ diff --git a/toxygen/images/accept_audio.png b/toxygen/images/accept_audio.png old mode 100644 new mode 100755 index 7969974..2fd2818 Binary files a/toxygen/images/accept_audio.png and b/toxygen/images/accept_audio.png differ diff --git a/toxygen/images/accept_video.png b/toxygen/images/accept_video.png old mode 100644 new mode 100755 index bac3af7..2fdebe7 Binary files a/toxygen/images/accept_video.png and b/toxygen/images/accept_video.png differ diff --git a/toxygen/images/audio_message.png b/toxygen/images/audio_message.png new file mode 100755 index 0000000..22ba2a0 Binary files /dev/null and b/toxygen/images/audio_message.png differ diff --git a/toxygen/images/avatar.png b/toxygen/images/avatar.png old mode 100644 new mode 100755 index 06255a1..91d1200 Binary files a/toxygen/images/avatar.png and b/toxygen/images/avatar.png differ diff --git a/toxygen/images/busy.png b/toxygen/images/busy.png index 40b9bff..857b396 100644 Binary files a/toxygen/images/busy.png and b/toxygen/images/busy.png differ diff --git a/toxygen/images/busy_notification.png b/toxygen/images/busy_notification.png index 5f73464..a01eb3f 100644 Binary files a/toxygen/images/busy_notification.png and b/toxygen/images/busy_notification.png differ diff --git a/toxygen/images/call.png b/toxygen/images/call.png old mode 100644 new mode 100755 index 1820653..dc0d672 Binary files a/toxygen/images/call.png and b/toxygen/images/call.png differ diff --git a/toxygen/images/call_video.png b/toxygen/images/call_video.png deleted file mode 100644 index ba153e9..0000000 Binary files a/toxygen/images/call_video.png and /dev/null differ diff --git a/toxygen/images/decline.png b/toxygen/images/decline.png old mode 100644 new mode 100755 index e6313fd..9bbc9d5 Binary files a/toxygen/images/decline.png and b/toxygen/images/decline.png differ diff --git a/toxygen/images/decline_call.png b/toxygen/images/decline_call.png old mode 100644 new mode 100755 index 3ac0b6d..9f39789 Binary files a/toxygen/images/decline_call.png and b/toxygen/images/decline_call.png differ diff --git a/toxygen/images/file.png b/toxygen/images/file.png old mode 100644 new mode 100755 index 526fd10..edbfad9 Binary files a/toxygen/images/file.png and b/toxygen/images/file.png differ diff --git a/toxygen/images/finish_call.png b/toxygen/images/finish_call.png old mode 100644 new mode 100755 index d8d85d7..a08361e Binary files a/toxygen/images/finish_call.png and b/toxygen/images/finish_call.png differ diff --git a/toxygen/images/finish_call_video.png b/toxygen/images/finish_call_video.png deleted file mode 100644 index 9e4f830..0000000 Binary files a/toxygen/images/finish_call_video.png and /dev/null differ diff --git a/toxygen/images/group.png b/toxygen/images/group.png deleted file mode 100644 index 3ea6469..0000000 Binary files a/toxygen/images/group.png and /dev/null differ diff --git a/toxygen/images/icon.png b/toxygen/images/icon.png index 6051ac7..a790ae1 100644 Binary files a/toxygen/images/icon.png and b/toxygen/images/icon.png differ diff --git a/toxygen/images/icon.xcf b/toxygen/images/icon.xcf deleted file mode 100644 index b9fae66..0000000 Binary files a/toxygen/images/icon.xcf and /dev/null differ diff --git a/toxygen/images/icon_new_messages.png b/toxygen/images/icon_new_messages.png old mode 100644 new mode 100755 index aa15890..a3f1900 Binary files a/toxygen/images/icon_new_messages.png and b/toxygen/images/icon_new_messages.png differ diff --git a/toxygen/images/idle.png b/toxygen/images/idle.png index 62fa74c..2550926 100644 Binary files a/toxygen/images/idle.png and b/toxygen/images/idle.png differ diff --git a/toxygen/images/idle_notification.png b/toxygen/images/idle_notification.png index be372f9..29f3b49 100644 Binary files a/toxygen/images/idle_notification.png and b/toxygen/images/idle_notification.png differ diff --git a/toxygen/images/incoming_call.png b/toxygen/images/incoming_call.png old mode 100644 new mode 100755 index 6467b23..b83350a Binary files a/toxygen/images/incoming_call.png and b/toxygen/images/incoming_call.png differ diff --git a/toxygen/images/incoming_call_video.png b/toxygen/images/incoming_call_video.png deleted file mode 100644 index 2301877..0000000 Binary files a/toxygen/images/incoming_call_video.png and /dev/null differ diff --git a/toxygen/images/menu.png b/toxygen/images/menu.png old mode 100644 new mode 100755 index 72bd478..4d72f03 Binary files a/toxygen/images/menu.png and b/toxygen/images/menu.png differ diff --git a/toxygen/images/offline.png b/toxygen/images/offline.png index 54f83b7..70a863b 100644 Binary files a/toxygen/images/offline.png and b/toxygen/images/offline.png differ diff --git a/toxygen/images/offline_notification.png b/toxygen/images/offline_notification.png index 98dc068..77006ed 100644 Binary files a/toxygen/images/offline_notification.png and b/toxygen/images/offline_notification.png differ diff --git a/toxygen/images/online.png b/toxygen/images/online.png index 2381304..1e5f40a 100644 Binary files a/toxygen/images/online.png and b/toxygen/images/online.png differ diff --git a/toxygen/images/online_notification.png b/toxygen/images/online_notification.png index 72b988b..6e85b15 100644 Binary files a/toxygen/images/online_notification.png and b/toxygen/images/online_notification.png differ diff --git a/toxygen/images/pause.png b/toxygen/images/pause.png old mode 100644 new mode 100755 index bbedc4a..5c8ee4c Binary files a/toxygen/images/pause.png and b/toxygen/images/pause.png differ diff --git a/toxygen/images/resume.png b/toxygen/images/resume.png old mode 100644 new mode 100755 index 4ceca74..22bb736 Binary files a/toxygen/images/resume.png and b/toxygen/images/resume.png differ diff --git a/toxygen/images/screenshot.png b/toxygen/images/screenshot.png old mode 100644 new mode 100755 index 9c14c6f..5599da9 Binary files a/toxygen/images/screenshot.png and b/toxygen/images/screenshot.png differ diff --git a/toxygen/images/search.png b/toxygen/images/search.png index 8e4875b..bf0dff6 100644 Binary files a/toxygen/images/search.png and b/toxygen/images/search.png differ diff --git a/toxygen/images/send.png b/toxygen/images/send.png old mode 100644 new mode 100755 index ef17f60..a2aeed8 Binary files a/toxygen/images/send.png and b/toxygen/images/send.png differ diff --git a/toxygen/images/smiley.png b/toxygen/images/smiley.png old mode 100644 new mode 100755 index 98787dc..6b5c0f6 Binary files a/toxygen/images/smiley.png and b/toxygen/images/smiley.png differ diff --git a/toxygen/images/sticker.png b/toxygen/images/sticker.png old mode 100644 new mode 100755 index 901de59..f82eae7 Binary files a/toxygen/images/sticker.png and b/toxygen/images/sticker.png differ diff --git a/toxygen/images/typing.png b/toxygen/images/typing.png old mode 100644 new mode 100755 index 405f80d..26ad69b Binary files a/toxygen/images/typing.png and b/toxygen/images/typing.png differ diff --git a/toxygen/images/video_message.png b/toxygen/images/video_message.png new file mode 100755 index 0000000..37603ce Binary files /dev/null and b/toxygen/images/video_message.png differ diff --git a/toxygen/images/videocall.png b/toxygen/images/videocall.png new file mode 100755 index 0000000..ef9fa86 Binary files /dev/null and b/toxygen/images/videocall.png differ diff --git a/toxygen/libtox.py b/toxygen/libtox.py new file mode 100644 index 0000000..1c30eee --- /dev/null +++ b/toxygen/libtox.py @@ -0,0 +1,53 @@ +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') + 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') + 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') + 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/list_items.py b/toxygen/list_items.py new file mode 100644 index 0000000..81469f7 --- /dev/null +++ b/toxygen/list_items.py @@ -0,0 +1,529 @@ +from toxcore_enums_and_consts import * +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui + QtCore.Slot = QtCore.pyqtSlot +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 +import html as h +import smileys +import settings + + +class MessageEdit(QtGui.QTextBrowser): + + def __init__(self, text, width, message_type, parent=None): + super(MessageEdit, self).__init__(parent) + self.urls = {} + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere) + self.document().setTextWidth(width) + self.setOpenExternalLinks(True) + self.setAcceptRichText(True) + self.setOpenLinks(False) + self.setSearchPaths([smileys.SmileyLoader.get_instance().get_smileys_path()]) + self.document().setDefaultStyleSheet('a { color: #306EFF; }') + text = self.decoratedText(text) + if message_type != TOX_MESSAGE_TYPE['NORMAL']: + self.setHtml('

' + text + '

') + else: + self.setHtml(text) + font = QtGui.QFont() + font.setFamily(settings.Settings.get_instance()['font']) + font.setPixelSize(settings.Settings.get_instance()['message_font_size']) + font.setBold(False) + self.setFont(font) + self.resize(width, self.document().size().height()) + self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) + self.anchorClicked.connect(self.on_anchor_clicked) + + def contextMenuEvent(self, event): + menu = create_menu(self.createStandardContextMenu(event.pos())) + quote = menu.addAction(QtGui.QApplication.translate("MainWindow", 'Quote selected text', None, QtGui.QApplication.UnicodeUTF8)) + 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(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8)) + plug.addActions(submenu) + 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) + + 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() + else: + QtGui.QDesktopServices.openUrl(url) + self.clearFocus() + + def addAnimation(self, url, fileName): + movie = QtGui.QMovie(self) + movie.setFileName(fileName) + self.urls[movie] = url + movie.frameChanged[int].connect(lambda x: self.animate(movie)) + movie.start() + + def animate(self, movie): + self.document().addResource(QtGui.QTextDocument.ImageResource, + self.urls[movie], + movie.currentPixmap()) + self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth()) + + def decoratedText(self, text): + text = h.escape(text) # replace < and > + exp = QtCore.QRegExp( + '(' + '(?:\\b)((www\\.)|(http[s]?|ftp)://)' + '\\w+\\S+)' + '|(?:\\b)(file:///)([\\S| ]*)' + '|(?:\\b)(tox:[a-zA-Z\\d]{76}$)' + '|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)' + '|(?:\\b)(tox:\\S+@\\S+)') + offset = exp.indexIn(text, 0) + while offset != -1: # add links + url = exp.cap() + if exp.cap(2) == 'www.': + html = '{0}'.format(url) + else: + html = '{0}'.format(url) + text = text[:offset] + html + text[offset + len(exp.cap()):] + offset += len(html) + offset = exp.indexIn(text, offset) + arr = text.split('\n') + for i in range(len(arr)): # quotes + if arr[i].startswith('>'): + arr[i] = '' + arr[i][4:] + '' + text = '
'.join(arr) + text = smileys.SmileyLoader.get_instance().add_smileys_to_text(text, self) # smileys + return text + + +class MessageItem(QtGui.QWidget): + """ + Message in messages list + """ + def __init__(self, text, time, user='', sent=True, message_type=TOX_MESSAGE_TYPE['NORMAL'], parent=None): + QtGui.QWidget.__init__(self, parent) + self.name = DataLabel(self) + self.name.setGeometry(QtCore.QRect(2, 2, 95, 20)) + self.name.setTextFormat(QtCore.Qt.PlainText) + font = QtGui.QFont() + font.setFamily(settings.Settings.get_instance()['font']) + font.setPointSize(11) + font.setBold(True) + self.name.setFont(font) + self.name.setText(user) + + self.time = QtGui.QLabel(self) + self.time.setGeometry(QtCore.QRect(parent.width() - 50, 0, 50, 20)) + 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.setMovie(movie) + movie.start() + self.t = True + else: + self.time.setText(convert_time(time)) + self.t = False + + self.message = MessageEdit(text, parent.width() - 150, message_type, self) + if message_type != TOX_MESSAGE_TYPE['NORMAL']: + self.name.setStyleSheet("QLabel { color: #5CB3FF; }") + self.message.setAlignment(QtCore.Qt.AlignCenter) + self.time.setStyleSheet("QLabel { color: #5CB3FF; }") + self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 150, self.message.height())) + self.setFixedHeight(self.message.height()) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x(): + self.listMenu = QtGui.QMenu() + delete_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Delete message', None, QtGui.QApplication.UnicodeUTF8)) + self.connect(delete_item, QtCore.SIGNAL("triggered()"), 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) + + def mark_as_sent(self): + if self.t: + self.time.setText(convert_time(self._time)) + self.t = False + return True + return False + + def set_avatar(self, pixmap): + self.name.setAlignment(QtCore.Qt.AlignCenter) + self.message.setAlignment(QtCore.Qt.AlignVCenter) + self.setFixedHeight(max(self.height(), 36)) + self.name.setFixedHeight(self.height()) + self.message.setFixedHeight(self.height()) + self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) + + +class ContactItem(QtGui.QWidget): + """ + Contact in friends list + """ + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + mode = settings.Settings.get_instance()['compact_mode'] + self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) + self.avatar_label = QtGui.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(QtGui.QWidget): + """ + Connection status + """ + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + self.setGeometry(0, 0, 32, 32) + self.label = QtGui.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(QtGui.QWidget): + + def __init__(self, parent=None): + super(UnreadMessagesCount, self).__init__(parent) + self.resize(30, 20) + self.label = QtGui.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(QtGui.QListWidget): + + def __init__(self, file_name, size, time, user, friend_number, file_number, state, width, parent=None): + + QtGui.QListWidget.__init__(self, parent) + self.resize(QtCore.QSize(width, 34)) + if state == TOX_FILE_TRANSFER_STATE['CANCELLED']: + self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') + elif state in PAUSED_FILE_TRANSFERS: + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') + else: + self.setStyleSheet('QListWidget { border: 1px solid green; }') + self.state = state + + self.name = DataLabel(self) + self.name.setGeometry(QtCore.QRect(3, 7, 95, 20)) + self.name.setTextFormat(QtCore.Qt.PlainText) + font = QtGui.QFont() + font.setFamily(settings.Settings.get_instance()['font']) + font.setPointSize(11) + font.setBold(True) + self.name.setFont(font) + self.name.setText(user) + + self.time = QtGui.QLabel(self) + self.time.setGeometry(QtCore.QRect(width - 53, 7, 50, 20)) + font.setPointSize(10) + font.setBold(False) + self.time.setFont(font) + self.time.setText(convert_time(time)) + + self.cancel = QtGui.QPushButton(self) + self.cancel.setGeometry(QtCore.QRect(width - 120, 2, 30, 30)) + pixmap = QtGui.QPixmap(curr_directory() + '/images/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.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none;}') + + self.accept_or_pause = QtGui.QPushButton(self) + self.accept_or_pause.setGeometry(QtCore.QRect(width - 170, 2, 30, 30)) + if state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + self.accept_or_pause.setVisible(True) + self.button_update('accept') + elif 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 + self.accept_or_pause.setVisible(True) + self.button_update('resume') + 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.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none}') + + self.pb = QtGui.QProgressBar(self) + 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.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 + if not file_size: + file_size = '{}B'.format(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) + self.file_name.setText(file_data) + self.file_name.setToolTip(file_name) + self.saved_name = file_name + self.time_left = QtGui.QLabel(self) + self.time_left.setGeometry(QtCore.QRect(width - 87, 7, 30, 20)) + font.setPointSize(10) + self.time_left.setFont(font) + self.time_left.setVisible(state == TOX_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.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 = QtGui.QFileDialog.getExistingDirectory(self, + QtGui.QApplication.translate("MainWindow", 'Choose folder', None, QtGui.QApplication.UnicodeUTF8), + curr_directory(), + QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) + 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.button_update('pause') + elif self.state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume + self.paused = False + profile.Profile.get_instance().resume_transfer(friend_number, file_number) + self.button_update('pause') + self.state = TOX_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.button_update('resume') + self.accept_or_pause.clearFocus() + + def button_update(self, path): + pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(path)) + icon = QtGui.QIcon(pixmap) + self.accept_or_pause.setIcon(icon) + self.accept_or_pause.setIconSize(QtCore.QSize(30, 30)) + + @QtCore.Slot(int, float, int) + def update(self, state, progress, time): + self.pb.setValue(int(progress * 100)) + if time + 1: + 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']: + 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']: + 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']: + 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']: + 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']: + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') + self.accept_or_pause.setVisible(False) + self.time_left.setVisible(False) + self.pb.setVisible(False) + elif not self.paused: # active + self.pb.setVisible(True) + self.accept_or_pause.setVisible(True) # setup to pause + self.button_update('pause') + self.setStyleSheet('QListWidget { border: 1px solid green; }') + 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) + self._time = time + self.pb.setVisible(False) + movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif') + self.time.setMovie(movie) + movie.start() + + def cancel_transfer(self, *args): + pr = profile.Profile.get_instance() + pr.cancel_not_started_transfer(self._time) + + +class InlineImageItem(QtGui.QScrollArea): + + def __init__(self, data, width, elem): + + QtGui.QScrollArea.__init__(self) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self._elem = elem + self._image_label = QtGui.QLabel(self) + self._image_label.raise_() + self.setWidget(self._image_label) + self._image_label.setScaledContents(False) + self._pixmap = QtGui.QPixmap() + self._pixmap.loadFromData(data, 'PNG') + self._max_size = width - 30 + self._resize_needed = not (self._pixmap.width() <= self._max_size) + self._full_size = not self._resize_needed + if not self._resize_needed: + self._image_label.setPixmap(self._pixmap) + self.resize(QtCore.QSize(self._max_size + 5, self._pixmap.height() + 5)) + self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) + else: + pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) + self._image_label.setPixmap(pixmap) + self.resize(QtCore.QSize(self._max_size + 5, pixmap.height())) + self._image_label.setGeometry(5, 0, self._max_size + 5, pixmap.height()) + self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton and self._resize_needed: # scale inline + if self._full_size: + pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) + self._image_label.setPixmap(pixmap) + self.resize(QtCore.QSize(self._max_size, pixmap.height())) + self._image_label.setGeometry(5, 0, pixmap.width(), pixmap.height()) + else: + self._image_label.setPixmap(self._pixmap) + self.resize(QtCore.QSize(self._max_size, self._pixmap.height() + 17)) + self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) + 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 = QtGui.QFileDialog.getExistingDirectory(self, + QtGui.QApplication.translate("MainWindow", + 'Choose folder', None, + QtGui.QApplication.UnicodeUTF8), + curr_directory(), + QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) + if directory: + fl = QtCore.QFile(directory + '/toxygen_inline_' + curr_time().replace(':', '_') + '.png') + self._pixmap.save(fl, 'PNG') + + return False + + def mark_as_sent(self): + return False + + + + diff --git a/toxygen/loginscreen.py b/toxygen/loginscreen.py new file mode 100644 index 0000000..b6d0811 --- /dev/null +++ b/toxygen/loginscreen.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +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 = QtGui.QPushButton(self) + self.new_profile.setGeometry(QtCore.QRect(20, 150, 171, 27)) + self.new_profile.clicked.connect(self.create_profile) + self.label = QtGui.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 = QtGui.QPushButton(self) + self.load_profile.setGeometry(QtCore.QRect(220, 150, 161, 27)) + self.load_profile.clicked.connect(self.load_ex_profile) + self.default = QtGui.QCheckBox(self) + self.default.setGeometry(QtCore.QRect(220, 110, 131, 22)) + self.groupBox = QtGui.QGroupBox(self) + self.groupBox.setGeometry(QtCore.QRect(210, 40, 181, 151)) + self.comboBox = QtGui.QComboBox(self.groupBox) + self.comboBox.setGeometry(QtCore.QRect(10, 30, 161, 27)) + self.groupBox_2 = QtGui.QGroupBox(self) + self.groupBox_2.setGeometry(QtCore.QRect(10, 40, 191, 151)) + self.toxygen = QtGui.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(QtGui.QApplication.translate("login", "Profile name", None, QtGui.QApplication.UnicodeUTF8)) + self.setWindowTitle(QtGui.QApplication.translate("login", "Log in", None, QtGui.QApplication.UnicodeUTF8)) + self.new_profile.setText(QtGui.QApplication.translate("login", "Create", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("login", "Profile name:", None, QtGui.QApplication.UnicodeUTF8)) + self.load_profile.setText(QtGui.QApplication.translate("login", "Load profile", None, QtGui.QApplication.UnicodeUTF8)) + self.default.setText(QtGui.QApplication.translate("login", "Use as default", None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox.setTitle(QtGui.QApplication.translate("login", "Load existing profile", None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox_2.setTitle(QtGui.QApplication.translate("login", "Create new profile", None, QtGui.QApplication.UnicodeUTF8)) + self.toxygen.setText(QtGui.QApplication.translate("login", "toxygen", None, QtGui.QApplication.UnicodeUTF8)) + + 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 new file mode 100644 index 0000000..6f34785 --- /dev/null +++ b/toxygen/main.py @@ -0,0 +1,462 @@ +import sys +from loginscreen import LoginScreen +import profile +from settings import * +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +from bootstrap import node_generator +from mainscreen import MainWindow +from callbacks import init_callbacks, stop, start +from util import curr_directory, program_version +import styles.style +import platform +import toxencryptsave +from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen +from plugin_support import PluginLoader + + +class Toxygen: + + def __init__(self, path_or_uri=None): + super(Toxygen, self).__init__() + self.tox = self.ms = self.init = 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(toxencryptsave.ToxEncryptSave.get_instance(), tmp) + p.show() + self.app.connect(self.app, QtCore.SIGNAL("lastWindowClosed()"), self.app, QtCore.SLOT("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 = QtGui.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) + + # application color scheme + with open(curr_directory() + '/styles/style.qss') as fl: + dark_style = fl.read() + app.setStyleSheet(dark_style) + + encrypt_save = toxencryptsave.ToxEncryptSave() + + 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.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()")) + 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 = QtGui.QMessageBox() + msgBox.setWindowTitle( + QtGui.QApplication.translate("MainWindow", "Error", None, QtGui.QApplication.UnicodeUTF8)) + text = (QtGui.QApplication.translate("MainWindow", + 'Profile with this name already exists', + None, QtGui.QApplication.UnicodeUTF8)) + 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 = QtGui.QMessageBox.question(None, + 'Profile {}'.format(name), + QtGui.QApplication.translate("login", + 'Do you want to set profile password?', + None, + QtGui.QApplication.UnicodeUTF8), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: + set_pass = SetProfilePasswordScreen(encrypt_save) + set_pass.show() + self.app.connect(self.app, QtCore.SIGNAL("lastWindowClosed()"), self.app, QtCore.SLOT("quit()")) + self.app.exec_() + reply = QtGui.QMessageBox.question(None, + 'Profile {}'.format(name), + QtGui.QApplication.translate("login", + 'Do you want to save profile in default folder? If no, profile will be saved in program folder', + None, + QtGui.QApplication.UnicodeUTF8), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: + path = Settings.get_default_path() + else: + path = curr_directory() + ProfileHelper(path, name).save_profile(self.tox.get_savedata()) + 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 = QtGui.QMessageBox.question(None, + 'Profile {}'.format(name), + QtGui.QApplication.translate("login", 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?', None, QtGui.QApplication.UnicodeUTF8), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) + if reply != QtGui.QMessageBox.Yes: + return + else: + settings.set_active_profile() + + 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 = QtGui.QSystemTrayIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) + self.tray.setObjectName('tray') + + self.ms = MainWindow(self.tox, self.reset, self.tray) + + class Menu(QtGui.QMenu): + + def newStatus(self, status): + profile.Profile.get_instance().set_status(status) + self.aboutToShow() + self.hide() + + def aboutToShow(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(QtGui.QApplication.translate('tray', 'Open Toxygen', None, QtGui.QApplication.UnicodeUTF8)) + self.actions()[1].setText(QtGui.QApplication.translate('tray', 'Set status', None, QtGui.QApplication.UnicodeUTF8)) + self.actions()[2].setText(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8)) + self.act.actions()[0].setText(QtGui.QApplication.translate('tray', 'Online', None, QtGui.QApplication.UnicodeUTF8)) + self.act.actions()[1].setText(QtGui.QApplication.translate('tray', 'Away', None, QtGui.QApplication.UnicodeUTF8)) + self.act.actions()[2].setText(QtGui.QApplication.translate('tray', 'Busy', None, QtGui.QApplication.UnicodeUTF8)) + + m = Menu() + show = m.addAction(QtGui.QApplication.translate('tray', 'Open Toxygen', None, QtGui.QApplication.UnicodeUTF8)) + sub = m.addMenu(QtGui.QApplication.translate('tray', 'Set status', None, QtGui.QApplication.UnicodeUTF8)) + onl = sub.addAction(QtGui.QApplication.translate('tray', 'Online', None, QtGui.QApplication.UnicodeUTF8)) + away = sub.addAction(QtGui.QApplication.translate('tray', 'Away', None, QtGui.QApplication.UnicodeUTF8)) + busy = sub.addAction(QtGui.QApplication.translate('tray', 'Busy', None, QtGui.QApplication.UnicodeUTF8)) + onl.setCheckable(True) + away.setCheckable(True) + busy.setCheckable(True) + m.act = sub + exit = m.addAction(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8)) + + def show_window(): + 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 Settings.get_instance().locked: + show() + else: + def correct_pass(): + show() + Settings.get_instance().locked = False + self.p = UnlockAppScreen(toxencryptsave.ToxEncryptSave.get_instance(), correct_pass) + self.p.show() + + def tray_activated(reason): + if reason == QtGui.QSystemTrayIcon.DoubleClick: + show_window() + + def close_app(): + settings.closing = True + self.ms.close() + + m.connect(show, QtCore.SIGNAL("triggered()"), show_window) + m.connect(exit, QtCore.SIGNAL("triggered()"), close_app) + m.connect(m, QtCore.SIGNAL("aboutToShow()"), lambda: m.aboutToShow()) + sub.connect(onl, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(0)) + sub.connect(away, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(1)) + sub.connect(busy, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(2)) + + self.tray.setContextMenu(m) + self.tray.show() + self.tray.activated.connect(tray_activated) + + self.ms.show() + + 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.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("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() + 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) + # bootstrap + try: + for data in node_generator(): + 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 node_generator(): + 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] + + +def clean(): + """Removes all windows libs from libs folder""" + d = curr_directory() + '/libs/' + for fl in ('libtox64.dll', 'libtox.dll', 'libsodium64.a', 'libsodium.a'): + if os.path.exists(d + fl): + os.remove(d + fl) + + +def configure(): + """Removes unused libs""" + d = curr_directory() + '/libs/' + is_64bits = sys.maxsize > 2 ** 32 + if not is_64bits: + if os.path.exists(d + 'libtox64.dll'): + os.remove(d + 'libtox64.dll') + if os.path.exists(d + 'libsodium64.a'): + os.remove(d + 'libsodium64.a') + else: + if os.path.exists(d + 'libtox.dll'): + os.remove(d + 'libtox.dll') + if os.path.exists(d + 'libsodium.a'): + os.remove(d + 'libsodium.a') + try: + os.rename(d + 'libtox64.dll', d + 'libtox.dll') + os.rename(d + 'libsodium64.a', d + 'libsodium.a') + except: + pass + + +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') + return + elif arg == '--configure': + configure() + return + elif arg == '--clean': + clean() + return + else: + toxygen = Toxygen(arg) + toxygen.main() + + +if __name__ == '__main__': + main() diff --git a/toxygen/mainscreen.py b/toxygen/mainscreen.py new file mode 100644 index 0000000..14c9f86 --- /dev/null +++ b/toxygen/mainscreen.py @@ -0,0 +1,651 @@ +# -*- coding: utf-8 -*- + +from menu import * +from profile import * +from list_items import * +from widgets import MultilineEdit, LineEdit +import plugin_support +from mainscreen_widgets import * +import settings + + +class MainWindow(QtGui.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) + if settings.Settings.get_instance()['show_welcome_screen']: + self.ws = WelcomeScreen() + + def setup_menu(self, MainWindow): + self.menubar = QtGui.QMenuBar(MainWindow) + 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 = QtGui.QMenu(self.menubar) + self.menuProfile.setObjectName("menuProfile") + self.menuSettings = QtGui.QMenu(self.menubar) + self.menuSettings.setObjectName("menuSettings") + self.menuPlugins = QtGui.QMenu(self.menubar) + self.menuPlugins.setObjectName("menuPlugins") + self.menuAbout = QtGui.QMenu(self.menubar) + self.menuAbout.setObjectName("menuAbout") + + self.actionAdd_friend = QtGui.QAction(MainWindow) + self.actionAdd_friend.setObjectName("actionAdd_friend") + self.actionprofilesettings = QtGui.QAction(MainWindow) + self.actionprofilesettings.setObjectName("actionprofilesettings") + self.actionPrivacy_settings = QtGui.QAction(MainWindow) + self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") + self.actionInterface_settings = QtGui.QAction(MainWindow) + self.actionInterface_settings.setObjectName("actionInterface_settings") + self.actionNotifications = QtGui.QAction(MainWindow) + self.actionNotifications.setObjectName("actionNotifications") + self.actionNetwork = QtGui.QAction(MainWindow) + self.actionNetwork.setObjectName("actionNetwork") + self.actionAbout_program = QtGui.QAction(MainWindow) + self.actionAbout_program.setObjectName("actionAbout_program") + self.actionSettings = QtGui.QAction(MainWindow) + self.actionSettings.setObjectName("actionSettings") + self.audioSettings = QtGui.QAction(MainWindow) + self.pluginData = QtGui.QAction(MainWindow) + self.importPlugin = QtGui.QAction(MainWindow) + self.lockApp = QtGui.QAction(MainWindow) + self.menuProfile.addAction(self.actionAdd_friend) + 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.menuPlugins.addAction(self.pluginData) + self.menuPlugins.addAction(self.importPlugin) + 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.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.pluginData.triggered.connect(self.plugins_menu) + self.lockApp.triggered.connect(self.lock_app) + self.importPlugin.triggered.connect(self.import_plugin) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + 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(QtGui.QApplication.translate("MainWindow", "Lock", None, QtGui.QApplication.UnicodeUTF8)) + self.menuPlugins.setTitle(QtGui.QApplication.translate("MainWindow", "Plugins", None, QtGui.QApplication.UnicodeUTF8)) + self.pluginData.setText(QtGui.QApplication.translate("MainWindow", "List of plugins", None, QtGui.QApplication.UnicodeUTF8)) + self.menuProfile.setTitle(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8)) + self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) + self.menuAbout.setTitle(QtGui.QApplication.translate("MainWindow", "About", None, QtGui.QApplication.UnicodeUTF8)) + self.actionAdd_friend.setText(QtGui.QApplication.translate("MainWindow", "Add contact", None, QtGui.QApplication.UnicodeUTF8)) + self.actionprofilesettings.setText(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8)) + self.actionPrivacy_settings.setText(QtGui.QApplication.translate("MainWindow", "Privacy", None, QtGui.QApplication.UnicodeUTF8)) + self.actionInterface_settings.setText(QtGui.QApplication.translate("MainWindow", "Interface", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNotifications.setText(QtGui.QApplication.translate("MainWindow", "Notifications", None, QtGui.QApplication.UnicodeUTF8)) + self.actionNetwork.setText(QtGui.QApplication.translate("MainWindow", "Network", None, QtGui.QApplication.UnicodeUTF8)) + self.actionAbout_program.setText(QtGui.QApplication.translate("MainWindow", "About program", None, QtGui.QApplication.UnicodeUTF8)) + self.actionSettings.setText(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) + self.audioSettings.setText(QtGui.QApplication.translate("MainWindow", "Audio", None, QtGui.QApplication.UnicodeUTF8)) + self.contact_name.setPlaceholderText(QtGui.QApplication.translate("MainWindow", "Search", None, QtGui.QApplication.UnicodeUTF8)) + self.sendMessageButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Send message", None, QtGui.QApplication.UnicodeUTF8)) + self.callButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Start audio call with friend", None, QtGui.QApplication.UnicodeUTF8)) + self.online_contacts.clear() + self.online_contacts.addItem(QtGui.QApplication.translate("MainWindow", "All", None, QtGui.QApplication.UnicodeUTF8)) + self.online_contacts.addItem(QtGui.QApplication.translate("MainWindow", "Online", None, QtGui.QApplication.UnicodeUTF8)) + self.online_contacts.setCurrentIndex(int(Settings.get_instance()['show_online_friends'])) + self.importPlugin.setText(QtGui.QApplication.translate("MainWindow", "Import plugin", None, QtGui.QApplication.UnicodeUTF8)) + + 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(10) + font.setFamily(settings.Settings.get_instance()['font']) + self.messageEdit.setFont(font) + + self.sendMessageButton = QtGui.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 = QtGui.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 = QtGui.QComboBox(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, 100)) + Form.setMaximumSize(QtCore.QSize(270, 100)) + Form.setBaseSize(QtCore.QSize(270, 100)) + self.avatar_label = Form.avatar_label = QtGui.QLabel(Form) + self.avatar_label.setGeometry(QtCore.QRect(5, 30, 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, 40, 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, 60, 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, 35, 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, 100) + self.account_avatar = QtGui.QLabel(Form) + self.account_avatar.setGeometry(QtCore.QRect(10, 30, 64, 64)) + self.account_avatar.setScaledContents(False) + self.account_name = DataLabel(Form) + self.account_name.setGeometry(QtCore.QRect(100, 25, 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, 45, 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 = QtGui.QPushButton(Form) + self.callButton.setGeometry(QtCore.QRect(550, 30, 50, 50)) + self.callButton.setObjectName("callButton") + self.callButton.clicked.connect(lambda: self.profile.call_click(True)) + self.videocallButton = QtGui.QPushButton(Form) + self.videocallButton.setGeometry(QtCore.QRect(550, 30, 50, 50)) + self.videocallButton.setObjectName("videocallButton") + self.videocallButton.clicked.connect(lambda: self.profile.call_click(True, True)) + self.update_call_state('call') + self.typing = QtGui.QLabel(Form) + self.typing.setGeometry(QtCore.QRect(500, 50, 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 = QtGui.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.connect(self.friends_list, QtCore.SIGNAL("customContextMenuRequested(QPoint)"), + self.friend_right_click) + self.friends_list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + + def setup_right_center(self, widget): + self.messages = QtGui.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.setFocusPolicy(QtCore.Qt.NoFocus) + + def load(pos): + if not pos: + self.profile.load_history() + self.messages.verticalScrollBar().setValue(1) + self.messages.verticalScrollBar().valueChanged.connect(load) + self.messages.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + self.messages.setSelectionMode(QtGui.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/') + main = QtGui.QWidget() + grid = QtGui.QGridLayout() + search = QtGui.QWidget() + name = QtGui.QWidget() + info = QtGui.QWidget() + main_list = QtGui.QWidget() + messages = QtGui.QWidget() + message_buttons = QtGui.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) + if not Settings.get_instance()['mirror_mode']: + grid.addWidget(search, 1, 0) + grid.addWidget(name, 0, 0) + grid.addWidget(messages, 1, 1, 2, 1) + grid.addWidget(info, 0, 1) + grid.addWidget(message_buttons, 3, 1) + grid.addWidget(main_list, 2, 0, 2, 1) + grid.setColumnMinimumWidth(1, 500) + grid.setColumnMinimumWidth(0, 270) + else: + grid.addWidget(search, 1, 1) + grid.addWidget(name, 0, 1) + grid.addWidget(messages, 1, 0, 2, 1) + grid.addWidget(info, 0, 0) + grid.addWidget(message_buttons, 3, 0) + grid.addWidget(main_list, 2, 1, 2, 1) + grid.setColumnMinimumWidth(0, 500) + grid.setColumnMinimumWidth(1, 270) + grid.setSpacing(0) + grid.setContentsMargins(0, 0, 0, 0) + grid.setRowMinimumHeight(0, 100) + grid.setRowMinimumHeight(1, 25) + grid.setRowMinimumHeight(2, 320) + grid.setRowMinimumHeight(3, 55) + grid.setColumnStretch(1, 1) + grid.setRowStretch(2, 1) + main.setLayout(grid) + self.setCentralWidget(main) + self.setup_menu(self) + self.messageEdit.setFocus() + self.user_info = name + self.friend_info = info + self.retranslateUi() + self.profile = Profile(tox, self) + + def closeEvent(self, event): + self.profile.save_history() + self.profile.close() + s = Settings.get_instance() + if not s['close_to_tray'] or s.closing: + s['x'] = self.geometry().x() + s['y'] = self.geometry().y() + s['width'] = self.width() + s['height'] = self.height() + s.save() + QtGui.QApplication.closeAllWindows() + event.accept() + else: + event.ignore() + self.hide() + + 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, 40, 50, 50)) + self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 40, 50, 50)) + self.typing.setGeometry(QtCore.QRect(self.width() - 450, 50, 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, 40, self.width() - 560, 25)) + self.account_status.setGeometry(QtCore.QRect(100, 60, self.width() - 560, 25)) + self.messageEdit.setFocus() + self.profile.update() + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + 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 = QtGui.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() + else: + super(MainWindow, self).keyPressEvent(event) + + # ----------------------------------------------------------------------------------------------------------------- + # Functions which called when user click in menu + # ----------------------------------------------------------------------------------------------------------------- + + def about_program(self): + import util + msgBox = QtGui.QMessageBox() + msgBox.setWindowTitle(QtGui.QApplication.translate("MainWindow", "About", None, QtGui.QApplication.UnicodeUTF8)) + text = (QtGui.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.\nVersion: ', None, QtGui.QApplication.UnicodeUTF8)) + msgBox.setText(text + util.program_version + '\nGitHub: https://github.com/toxygen-project/toxygen/') + 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 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 import_plugin(self): + import util + directory = QtGui.QFileDialog.getExistingDirectory(self, + QtGui.QApplication.translate("MainWindow", 'Choose folder with plugin', + None, + QtGui.QApplication.UnicodeUTF8), + util.curr_directory(), + QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) + if directory: + src = directory + '/' + dest = curr_directory() + '/plugins/' + util.copy(src, dest) + msgBox = QtGui.QMessageBox() + msgBox.setWindowTitle( + QtGui.QApplication.translate("MainWindow", "Restart Toxygen", None, QtGui.QApplication.UnicodeUTF8)) + msgBox.setText( + QtGui.QApplication.translate("MainWindow", 'Plugin will be loaded after restart', None, + QtGui.QApplication.UnicodeUTF8)) + msgBox.exec_() + + def lock_app(self): + if toxencryptsave.ToxEncryptSave.get_instance().has_password(): + Settings.get_instance().locked = True + self.hide() + else: + msgBox = QtGui.QMessageBox() + msgBox.setWindowTitle( + QtGui.QApplication.translate("MainWindow", "Cannot lock app", None, QtGui.QApplication.UnicodeUTF8)) + msgBox.setText( + QtGui.QApplication.translate("MainWindow", 'Error. Profile password is not set.', None, + QtGui.QApplication.UnicodeUTF8)) + 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 + 1: + choose = QtGui.QApplication.translate("MainWindow", 'Choose file', None, QtGui.QApplication.UnicodeUTF8) + name = QtGui.QFileDialog.getOpenFileName(self, choose, options=QtGui.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: + 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: + 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, fl): + # TODO: do smth with video call button + os.chdir(curr_directory() + '/images/') + pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(fl)) + icon = QtGui.QIcon(pixmap) + self.callButton.setIcon(icon) + self.callButton.setIconSize(QtCore.QSize(50, 50)) + pixmap = QtGui.QPixmap(curr_directory() + '/images/videocall.png') + 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) + settings = Settings.get_instance() + allowed = friend.tox_id in settings['auto_accept_from_friends'] + auto = QtGui.QApplication.translate("MainWindow", 'Disallow auto accept', None, QtGui.QApplication.UnicodeUTF8) if allowed else QtGui.QApplication.translate("MainWindow", 'Allow auto accept', None, QtGui.QApplication.UnicodeUTF8) + if item is not None: + self.listMenu = QtGui.QMenu() + set_alias_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Set alias', None, QtGui.QApplication.UnicodeUTF8)) + + history_menu = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Chat history', None, QtGui.QApplication.UnicodeUTF8)) + clear_history_item = history_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Clear history', None, QtGui.QApplication.UnicodeUTF8)) + export_to_text_item = history_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Export as text', None, QtGui.QApplication.UnicodeUTF8)) + export_to_html_item = history_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Export as HTML', None, QtGui.QApplication.UnicodeUTF8)) + + copy_menu = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Copy', None, QtGui.QApplication.UnicodeUTF8)) + copy_name_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Name', None, QtGui.QApplication.UnicodeUTF8)) + copy_status_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Status message', None, QtGui.QApplication.UnicodeUTF8)) + copy_key_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Public key', None, QtGui.QApplication.UnicodeUTF8)) + + auto_accept_item = self.listMenu.addAction(auto) + remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8)) + notes_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Notes', None, QtGui.QApplication.UnicodeUTF8)) + + submenu = plugin_support.PluginLoader.get_instance().get_menu(self.listMenu, num) + if len(submenu): + plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8)) + plug.addActions(submenu) + self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) + self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) + self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) + self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num)) + self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed)) + self.connect(notes_item, QtCore.SIGNAL("triggered()"), lambda: self.show_note(friend)) + self.connect(copy_name_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_name(friend)) + self.connect(copy_status_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_status(friend)) + self.connect(export_to_text_item, QtCore.SIGNAL("triggered()"), lambda: self.export_history(num)) + self.connect(export_to_html_item, QtCore.SIGNAL("triggered()"), + 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 = QtGui.QApplication.translate("MainWindow", 'Notes about user', None, QtGui.QApplication.UnicodeUTF8) + 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) + directory = QtGui.QFileDialog.getExistingDirectory(None, + QtGui.QApplication.translate("MainWindow", 'Choose folder', + None, + QtGui.QApplication.UnicodeUTF8), + curr_directory(), + QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) + + if directory: + name = 'exported_history_{}.{}'.format(convert_time(time.time()), 'txt' if as_text else 'html') + with open(directory + '/' + 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 copy_friend_key(self, num): + tox_id = self.profile.friend_public_key(num) + clipboard = QtGui.QApplication.clipboard() + clipboard.setText(tox_id) + + def copy_name(self, friend): + clipboard = QtGui.QApplication.clipboard() + clipboard.setText(friend.name) + + def copy_status(self, friend): + clipboard = QtGui.QApplication.clipboard() + clipboard.setText(friend.status_message) + + def clear_history(self, num): + self.profile.clear_history(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() + + # ----------------------------------------------------------------------------------------------------------------- + # 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 filtering(self): + self.profile.filtration(self.online_contacts.currentIndex() == 1, self.contact_name.text()) diff --git a/toxygen/mainscreen_widgets.py b/toxygen/mainscreen_widgets.py new file mode 100644 index 0000000..09c3f36 --- /dev/null +++ b/toxygen/mainscreen_widgets.py @@ -0,0 +1,394 @@ +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget +from profile import Profile +import smileys +import util + + +class MessageArea(QtGui.QPlainTextEdit): + """User types messages here""" + + def __init__(self, parent, form): + super(MessageArea, self).__init__(parent) + self.parent = form + self.setAcceptDrops(True) + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(lambda: self.parent.profile.send_typing(False)) + + def keyPressEvent(self, event): + if event.matches(QtGui.QKeySequence.Paste): + mimeData = QtGui.QApplication.clipboard().mimeData() + if mimeData.hasUrls(): + for url in mimeData.urls(): + self.pasteEvent(url.toString()) + else: + self.pasteEvent() + elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + modifiers = event.modifiers() + 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() + elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): + self.appendPlainText(Profile.get_instance().get_last_message()) + else: + self.parent.profile.send_typing(True) + if self.timer.isActive(): + self.timer.stop() + self.timer.start(5000) + super(MessageArea, self).keyPressEvent(event) + + def contextMenuEvent(self, event): + menu = create_menu(self.createStandardContextMenu()) + menu.exec_(event.globalPos()) + del menu + + def dragEnterEvent(self, e): + e.accept() + + def dragMoveEvent(self, e): + e.accept() + + def dropEvent(self, e): + if e.mimeData().hasFormat('text/plain') or e.mimeData().hasFormat('text/html'): + e.accept() + self.pasteEvent(e.mimeData().text()) + elif e.mimeData().hasUrls(): + for url in e.mimeData().urls(): + self.pasteEvent(url.toString()) + e.accept() + else: + e.ignore() + + def pasteEvent(self, text=None): + text = text or QtGui.QApplication.clipboard().text() + if text.startswith('file://'): + self.parent.profile.send_file(text[7:]) + else: + self.insertPlainText(text) + + +class ScreenShotWindow(QtGui.QWidget): + + def __init__(self, parent): + super(ScreenShotWindow, self).__init__() + self.parent = parent + self.setMouseTracking(True) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) + self.showFullScreen() + self.setWindowOpacity(0.5) + self.rubberband = RubberBand() + + def closeEvent(self, *args): + if self.parent.isHidden(): + self.parent.show() + + def mousePressEvent(self, event): + self.origin = event.pos() + self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize())) + self.rubberband.show() + QtGui.QWidget.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + if self.rubberband.isVisible(): + self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized()) + left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height())) + right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height())) + top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y()) + bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height()) + self.setMask(left + right + top + bottom) + + def mouseReleaseEvent(self, event): + if self.rubberband.isVisible(): + self.rubberband.hide() + rect = self.rubberband.geometry() + if rect.width() and rect.height(): + p = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop().winId(), + rect.x() + 4, + rect.y() + 4, + rect.width() - 8, + rect.height() - 8) + byte_array = QtCore.QByteArray() + buffer = QtCore.QBuffer(byte_array) + buffer.open(QtCore.QIODevice.WriteOnly) + p.save(buffer, 'PNG') + Profile.get_instance().send_screenshot(bytes(byte_array.data())) + self.close() + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + self.rubberband.setHidden(True) + self.close() + else: + super(ScreenShotWindow, self).keyPressEvent(event) + + +class SmileyWindow(QtGui.QWidget): + """ + Smiley selection window + """ + + def __init__(self, parent): + super(SmileyWindow, self).__init__() + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + inst = smileys.SmileyLoader.get_instance() + self.data = inst.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 + 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 + elem = QtGui.QRadioButton(self) + elem.setGeometry(QtCore.QRect(i * 20 + 5, 180, 20, 20)) + elem.clicked.connect(lambda i=i: self.checked(i)) + 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 + b = QtGui.QPushButton(self) + b.setGeometry(QtCore.QRect((i // 8) * 20 + 5, (i % 8) * 20, 20, 20)) + b.clicked.connect(lambda i=i: self.clicked(i)) + 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() + + def leaveEvent(self, event): + self.close() + + +class MenuButton(QtGui.QPushButton): + + def __init__(self, parent, enter): + super(MenuButton, self).__init__(parent) + self.enter = enter + + def enterEvent(self, event): + self.enter() + super(MenuButton, self).enterEvent(event) + + +class DropdownMenu(QtGui.QWidget): + + def __init__(self, parent): + super(DropdownMenu, self).__init__(parent) + self.installEventFilter(self) + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + self.setMaximumSize(180, 120) + self.setMinimumSize(180, 120) + self.screenshotButton = QRightClickButton(self) + self.screenshotButton.setGeometry(QtCore.QRect(0, 60, 60, 60)) + self.screenshotButton.setObjectName("screenshotButton") + + self.fileTransferButton = QtGui.QPushButton(self) + self.fileTransferButton.setGeometry(QtCore.QRect(60, 60, 60, 60)) + self.fileTransferButton.setObjectName("fileTransferButton") + + self.audioMessageButton = QtGui.QPushButton(self) + self.audioMessageButton.setGeometry(QtCore.QRect(120, 60, 60, 60)) + + self.smileyButton = QtGui.QPushButton(self) + self.smileyButton.setGeometry(QtCore.QRect(0, 0, 60, 60)) + + self.videoMessageButton = QtGui.QPushButton(self) + self.videoMessageButton.setGeometry(QtCore.QRect(120, 0, 60, 60)) + + self.stickerButton = QtGui.QPushButton(self) + self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60)) + + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/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') + icon = QtGui.QIcon(pixmap) + self.screenshotButton.setIcon(icon) + self.screenshotButton.setIconSize(QtCore.QSize(50, 60)) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/audio_message.png') + icon = QtGui.QIcon(pixmap) + self.audioMessageButton.setIcon(icon) + self.audioMessageButton.setIconSize(QtCore.QSize(50, 50)) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/smiley.png') + icon = QtGui.QIcon(pixmap) + self.smileyButton.setIcon(icon) + self.smileyButton.setIconSize(QtCore.QSize(50, 50)) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/video_message.png') + icon = QtGui.QIcon(pixmap) + self.videoMessageButton.setIcon(icon) + self.videoMessageButton.setIconSize(QtCore.QSize(55, 55)) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/sticker.png') + icon = QtGui.QIcon(pixmap) + self.stickerButton.setIcon(icon) + self.stickerButton.setIconSize(QtCore.QSize(55, 55)) + + self.screenshotButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send screenshot", None, QtGui.QApplication.UnicodeUTF8)) + self.fileTransferButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send file", None, QtGui.QApplication.UnicodeUTF8)) + self.audioMessageButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send audio message", None, QtGui.QApplication.UnicodeUTF8)) + self.videoMessageButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send video message", None, QtGui.QApplication.UnicodeUTF8)) + self.smileyButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Add smiley", None, QtGui.QApplication.UnicodeUTF8)) + self.stickerButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send sticker", None, QtGui.QApplication.UnicodeUTF8)) + + self.fileTransferButton.clicked.connect(parent.send_file) + self.screenshotButton.clicked.connect(parent.send_screenshot) + self.connect(self.screenshotButton, QtCore.SIGNAL("rightClicked()"), lambda: parent.send_screenshot(True)) + self.smileyButton.clicked.connect(parent.send_smiley) + self.stickerButton.clicked.connect(parent.send_sticker) + + def leaveEvent(self, event): + self.close() + + def eventFilter(self, object, event): + if event.type() == QtCore.QEvent.WindowDeactivate: + self.close() + return False + + +class StickerItem(QtGui.QWidget): + + def __init__(self, fl): + super(StickerItem, self).__init__() + self._image_label = QtGui.QLabel(self) + self.path = fl + self.pixmap = QtGui.QPixmap() + self.pixmap.load(fl) + if self.pixmap.width() > 150: + self.pixmap = self.pixmap.scaled(150, 200, QtCore.Qt.KeepAspectRatio) + self.setFixedSize(150, self.pixmap.height()) + self._image_label.setPixmap(self.pixmap) + + +class StickerWindow(QtGui.QWidget): + """Sticker selection window""" + + def __init__(self, parent): + super(StickerWindow, self).__init__() + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + self.setMaximumSize(250, 200) + self.setMinimumSize(250, 200) + self.list = QtGui.QListWidget(self) + self.list.setGeometry(QtCore.QRect(0, 0, 250, 200)) + self.arr = smileys.sticker_loader() + for sticker in self.arr: + item = StickerItem(sticker) + elem = QtGui.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(250, item.height())) + self.list.addItem(elem) + self.list.setItemWidget(elem, item) + self.list.setVerticalScrollMode(QtGui.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]) + self.close() + + def leaveEvent(self, event): + self.close() + + +class WelcomeScreen(CenteredWidget): + + def __init__(self): + super().__init__() + self.setMaximumSize(250, 200) + self.setMinimumSize(250, 200) + self.center() + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.text = QtGui.QTextBrowser(self) + self.text.setGeometry(QtCore.QRect(0, 0, 250, 170)) + self.text.setOpenExternalLinks(True) + self.checkbox = QtGui.QCheckBox(self) + self.checkbox.setGeometry(QtCore.QRect(5, 170, 240, 30)) + self.checkbox.setText(QtGui.QApplication.translate('WelcomeScreen', "Don't show again", + None, QtGui.QApplication.UnicodeUTF8)) + self.setWindowTitle(QtGui.QApplication.translate('WelcomeScreen', 'Tip of the day', + None, QtGui.QApplication.UnicodeUTF8)) + import random + num = random.randint(0, 10) + if num == 0: + text = QtGui.QApplication.translate('WelcomeScreen', 'Press Esc if you want hide app to tray.', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 1: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Right click on screenshot button hides app to tray during screenshot.', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 2: + text = QtGui.QApplication.translate('WelcomeScreen', + 'You can use Tox over Tor. For more info read this post', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 3: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Use Settings -> Interface to customize interface.', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 4: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 5: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Since v0.1.3 Toxygen supports plugins. Read more', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 6: + text = QtGui.QApplication.translate('WelcomeScreen', + 'New in Toxygen v0.2.3:
TCS compliance
Plugins, smileys and stickers import
Bug fixes', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 7: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 8: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu', + None, QtGui.QApplication.UnicodeUTF8) + elif num == 9: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Use right click on inline image to save it', + None, QtGui.QApplication.UnicodeUTF8) + else: + text = QtGui.QApplication.translate('WelcomeScreen', + 'Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.', + None, QtGui.QApplication.UnicodeUTF8) + 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() diff --git a/toxygen/menu.py b/toxygen/menu.py new file mode 100644 index 0000000..1a8bc28 --- /dev/null +++ b/toxygen/menu.py @@ -0,0 +1,901 @@ +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +from settings import * +from profile import Profile +from util import curr_directory, copy +from widgets import CenteredWidget, DataLabel, LineEdit +import pyaudio +import toxencryptsave +import plugin_support + + +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 = QtGui.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 = QtGui.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 = QtGui.QTextEdit(self) + self.message_edit.setGeometry(QtCore.QRect(50, 110, 471, 151)) + self.message_edit.setObjectName("textEdit") + self.message = QtGui.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(QtGui.QApplication.translate('AddContact', "Add contact", None, QtGui.QApplication.UnicodeUTF8)) + self.sendRequestButton.setText(QtGui.QApplication.translate("Form", "Send request", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate('AddContact', "TOX ID:", None, QtGui.QApplication.UnicodeUTF8)) + self.message.setText(QtGui.QApplication.translate('AddContact', "Message:", None, QtGui.QApplication.UnicodeUTF8)) + self.tox_id.setPlaceholderText(QtGui.QApplication.translate('AddContact', "TOX ID or public key of contact", None, QtGui.QApplication.UnicodeUTF8)) + + +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 = QtGui.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 = QtGui.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 = QtGui.QLabel(self) + self.label_2.setGeometry(QtCore.QRect(40, 100, 100, 25)) + self.label_2.setFont(font) + self.label_3 = QtGui.QLabel(self) + self.label_3.setGeometry(QtCore.QRect(40, 180, 100, 25)) + self.label_3.setFont(font) + self.tox_id = QtGui.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 = QtGui.QPushButton(self) + self.copyId.setGeometry(QtCore.QRect(40, 250, 180, 30)) + self.copyId.clicked.connect(self.copy) + self.export = QtGui.QPushButton(self) + self.export.setGeometry(QtCore.QRect(230, 250, 180, 30)) + self.export.clicked.connect(self.export_profile) + self.new_nospam = QtGui.QPushButton(self) + self.new_nospam.setGeometry(QtCore.QRect(420, 250, 180, 30)) + self.new_nospam.clicked.connect(self.new_no_spam) + self.copy_pk = QtGui.QPushButton(self) + self.copy_pk.setGeometry(QtCore.QRect(40, 300, 180, 30)) + self.copy_pk.clicked.connect(self.copy_public_key) + self.new_avatar = QtGui.QPushButton(self) + self.new_avatar.setGeometry(QtCore.QRect(230, 300, 180, 30)) + self.delete_avatar = QtGui.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 = QtGui.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(QtGui.QLineEdit.EchoMode.Password) + self.leave_blank = QtGui.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(QtGui.QLineEdit.EchoMode.Password) + self.set_password = QtGui.QPushButton(self) + self.set_password.setGeometry(QtCore.QRect(40, 470, 300, 30)) + self.set_password.clicked.connect(self.new_password) + self.not_match = QtGui.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 = QtGui.QLabel(self) + self.warning.setGeometry(QtCore.QRect(40, 510, 500, 30)) + self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') + self.default = QtGui.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(QtGui.QApplication.translate("ProfileSettingsForm", "Export profile", None, QtGui.QApplication.UnicodeUTF8)) + self.setWindowTitle(QtGui.QApplication.translate("ProfileSettingsForm", "Profile settings", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Name:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Status:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("ProfileSettingsForm", "TOX ID:", None, QtGui.QApplication.UnicodeUTF8)) + self.copyId.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Copy TOX ID", None, QtGui.QApplication.UnicodeUTF8)) + self.new_avatar.setText(QtGui.QApplication.translate("ProfileSettingsForm", "New avatar", None, QtGui.QApplication.UnicodeUTF8)) + self.delete_avatar.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Reset avatar", None, QtGui.QApplication.UnicodeUTF8)) + self.new_nospam.setText(QtGui.QApplication.translate("ProfileSettingsForm", "New NoSpam", None, QtGui.QApplication.UnicodeUTF8)) + self.profilepass.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Profile password", None, QtGui.QApplication.UnicodeUTF8)) + self.password.setPlaceholderText(QtGui.QApplication.translate("ProfileSettingsForm", "Password (at least 8 symbols)", None, QtGui.QApplication.UnicodeUTF8)) + self.confirm_password.setPlaceholderText(QtGui.QApplication.translate("ProfileSettingsForm", "Confirm password", None, QtGui.QApplication.UnicodeUTF8)) + self.set_password.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Set password", None, QtGui.QApplication.UnicodeUTF8)) + self.not_match.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Passwords do not match", None, QtGui.QApplication.UnicodeUTF8)) + self.leave_blank.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Leaving blank will reset current password", None, QtGui.QApplication.UnicodeUTF8)) + self.warning.setText(QtGui.QApplication.translate("ProfileSettingsForm", "There is no way to recover lost passwords", None, QtGui.QApplication.UnicodeUTF8)) + self.status.addItem(QtGui.QApplication.translate("ProfileSettingsForm", "Online", None, QtGui.QApplication.UnicodeUTF8)) + self.status.addItem(QtGui.QApplication.translate("ProfileSettingsForm", "Away", None, QtGui.QApplication.UnicodeUTF8)) + self.status.addItem(QtGui.QApplication.translate("ProfileSettingsForm", "Busy", None, QtGui.QApplication.UnicodeUTF8)) + self.copy_pk.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Copy public key", None, QtGui.QApplication.UnicodeUTF8)) + if self.auto: + self.default.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Mark as not default profile", None, QtGui.QApplication.UnicodeUTF8)) + else: + self.default.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Mark as default profile", None, QtGui.QApplication.UnicodeUTF8)) + + 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(QtGui.QApplication.translate("ProfileSettingsForm", "Mark as not default profile", None, + QtGui.QApplication.UnicodeUTF8)) + else: + self.default.setText( + QtGui.QApplication.translate("ProfileSettingsForm", "Mark as default profile", None, + QtGui.QApplication.UnicodeUTF8)) + + 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 = toxencryptsave.ToxEncryptSave.get_instance() + e.set_password(self.password.text()) + self.close() + else: + self.not_match.setText( + QtGui.QApplication.translate("ProfileSettingsForm", "Password must be at least 8 symbols", None, + QtGui.QApplication.UnicodeUTF8)) + self.not_match.setVisible(True) + else: + self.not_match.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Passwords do not match", None, + QtGui.QApplication.UnicodeUTF8)) + self.not_match.setVisible(True) + + def copy(self): + clipboard = QtGui.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 = QtGui.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 = QtGui.QApplication.translate("ProfileSettingsForm", "Choose avatar", None, QtGui.QApplication.UnicodeUTF8) + name = QtGui.QFileDialog.getOpenFileName(self, choose, None, 'Images (*.png)', + options=QtGui.QFileDialog.DontUseNativeDialog) + if name[0]: + bitmap = QtGui.QPixmap(name[0]) + bitmap.scaled(QtCore.QSize(128, 128), aspectMode=QtCore.Qt.KeepAspectRatio, + mode=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 = QtGui.QFileDialog.getExistingDirectory(options=QtGui.QFileDialog.DontUseNativeDialog, + dir=curr_directory()) + '/' + if directory != '/': + reply = QtGui.QMessageBox.question(None, + QtGui.QApplication.translate("ProfileSettingsForm", + 'Use new path', + None, + QtGui.QApplication.UnicodeUTF8), + QtGui.QApplication.translate("ProfileSettingsForm", + 'Do you want to move your profile to this location?', + None, + QtGui.QApplication.UnicodeUTF8), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) + settings = Settings.get_instance() + settings.export(directory) + profile = Profile.get_instance() + profile.export_db(directory) + ProfileHelper.get_instance().export_profile(directory, reply == QtGui.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, 330) + self.setMinimumSize(QtCore.QSize(300, 330)) + self.setMaximumSize(QtCore.QSize(300, 330)) + self.setBaseSize(QtCore.QSize(300, 330)) + self.ipv = QtGui.QCheckBox(self) + self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22)) + self.ipv.setObjectName("ipv") + self.udp = QtGui.QCheckBox(self) + self.udp.setGeometry(QtCore.QRect(150, 10, 97, 22)) + self.udp.setObjectName("udp") + self.proxy = QtGui.QCheckBox(self) + self.proxy.setGeometry(QtCore.QRect(20, 40, 97, 22)) + self.http = QtGui.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 = QtGui.QLabel(self) + self.label.setGeometry(QtCore.QRect(40, 100, 66, 17)) + self.label_2 = QtGui.QLabel(self) + self.label_2.setGeometry(QtCore.QRect(40, 165, 66, 17)) + self.reconnect = QtGui.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 = QtGui.QLabel(self) + self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60)) + self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') + self.retranslateUi() + self.proxy.stateChanged.connect(lambda x: self.activate()) + self.activate() + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle(QtGui.QApplication.translate("NetworkSettings", "Network settings", None, QtGui.QApplication.UnicodeUTF8)) + self.ipv.setText(QtGui.QApplication.translate("Form", "IPv6", None, QtGui.QApplication.UnicodeUTF8)) + self.udp.setText(QtGui.QApplication.translate("Form", "UDP", None, QtGui.QApplication.UnicodeUTF8)) + self.proxy.setText(QtGui.QApplication.translate("Form", "Proxy", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "IP:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("Form", "Port:", None, QtGui.QApplication.UnicodeUTF8)) + self.reconnect.setText(QtGui.QApplication.translate("NetworkSettings", "Restart TOX core", None, QtGui.QApplication.UnicodeUTF8)) + self.http.setText(QtGui.QApplication.translate("Form", "HTTP", None, QtGui.QApplication.UnicodeUTF8)) + self.warning.setText(QtGui.QApplication.translate("Form", "WARNING:\nusing proxy with enabled UDP\ncan produce IP leak", + None, QtGui.QApplication.UnicodeUTF8)) + + 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.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 = QtGui.QCheckBox(self) + self.saveHistory.setGeometry(QtCore.QRect(10, 20, 350, 22)) + self.saveUnsentOnly = QtGui.QCheckBox(self) + self.saveUnsentOnly.setGeometry(QtCore.QRect(10, 60, 350, 22)) + + self.fileautoaccept = QtGui.QCheckBox(self) + self.fileautoaccept.setGeometry(QtCore.QRect(10, 100, 350, 22)) + + self.typingNotifications = QtGui.QCheckBox(self) + self.typingNotifications.setGeometry(QtCore.QRect(10, 140, 350, 30)) + self.inlines = QtGui.QCheckBox(self) + self.inlines.setGeometry(QtCore.QRect(10, 180, 350, 30)) + self.auto_path = QtGui.QLabel(self) + self.auto_path.setGeometry(QtCore.QRect(10, 230, 350, 30)) + self.path = QtGui.QPlainTextEdit(self) + self.path.setGeometry(QtCore.QRect(10, 265, 350, 45)) + self.change_path = QtGui.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 = QtGui.QLabel(self) + self.block_user_label.setGeometry(QtCore.QRect(10, 360, 350, 30)) + self.block_id = QtGui.QPlainTextEdit(self) + self.block_id.setGeometry(QtCore.QRect(10, 390, 350, 30)) + self.block = QtGui.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 = QtGui.QLabel(self) + self.blocked_users_label.setGeometry(QtCore.QRect(10, 470, 350, 30)) + self.comboBox = QtGui.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(10, 500, 350, 30)) + self.comboBox.addItems(settings['blocked']) + self.unblock = QtGui.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(QtGui.QApplication.translate("privacySettings", "Privacy settings", None, QtGui.QApplication.UnicodeUTF8)) + self.saveHistory.setText(QtGui.QApplication.translate("privacySettings", "Save chat history", None, QtGui.QApplication.UnicodeUTF8)) + self.fileautoaccept.setText(QtGui.QApplication.translate("privacySettings", "Allow file auto accept", None, QtGui.QApplication.UnicodeUTF8)) + self.typingNotifications.setText(QtGui.QApplication.translate("privacySettings", "Send typing notifications", None, QtGui.QApplication.UnicodeUTF8)) + self.auto_path.setText(QtGui.QApplication.translate("privacySettings", "Auto accept default path:", None, QtGui.QApplication.UnicodeUTF8)) + self.change_path.setText(QtGui.QApplication.translate("privacySettings", "Change", None, QtGui.QApplication.UnicodeUTF8)) + self.inlines.setText(QtGui.QApplication.translate("privacySettings", "Allow inlines", None, QtGui.QApplication.UnicodeUTF8)) + self.block_user_label.setText(QtGui.QApplication.translate("privacySettings", "Block by public key:", None, QtGui.QApplication.UnicodeUTF8)) + self.blocked_users_label.setText(QtGui.QApplication.translate("privacySettings", "Blocked users:", None, QtGui.QApplication.UnicodeUTF8)) + self.unblock.setText(QtGui.QApplication.translate("privacySettings", "Unblock", None, QtGui.QApplication.UnicodeUTF8)) + self.block.setText(QtGui.QApplication.translate("privacySettings", "Block user", None, QtGui.QApplication.UnicodeUTF8)) + self.saveUnsentOnly.setText(QtGui.QApplication.translate("privacySettings", "Save unsent messages only", None, QtGui.QApplication.UnicodeUTF8)) + + 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 = QtGui.QApplication.translate("privacySettings", "Add to friend list", None, QtGui.QApplication.UnicodeUTF8) + info = QtGui.QApplication.translate("privacySettings", "Do you want to add this user to friend list?", None, QtGui.QApplication.UnicodeUTF8) + reply = QtGui.QMessageBox.question(None, title, info, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + Profile.get_instance().unblock_user(self.comboBox.currentText(), reply == QtGui.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 = QtGui.QMessageBox.question(None, + QtGui.QApplication.translate("privacySettings", + 'Chat history', + None, QtGui.QApplication.UnicodeUTF8), + QtGui.QApplication.translate("privacySettings", + 'History will be cleaned! Continue?', + None, QtGui.QApplication.UnicodeUTF8), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) + if reply == QtGui.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 = QtGui.QMessageBox.question(None, + QtGui.QApplication.translate("privacySettings", + 'Chat history', + None, QtGui.QApplication.UnicodeUTF8), + QtGui.QApplication.translate("privacySettings", + 'History will be cleaned! Continue?', + None, QtGui.QApplication.UnicodeUTF8), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) + if reply == QtGui.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 = QtGui.QFileDialog.getExistingDirectory(options=QtGui.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, 180) + self.setMinimumSize(QtCore.QSize(350, 180)) + self.setMaximumSize(QtCore.QSize(350, 180)) + self.enableNotifications = QtGui.QCheckBox(self) + self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18)) + self.callsSound = QtGui.QCheckBox(self) + self.callsSound.setGeometry(QtCore.QRect(10, 120, 340, 18)) + self.soundNotifications = QtGui.QCheckBox(self) + self.soundNotifications.setGeometry(QtCore.QRect(10, 70, 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.enableNotifications.setChecked(s['notifications']) + self.soundNotifications.setChecked(s['sound_notifications']) + self.callsSound.setChecked(s['calls_sound']) + self.retranslateUi() + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle(QtGui.QApplication.translate("notificationsForm", "Notification settings", None, QtGui.QApplication.UnicodeUTF8)) + self.enableNotifications.setText(QtGui.QApplication.translate("notificationsForm", "Enable notifications", None, QtGui.QApplication.UnicodeUTF8)) + self.callsSound.setText(QtGui.QApplication.translate("notificationsForm", "Enable call\'s sound", None, QtGui.QApplication.UnicodeUTF8)) + self.soundNotifications.setText(QtGui.QApplication.translate("notificationsForm", "Enable sound notifications", None, QtGui.QApplication.UnicodeUTF8)) + + def closeEvent(self, *args, **kwargs): + settings = Settings.get_instance() + settings['notifications'] = self.enableNotifications.isChecked() + settings['sound_notifications'] = self.soundNotifications.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 = QtGui.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 = QtGui.QComboBox(self) + self.themeSelect.setGeometry(QtCore.QRect(30, 40, 120, 30)) + list_of_themes = ['dark'] + self.themeSelect.addItems(list_of_themes) + theme = settings['theme'] + if theme in list_of_themes: + index = list_of_themes.index(theme) + else: + index = 0 + self.themeSelect.setCurrentIndex(index) + self.lang_choose = QtGui.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 = QtGui.QLabel(self) + self.lang.setGeometry(QtCore.QRect(30, 80, 370, 20)) + self.lang.setFont(font) + self.mirror_mode = QtGui.QCheckBox(self) + self.mirror_mode.setGeometry(QtCore.QRect(30, 160, 370, 20)) + self.mirror_mode.setChecked(settings['mirror_mode']) + self.smileys = QtGui.QCheckBox(self) + self.smileys.setGeometry(QtCore.QRect(30, 190, 120, 20)) + self.smileys.setChecked(settings['smileys']) + self.smiley_pack_label = QtGui.QLabel(self) + self.smiley_pack_label.setGeometry(QtCore.QRect(30, 230, 370, 20)) + self.smiley_pack_label.setFont(font) + self.smiley_pack = QtGui.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 = QtGui.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 = QtGui.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, 19)]) + self.messages_font_size.setCurrentIndex(settings['message_font_size'] - 10) + + self.unread = QtGui.QPushButton(self) + self.unread.setGeometry(QtCore.QRect(30, 470, 340, 30)) + self.unread.clicked.connect(self.select_color) + + self.compact_mode = QtGui.QCheckBox(self) + self.compact_mode.setGeometry(QtCore.QRect(30, 380, 370, 20)) + self.compact_mode.setChecked(settings['compact_mode']) + + self.close_to_tray = QtGui.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 = QtGui.QCheckBox(self) + self.show_avatars.setGeometry(QtCore.QRect(30, 440, 370, 20)) + self.show_avatars.setChecked(settings['show_avatars']) + + self.choose_font = QtGui.QPushButton(self) + self.choose_font.setGeometry(QtCore.QRect(30, 510, 340, 30)) + self.choose_font.clicked.connect(self.new_font) + + self.import_smileys = QtGui.QPushButton(self) + self.import_smileys.setGeometry(QtCore.QRect(30, 550, 340, 30)) + self.import_smileys.clicked.connect(self.import_sm) + + self.import_stickers = QtGui.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(QtGui.QApplication.translate("interfaceForm", "Show avatars in chat", None, QtGui.QApplication.UnicodeUTF8)) + self.setWindowTitle(QtGui.QApplication.translate("interfaceForm", "Interface settings", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("interfaceForm", "Theme:", None, QtGui.QApplication.UnicodeUTF8)) + self.lang.setText(QtGui.QApplication.translate("interfaceForm", "Language:", None, QtGui.QApplication.UnicodeUTF8)) + self.smileys.setText(QtGui.QApplication.translate("interfaceForm", "Smileys", None, QtGui.QApplication.UnicodeUTF8)) + self.smiley_pack_label.setText(QtGui.QApplication.translate("interfaceForm", "Smiley pack:", None, QtGui.QApplication.UnicodeUTF8)) + self.mirror_mode.setText(QtGui.QApplication.translate("interfaceForm", "Mirror mode", None, QtGui.QApplication.UnicodeUTF8)) + self.messages_font_size_label.setText(QtGui.QApplication.translate("interfaceForm", "Messages font size:", None, QtGui.QApplication.UnicodeUTF8)) + self.unread.setText(QtGui.QApplication.translate("interfaceForm", "Select unread messages notification color", None, QtGui.QApplication.UnicodeUTF8)) + self.compact_mode.setText(QtGui.QApplication.translate("interfaceForm", "Compact contact list", None, QtGui.QApplication.UnicodeUTF8)) + self.import_smileys.setText(QtGui.QApplication.translate("interfaceForm", "Import smiley pack", None, QtGui.QApplication.UnicodeUTF8)) + self.import_stickers.setText(QtGui.QApplication.translate("interfaceForm", "Import sticker pack", None, QtGui.QApplication.UnicodeUTF8)) + self.close_to_tray.setText(QtGui.QApplication.translate("interfaceForm", "Close to tray", None, QtGui.QApplication.UnicodeUTF8)) + self.choose_font.setText(QtGui.QApplication.translate("interfaceForm", "Select font", None, QtGui.QApplication.UnicodeUTF8)) + + def import_st(self): + directory = QtGui.QFileDialog.getExistingDirectory(self, + QtGui.QApplication.translate("MainWindow", + 'Choose folder with sticker pack', + None, + QtGui.QApplication.UnicodeUTF8), + curr_directory(), + QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) + + if directory: + src = directory + '/' + dest = curr_directory() + '/stickers/' + os.path.basename(directory) + '/' + copy(src, dest) + + def import_sm(self): + directory = QtGui.QFileDialog.getExistingDirectory(self, + QtGui.QApplication.translate("MainWindow", + 'Choose folder with smiley pack', + None, + QtGui.QApplication.UnicodeUTF8), + curr_directory(), + QtGui.QFileDialog.ShowDirsOnly | QtGui.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 = QtGui.QFontDialog.getFont(QtGui.QFont(settings['font'], 10), self) + if ok: + settings['font'] = font.family() + settings.save() + msgBox = QtGui.QMessageBox() + text = QtGui.QApplication.translate("interfaceForm", 'Restart app to apply settings', None, + QtGui.QApplication.UnicodeUTF8) + msgBox.setWindowTitle(QtGui.QApplication.translate("interfaceForm", 'Restart required', None, + QtGui.QApplication.UnicodeUTF8)) + msgBox.setText(text) + msgBox.exec_() + + def select_color(self): + col = QtGui.QColorDialog.getColor() + + if col.isValid(): + settings = Settings.get_instance() + name = col.name() + settings['unread_color'] = name + settings.save() + + def closeEvent(self, event): + settings = Settings.get_instance() + settings['theme'] = str(self.themeSelect.currentText()) + 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 = QtGui.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 = QtGui.QMessageBox() + text = QtGui.QApplication.translate("interfaceForm", 'Restart app to apply settings', None, + QtGui.QApplication.UnicodeUTF8) + msgBox.setWindowTitle(QtGui.QApplication.translate("interfaceForm", 'Restart required', None, + QtGui.QApplication.UnicodeUTF8)) + 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 = QtGui.QLabel(self) + self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20)) + self.out_label = QtGui.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 = QtGui.QComboBox(self) + self.input.setGeometry(QtCore.QRect(25, 30, 350, 30)) + self.output = QtGui.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(QtGui.QApplication.translate("audioSettingsForm", "Audio settings", None, QtGui.QApplication.UnicodeUTF8)) + self.in_label.setText(QtGui.QApplication.translate("audioSettingsForm", "Input device:", None, QtGui.QApplication.UnicodeUTF8)) + self.out_label.setText(QtGui.QApplication.translate("audioSettingsForm", "Output device:", None, QtGui.QApplication.UnicodeUTF8)) + + 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 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 = QtGui.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30)) + self.label = QtGui.QLabel(self) + self.label.setGeometry(QtCore.QRect(30, 40, 340, 90)) + self.label.setWordWrap(True) + self.button = QtGui.QPushButton(self) + self.button.setGeometry(QtCore.QRect(30, 130, 340, 30)) + self.button.clicked.connect(self.button_click) + self.open = QtGui.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(QtGui.QApplication.translate('PluginsForm', "Plugins", None, QtGui.QApplication.UnicodeUTF8)) + self.open.setText(QtGui.QApplication.translate('PluginsForm', "Open selected plugin", None, QtGui.QApplication.UnicodeUTF8)) + + 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 = QtGui.QMessageBox() + text = QtGui.QApplication.translate("PluginsForm", 'No GUI found for this plugin', None, + QtGui.QApplication.UnicodeUTF8) + msgBox.setWindowTitle(QtGui.QApplication.translate("PluginsForm", 'Error', None, + QtGui.QApplication.UnicodeUTF8)) + 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 QtGui.QApplication.translate("PluginsForm", "No description available", None, QtGui.QApplication.UnicodeUTF8) + self.label.setText(descr) + if plugin[1]: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8)) + else: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8)) + else: + self.open.setVisible(False) + self.button.setVisible(False) + self.label.setText(QtGui.QApplication.translate("PluginsForm", "No plugins found", None, QtGui.QApplication.UnicodeUTF8)) + + 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(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8)) + else: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/toxygen/messages.py b/toxygen/messages.py new file mode 100644 index 0000000..87a1cc2 --- /dev/null +++ b/toxygen/messages.py @@ -0,0 +1,101 @@ + + +MESSAGE_TYPE = { + 'TEXT': 0, + 'ACTION': 1, + 'FILE_TRANSFER': 2, + 'INLINE': 3, + 'INFO_MESSAGE': 4 +} + + +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 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 deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py deleted file mode 100644 index d44a7a9..0000000 --- a/toxygen/messenger/messages.py +++ /dev/null @@ -1,243 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os.path - -from history.database import MESSAGE_AUTHOR -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, iTime): - self._time = iTime - 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): - # FixMe - self._widget = self._create_widget(*args) # pylint: disable=assignment-from-none - - 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): - # overridden - return None - - @staticmethod - def _get_id() -> int: - Message.MESSAGE_ID += 1 - - return int(Message.MESSAGE_ID) - - -class TextMessage(Message): - """ - Plain text or action message - """ - - def __init__(self, message, owner, iTime, message_type, message_id=0): - super().__init__(message_type, owner, iTime) - self._message = message - self._id = message_id - - def get_text(self) -> str: - 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, iTime, message_type, tox_message_id=0): - super().__init__(message, owner, iTime, message_type) - self._tox_message_id = tox_message_id - - def get_tox_message_id(self): - 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, cid, message, owner, iTime, message_type, name): - super().__init__(cid, message, owner, iTime, message_type) - self._user_name = name - - -class TransferMessage(Message): - """ - Message with info about file transfer - """ - - def __init__(self, author, iTime, state, size, file_name, friend_number, file_number): - super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, iTime) - self._state = state - self._size = size - self._file_name = file_name - self._friend_number, self._file_number = friend_number, file_number - - def is_active(self, file_number) -> bool: - 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) -> int: - 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, iTime): - self._state = state - if self._widget is not None: - self._widget.update_transfer_state(state, percentage, iTime) - - def _create_widget(self, *args): - return FileTransferItem(self, *args) - - -class UnsentFileMessage(TransferMessage): - - def __init__(self, path, data, iTime, author, size, friend_number): - file_name = os.path.basename(path) - super().__init__(author, iTime, 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, iTime): - super().__init__(message, None, iTime, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py deleted file mode 100644 index c38bc31..0000000 --- a/toxygen/messenger/messenger.py +++ /dev/null @@ -1,367 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import logging -import common.tox_save as tox_save -import utils.ui as util_ui - -from messenger.messages import * -from toxygen_wrapper.tests.support_testing import assert_main_thread -from toxygen_wrapper.toxcore_enums_and_consts import TOX_MAX_MESSAGE_LENGTH - -global LOG -LOG = logging.getLogger('app.'+__name__) -log = lambda x: LOG.info(x) - -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 __repr__(self): - return "" - - def get_last_message(self) -> str: - 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) -> None: - """ - 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) -> None: - 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 - - message_type = TOX_MESSAGE_TYPE['NORMAL'] - if False: # undocumented - action_message_prefix = '/me ' - if text.startswith(action_message_prefix): - message_type = TOX_MESSAGE_TYPE['ACTION'] - text = text[len(action_message_prefix):] - - if len(text) > TOX_MAX_MESSAGE_LENGTH: - text = text[:TOX_MAX_MESSAGE_LENGTH] # 1372 - try: - 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) - else: - LOG.warn(f'Unknown friend type for Messenger send_message') - except Exception as e: - LOG.error(f'Messenger send_message {e}') - import traceback - LOG.warn(traceback.format_exc()) - title = 'Messenger send_message Error' - text = 'Error: ' + str(e) - assert_main_thread() - util_ui.message_box(text, title) - - def send_message_to_friend(self, text, message_type, friend_number=None) -> None: - """ - Send message - :param text: message text - :param friend_number: number of friend - from Qt callback - """ - if not text: - return - if friend_number is None: - friend_number = self._contacts_manager.get_active_number() - if friend_number is None or friend_number < 0: - LOG.error(f"No _contacts_manager.get_active_number") - return - assert_main_thread() - - friend = self._get_friend_by_number(friend_number) - if not friend: - LOG.error(f"No self._get_friend_by_number") - return - 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:int) -> None: - """ - 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: - LOG.warn('Sending pending messages failed with ' + str(ex)) - - # Messaging - groups - - def send_message_to_group(self, text, message_type, group_number=None) -> 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) -> None: - """ - 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) - if not group: - LOG.error(f"FixMe new_group_message _get_group_by_number({group_number})") - return - peer = group.get_peer_by_id(peer_id) - if not peer: - LOG.error('FixMe new_group_message group.get_peer_by_id ' + str(peer_id)) - return - text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) - self._add_message(text_message, group) - - # Messaging - group peers - - def send_message_to_group_peer(self, text, message_type, group_number=None, peer_id=None) -> 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: - return - if group.number < 0: - return - if peer_id is not None and peer_id < 0: - return - - assert_main_thread() - group = self._get_group_by_number(group_number) - messages = self._split_message(text.encode('utf-8')) - - # FixMe: peer_id is None? - group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) - if group_peer_contact is None: - LOG.warn("M.group_send_private_message group_peer_contact is None") - return - # group_peer_contact now may be None - - t = util.get_unix_time() - for message in messages: - bRet = self._tox.group_send_private_message(group_number, peer_id, message_type, message) - if not bRet: - LOG.warn("M.group_send_private_messag failed") - continue - message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) - message = OutgoingTextMessage(text, message_author, t, message_type) - # AttributeError: 'GroupChatPeer' object has no attribute 'append_message' - if not hasattr(group_peer_contact, 'append_message'): - LOG.warn("M. group_peer_contact has no append_message group_peer_contact={group_peer_contact}") - else: - 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) -> None: - """ - 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) - if not peer: - LOG.warn('FixMe new_group_private_message group.get_peer_by_id ' + str(peer_id)) - return - text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), - t, message_type) - group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) - if not group_peer_contact: - LOG.warn('FixMe new_group_private_message group_peer_contact ' + str(peer_id)) - return - self._add_message(text_message, group_peer_contact) - - # Message receipts - - def receipt(self, friend_number, message_id) -> None: - friend = self._get_friend_by_number(friend_number) - friend.mark_as_sent(message_id) - - # Typing notifications - - def send_typing(self, typing) -> None: - """ - 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) -> None: - """ - 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) -> None: - 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) -> list: - 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:int): - 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) -> None: - 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) -> None: - 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) -> None: - 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) -> None: - friend = self._get_friend_by_number(friend_number) - assert friend - 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) -> None: - assert_main_thread() - self._items_factory.create_message_item(message) - self._screen.messages.scrollToBottom() - - def _add_message(self, text_message, contact) -> None: - assert_main_thread() - if not contact: - LOG.warn("_add_message null contact") - return - if self._contacts_manager.is_contact_active(contact): # add message to list -# LOG.debug("_add_message is_contact_active(contact)") - self._create_message_item(text_message) - self._screen.messages.scrollToBottom() - self._contacts_manager.get_curr_contact().append_message(text_message) - else: -# LOG.debug("_add_message not is_contact_active(contact)") - contact.inc_messages() - contact.append_message(text_message) - if not contact.visibility: - self._contacts_manager.update_filtration() - - def _create_message_item(self, text_message) -> None: - # 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 deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py deleted file mode 100644 index e0842f7..0000000 --- a/toxygen/middleware/callbacks.py +++ /dev/null @@ -1,775 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import sys -import os -import threading -from qtpy import QtGui -from toxygen_wrapper.toxcore_enums_and_consts import * -from toxygen_wrapper.toxav_enums import * -from toxygen_wrapper.tox import bin_to_string -import utils.ui as util_ui -import utils.util as util -from middleware.threads import invoke_in_main_thread, execute -from notifications.tray import tray_notification -from notifications.sound import * -from datetime import datetime - -iMAX_INT32 = 4294967295 -# callbacks can be called in any thread so were being careful -def LOG_ERROR(l): print(f"EROR. {l}") -def LOG_WARN(l): print(f"WARN. {l}") -def LOG_INFO(l): - bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel <= 20-1 # pylint dusable=undefined-variable - if bIsVerbose: print(f"INFO. {l}") -def LOG_DEBUG(l): - bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel <= 10-1 # pylint dusable=undefined-variable - if bIsVerbose: print(f"DBUG. {l}") -def LOG_TRACE(l): - bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel < 10-1 # pylint dusable=undefined-variable - pass # print(f"TRACE. {l}") - -global aTIMES -aTIMES=dict() -def bTooSoon(key, sSlot, fSec=10.0): - # rate limiting - global aTIMES - if sSlot not in aTIMES: - aTIMES[sSlot] = dict() - OTIME = aTIMES[sSlot] - now = datetime.now() - if key not in OTIME: - OTIME[key] = now - return False - delta = now - OTIME[key] - OTIME[key] = now - if delta.total_seconds() < fSec: return True - return False - -# TODO: refactoring. Use contact provider instead of manager - -# Callbacks - current user - -global iBYTES -iBYTES=0 -def sProcBytes(sFile=None): - if sys.platform == 'win32': return '' - global iBYTES - if sFile is None: - pid = os.getpid() - sFile = f"/proc/{pid}/net/softnet_stat" - if os.path.exists(sFile): - total = 0 - with open(sFile, 'r') as iFd: - for elt in iFd.readlines(): - i = elt.find(' ') - p = int(elt[:i], 16) - total = total + p - if iBYTES == 0: - iBYTES = total - return '' - diff = total - iBYTES - s = f' {diff // 1024} Kbytes' - else: - s = '' - return s - -def self_connection_status(tox, profile): - """ - Current user changed connection status (offline, TCP, UDP) - """ - sSlot = 'self connection status' - def wrapped(tox_link, connection, user_data): - key = f"connection {connection}" - if bTooSoon(key, sSlot, 10): return - s = sProcBytes() - try: - status = tox.self_get_status() if connection != TOX_CONNECTION['NONE'] else None - if status: - LOG_DEBUG(f"self_connection_status: connection={connection} status={status}" +' '+s) - invoke_in_main_thread(profile.set_status, status) - except Exception as e: - LOG_ERROR(f"self_connection_status: {e}") - pass - - return wrapped - - -# Callbacks - friends - - -def friend_status(contacts_manager, file_transfer_handler, profile, settings): - sSlot = 'friend status' - def wrapped(tox, friend_number, new_status, user_data): - """ - Check friend's status (none, busy, away) - """ - LOG_INFO(f"Friend's #{friend_number} status changed") - key = f"friend_number {friend_number}" - if bTooSoon(key, sSlot, 10): return - friend = contacts_manager.get_friend_by_number(friend_number) - if friend.status is None and settings['sound_notifications'] and \ - profile.status != TOX_USER_STATUS['BUSY']: - 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) - """ - LOG_DEBUG(f"Friend #{friend_number} connection status: {new_status}") - friend = contacts_manager.get_friend_by_number(friend_number) - if new_status == TOX_CONNECTION['NONE']: - invoke_in_main_thread(friend.set_status, None) - 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): - sSlot = 'friend_name' - def wrapped(tox, friend_number, name, size, user_data): - """ - Friend changed his name - """ - key = f"friend_number={friend_number}" - if bTooSoon(key, sSlot, 60): return - friend = contacts_provider.get_friend_by_number(friend_number) - old_name = friend.name - new_name = str(name, 'utf-8') - LOG_DEBUG(f"get_friend_by_number #{friend_number} {new_name}") - 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): - sSlot = 'status_message' - def wrapped(tox, friend_number, status_message, size, user_data): - """ - :return: function for callback friend_status_message. It updates friend's status message - and calls window repaint - """ - friend = contacts_manager.get_friend_by_number(friend_number) - key = f"friend_number={friend_number}" - if bTooSoon(key, sSlot, 10): return - - invoke_in_main_thread(friend.set_status_message, str(status_message, 'utf-8')) - LOG_DEBUG(f'User #{friend_number} has new status message') - 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 - """ - LOG_DEBUG(f"friend_message #{friend_number}") - message = str(message, 'utf-8') - invoke_in_main_thread(messenger.new_message, friend_number, message_type, message) - if not window.isActiveWindow(): - 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') - if tray: - 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 - """ - LOG_DEBUG(f'Friend request') - key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) - tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) - invoke_in_main_thread(contacts_manager.process_friend_request, tox_id, str(message, 'utf-8')) - - return wrapped - - -def friend_typing(messenger): - sSlot = "friend_typing" - def wrapped(tox, friend_number, typing, user_data): - key = f"friend_number={friend_number}" - if bTooSoon(key, sSlot, 10): return - LOG_DEBUG(f"friend_typing #{friend_number}") - invoke_in_main_thread(messenger.friend_typing, friend_number, typing) - return wrapped - - -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']: - LOG_INFO(f'file_transfer_handler File friend_number={friend_number}') - 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']) - if tray: - icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) - else: # avatar - LOG_DEBUG(f'file_transfer_handler 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(iToxav, friend_number, mask, user_data): - """ - New call state - """ - LOG_DEBUG(f"call_state #{friend_number}") - if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: - invoke_in_main_thread(calls_manager.stop_call, friend_number, True) - else: - # guessing was calls_manager. - #? incoming_call - calls_manager._call.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 - """ - LOG_DEBUG(f"Incoming call from {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 - """ -#trace LOG_DEBUG(f"callback_audio #{friend_number}") - # dunno was .call - 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 - """ - LOG_DEBUG(f"video_receive_frame from toxav_video_receive_frame_cb={friend_number}") - with ts.ignoreStdout(): import cv2 - import numpy as np - 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) # pylint: disable=no-member - # imshow - invoke_in_main_thread(cv2.imshow, str(friend_number), frame) # pylint: disable=no-member - except Exception as ex: - LOG_ERROR(f"video_receive_frame {ex} #{friend_number}") - pass - -# 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): - LOG_DEBUG(f"group_message #{group_number}") - message = str(message[:length], 'utf-8') - invoke_in_main_thread(messenger.new_group_message, group_number, message_type, message, peer_id) - if window.isActiveWindow(): - return - bl = settings['notify_all_gc'] or profile.name in message - name = tox.group_peer_get_name(group_number, peer_id) - if settings['sound_notifications'] and bl and \ - profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['MESSAGE']) - if False and settings['tray_icon'] and tray: - 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 tray: - 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): - LOG_DEBUG(f"group_private_message #{group_number}") - message = str(message[:length], 'utf-8') - invoke_in_main_thread(messenger.new_group_private_message, group_number, message_type, message, peer_id) - if window.isActiveWindow(): - return - bl = settings['notify_all_gc'] or profile.name in message - try: - name = tox.group_peer_get_name(group_number, peer_id) - except Exception as e: - LOG_WARN("tox.group_peer_get_name {group_number} {peer_id}") - name = '' - if settings['notifications'] and settings['tray_icon'] \ - 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') - if tray and hasattr(tray, 'setIcon'): - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) - - return wrapped - -# Exception ignored on calling ctypes callback function: .wrapped at 0x7ffede910700> -def group_invite(window, settings, tray, profile, groups_service, contacts_provider): - def wrapped(tox, friend_number, invite_data, length, group_name, group_name_length, user_data): - LOG_DEBUG(f"group_invite friend_number={friend_number}") - group_name = str(bytes(group_name[:group_name_length]), 'utf-8') - invoke_in_main_thread(groups_service.process_group_invite, - friend_number, group_name, - bytes(invite_data[:length])) - if window.isActiveWindow(): - return - bHasTray = tray and settings['tray_icon'] - if settings['notifications'] \ - and bHasTray \ - 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) - if tray: - 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): - sSlot = 'group_self_join' - def wrapped(tox, group_number, user_data): - if group_number is None: - LOG_ERROR(f"group_self_join NULL group_number #{group_number}") - return - LOG_DEBUG(f"group_self_join #{group_number}") - key = f"group_number {group_number}" - if bTooSoon(key, sSlot, 10): return - group = contacts_provider.get_group_by_number(group_number) - if group is None: - LOG_ERROR(f"group_self_join NULL group #{group}") - return - 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): - sSlot = "group_peer_join" - def wrapped(tox, group_number, peer_id, user_data): - key = f"group_peer_join #{group_number} peer_id={peer_id}" - if bTooSoon(key, sSlot, 20): return - group = contacts_provider.get_group_by_number(group_number) - if group is None: - LOG_ERROR(f"group_peer_join NULL group #{group} group_number={group_number}") - return - if peer_id > group._peers_limit: - LOG_ERROR(key +f" {peer_id} > {group._peers_limit}") - return - LOG_DEBUG(f"group_peer_join group={group}") - 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, - exit_type, name, name_length, - message, length, - user_data): - group = contacts_provider.get_group_by_number(group_number) - if group: - LOG_DEBUG(f"group_peer_exit #{group_number} peer_id={peer_id} exit_type={exit_type}") - group.remove_peer(peer_id) - invoke_in_main_thread(groups_service.generate_peers_list) - else: - LOG_WARN(f"group_peer_exit group not found #{group_number} peer_id={peer_id}") - - return wrapped - -def group_peer_name(contacts_provider, groups_service): - def wrapped(tox, group_number, peer_id, name, length, user_data): - LOG_DEBUG(f"group_peer_name #{group_number} peer_id={peer_id}") - group = contacts_provider.get_group_by_number(group_number) - peer = group.get_peer_by_id(peer_id) - if peer: - peer.name = str(name[:length], 'utf-8') - invoke_in_main_thread(groups_service.generate_peers_list) - else: - # FixMe: known signal to revalidate roles... - #_peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") - return - - return wrapped - - -def group_peer_status(contacts_provider, groups_service): - def wrapped(tox, group_number, peer_id, peer_status, user_data): - LOG_DEBUG(f"group_peer_status #{group_number} peer_id={peer_id}") - group = contacts_provider.get_group_by_number(group_number) - peer = group.get_peer_by_id(peer_id) - if peer: - peer.status = peer_status - else: - # _peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") - # TODO: add info message - 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): - LOG_DEBUG(f"group_topic #{group_number} peer_id={peer_id}") - group = contacts_provider.get_group_by_number(group_number) - if group: - topic = str(topic[:length], 'utf-8') - invoke_in_main_thread(group.set_status_message, topic) - else: - _peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_WARN(f"group_topic {group} has no peer_id={peer_id} in {_peers}") - # TODO: add info message - - return wrapped - -def group_moderation(groups_service, contacts_provider, contacts_manager, messenger): - def update_peer_role(group, mod_peer_id, peer_id, new_role): - peer = group.get_peer_by_id(peer_id) - if peer: - peer.role = new_role - # TODO: add info message - else: - # FixMe: known signal to revalidate roles... - # _peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_TRACE(f"update_peer_role group {group} has no peer_id={peer_id} in _peers!r") - # TODO: add info message - - def remove_peer(group, mod_peer_id, peer_id, is_ban): - peer = group.get_peer_by_id(peer_id) - if peer: - contacts_manager.remove_group_peer_by_id(group, peer_id) - group.remove_peer(peer_id) - else: - # FixMe: known signal to revalidate roles... - #_peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") - # TODO: add info message - - # source_peer_number, target_peer_number, - def wrapped(tox, group_number, mod_peer_id, peer_id, event_type, user_data): - if mod_peer_id == iMAX_INT32 or peer_id == iMAX_INT32: - # FixMe: known signal to revalidate roles... - return - LOG_DEBUG(f"group_moderation #{group_number} mod_id={mod_peer_id} peer_id={peer_id} event_type={event_type}") - group = contacts_provider.get_group_by_number(group_number) - mod_peer = group.get_peer_by_id(mod_peer_id) - if not mod_peer: - #_peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_TRACE(f"remove_peer group {group} has no mod_peer_id={mod_peer_id} in _peers!r") - return - peer = group.get_peer_by_id(peer_id) - if not peer: - # FixMe: known signal to revalidate roles... - #_peers = [(p._name, p._peer_id) for p in group.get_peers()] - LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") - return - - if event_type == TOX_GROUP_MOD_EVENT['KICK']: - remove_peer(group, mod_peer_id, peer_id, False) - elif event_type == TOX_GROUP_MOD_EVENT['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): - LOG_DEBUG(f"group_password #{group_number}") - password = str(password[:length], 'utf-8') - group = contacts_provider.get_group_by_number(group_number) - group.password = password - - return wrapped - - -def group_peer_limit(contacts_provider): - - def wrapped(tox_link, group_number, peer_limit, user_data): - LOG_DEBUG(f"group_peer_limit #{group_number}") - group = contacts_provider.get_group_by_number(group_number) - group.peer_limit = peer_limit - - return wrapped - - -def group_privacy_state(contacts_provider): - - def wrapped(tox_link, group_number, privacy_state, user_data): - LOG_DEBUG(f"group_privacy_state #{group_number}") - group = contacts_provider.get_group_by_number(group_number) - group.is_private = privacy_state == TOX_GROUP_PRIVACY_STATE['PRIVATE'] - - 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, ms=None): - """ - 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 deleted file mode 100644 index 75e3fc9..0000000 --- a/toxygen/middleware/threads.py +++ /dev/null @@ -1,223 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import sys -import threading -import queue -from qtpy import QtCore - -from bootstrap.bootstrap import * -from bootstrap.bootstrap import download_nodes_list -from toxygen_wrapper.toxcore_enums_and_consts import TOX_USER_STATUS, TOX_CONNECTION -import toxygen_wrapper.tests.support_testing as ts -from utils import util - -import time -sleep = time.sleep - -# LOG=util.log -global LOG -import logging -LOG = logging.getLogger('app.'+'threads') -# log = lambda x: LOG.info(x) - -def LOG_ERROR(l): print('EROR+ '+l) -def LOG_WARN(l): print('WARN+ '+l) -def LOG_INFO(l): print('INFO+ '+l) -def LOG_DEBUG(l): print('DBUG+ '+l) -def LOG_TRACE(l): pass # print('TRAC+ '+l) - -iLAST_CONN = 0 -iLAST_DELTA = 60 - -# Base threads - -class BaseThread(threading.Thread): - - def __init__(self, name=None, target=None): - self._stop_thread = False - if name: - super().__init__(name=name, target=target) - else: - super().__init__(target=target) - - def stop_thread(self, timeout=-1): - self._stop_thread = True - if timeout < 0: - timeout = ts.iTHREAD_TIMEOUT - i = 0 - while i < ts.iTHREAD_JOINS: - self.join(timeout) - if not self.is_alive(): break - i = i + 1 - else: - LOG_WARN(f"BaseThread {self.name} BLOCKED after {ts.iTHREAD_JOINS}") - -class BaseQThread(QtCore.QThread): - - def __init__(self, name=None): - # NO name=name - super().__init__() - self._stop_thread = False - self.name = str(id(self)) - - def stop_thread(self, timeout=-1): - self._stop_thread = True - if timeout < 0: - timeout = ts.iTHREAD_TIMEOUT - i = 0 - while i < ts.iTHREAD_JOINS: - self.wait(timeout) - if not self.isRunning(): break - i = i + 1 - sleep(ts.iTHREAD_TIMEOUT) - else: - LOG_WARN(f"BaseQThread {self.name} BLOCKED") - -# Toxcore threads - -class InitThread(BaseThread): - - def __init__(self, tox, plugin_loader, settings, app, is_first_start): - super().__init__(name='InitThread') - self._tox = tox - self._plugin_loader = plugin_loader - self._settings = settings - self._app = app - self._is_first_start = is_first_start - - def run(self): - # DBUG+ InitThread run: ERROR name 'ts' is not defined - import toxygen_wrapper.tests.support_testing as ts - LOG_DEBUG('InitThread run: ') - try: - if self._is_first_start and ts.bAreWeConnected() and \ - self._settings['download_nodes_list']: - LOG_INFO(f"downloading list of nodes {self._settings['download_nodes_list']}") - download_nodes_list(self._settings, oArgs=self._app._args) - - if ts.bAreWeConnected(): - LOG_INFO(f"calling test_net nodes") - self._app.test_net(oThread=self, iMax=4) - - if self._is_first_start: - LOG_INFO('starting plugins') - self._plugin_loader.load() - - except Exception as e: - LOG_DEBUG(f"InitThread run: ERROR {e}") - pass - - for _ in range(ts.iTHREAD_JOINS): - if self._stop_thread: - return - sleep(ts.iTHREAD_SLEEP) - return - -class ToxIterateThread(BaseQThread): - - def __init__(self, tox, app=None): - super().__init__() - self._tox = tox - self._app = app - - def run(self): - LOG_DEBUG('ToxIterateThread run: ') - while not self._stop_thread: - try: - iMsec = self._tox.iteration_interval() - self._tox.iterate() - except Exception as e: - # Fatal Python error: Segmentation fault - LOG_ERROR(f"ToxIterateThread run: {e}") - else: - sleep(iMsec / 1000.0) - - global iLAST_CONN - if not iLAST_CONN: - iLAST_CONN = time.time() - # TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes - - # and segv - if time.time() - iLAST_CONN > iLAST_DELTA and \ - ts.bAreWeConnected() and \ - self._tox.self_get_status() == TOX_USER_STATUS['NONE'] and \ - self._tox.self_get_connection_status() == TOX_CONNECTION['NONE']: - iLAST_CONN = time.time() - LOG_INFO(f"ToxIterateThread calling test_net") - invoke_in_main_thread( - self._app.test_net, oThread=self, iMax=2) - - -class ToxAVIterateThread(BaseQThread): - def __init__(self, toxav): - super().__init__() - self._toxav = toxav - - def run(self): - LOG_DEBUG('ToxAVIterateThread run: ') - while not self._stop_thread: - self._toxav.iterate() - sleep(self._toxav.iteration_interval() / 1000) - - -# File transfers thread - -class FileTransfersThread(BaseQThread): - - def __init__(self): - super().__init__('FileTransfers') - 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: - LOG_WARN('Queue is full in _thread') - except Exception as ex: - LOG_ERROR('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 deleted file mode 100644 index 1216dd8..0000000 --- a/toxygen/middleware/tox_factory.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import ctypes -import traceback -import os -from ctypes import * - -import user_data.settings -import toxygen_wrapper.tox -import toxygen_wrapper.toxcore_enums_and_consts as enums -from toxygen_wrapper.tests import support_testing as ts -# callbacks can be called in any thread so were being careful -# tox.py can be called by callbacks -from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -global LOG -import logging -LOG = logging.getLogger('app.'+'tox_factory') - -from utils import util -from utils import ui as util_ui - - -def tox_factory(data=None, settings=None, args=None, app=None): - """ - :param data: user data from .tox file. None = no saved data, create new profile - :param settings: current profile settings. None = default settings will be used - :return: new tox instance - """ - if not settings: - LOG_WARN("tox_factory using get_default_settings") - settings = user_data.settings.Settings.get_default_settings() - else: - user_data.settings.clean_settings(settings) - - try: - tox_options = toxygen_wrapper.tox.Tox.options_new() - tox_options.contents.ipv6_enabled = settings['ipv6_enabled'] - tox_options.contents.udp_enabled = settings['udp_enabled'] - tox_options.contents.proxy_type = int(settings['proxy_type']) - if type(settings['proxy_host']) == str: - tox_options.contents.proxy_host = bytes(settings['proxy_host'],'UTF-8') - elif type(settings['proxy_host']) == bytes: - tox_options.contents.proxy_host = settings['proxy_host'] - else: - tox_options.contents.proxy_host = b'' - tox_options.contents.proxy_port = int(settings['proxy_port']) - tox_options.contents.start_port = settings['start_port'] - tox_options.contents.end_port = settings['end_port'] - tox_options.contents.tcp_port = settings['tcp_port'] - tox_options.contents.local_discovery_enabled = settings['local_discovery_enabled'] - tox_options.contents.dht_announcements_enabled = settings['dht_announcements_enabled'] - tox_options.contents.hole_punching_enabled = settings['hole_punching_enabled'] - if data: # load existing profile - tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] - tox_options.contents.savedata_data = ctypes.c_char_p(data) - tox_options.contents.savedata_length = len(data) - else: # create new profile - tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] - tox_options.contents.savedata_data = None - tox_options.contents.savedata_length = 0 - - # overrides - tox_options.contents.local_discovery_enabled = False - tox_options.contents.ipv6_enabled = False - tox_options.contents.hole_punching_enabled = False - - LOG.debug("toxygen_wrapper.tox.Tox settings: " +repr(settings)) - - if 'trace_enabled' in settings and not settings['trace_enabled']: - LOG_DEBUG("settings['trace_enabled' disabled" ) - elif tox_options._options_pointer and \ - 'trace_enabled' in settings and settings['trace_enabled']: - ts.vAddLoggerCallback(tox_options) - LOG_INFO("c-toxcore trace_enabled enabled" ) - else: - LOG_WARN("No tox_options._options_pointer to add self_logger_cb" ) - - retval = toxygen_wrapper.tox.Tox(tox_options) - except Exception as e: - if app and hasattr(app, '_log'): - pass - LOG_ERROR(f"toxygen_wrapper.tox.Tox failed: {e}") - LOG_WARN(traceback.format_exc()) - raise - - if app and hasattr(app, '_log'): - app._log("DEBUG: toxygen_wrapper.tox.Tox succeeded") - return retval diff --git a/toxygen/network/__init__.py b/toxygen/network/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/network/tox_dns.py b/toxygen/network/tox_dns.py deleted file mode 100644 index 58c9da1..0000000 --- a/toxygen/network/tox_dns.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import json -import urllib.request -import logging - -try: - import requests -except ImportError: - requests = None -from qtpy import QtNetwork, QtCore - -import utils.util as util - -global LOG - -iTIMEOUT=60 -LOG = logging.getLogger('app.'+__name__) - -class ToxDns: - - def __init__(self, settings, log=None): - self._settings = settings - self._log = log - - @staticmethod - def _send_request(url, data): - if requests: - LOG.info('send_request loading with requests: ' + str(url)) - headers = dict() - headers['Content-Type'] = 'application/json' - req = requests.get(url, headers=headers) - if req.status_code < 300: - retval = req.content - else: - raise LookupError(str(req.status_code)) - else: - req = urllib.request.Request(url) - req.add_header('Content-Type', 'application/json') - response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) - retval = response.read() - res = json.loads(str(retval, 'utf-8')) - if not res['c']: - return res['tox_id'] - else: - 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 requests: - for url in urls: - LOG.info('TOX nodes loading with requests: ' + str(url)) - try: - headers = dict() - headers['Content-Type'] = 'application/json' - req = requests.get(url, headers=headers, timeout=iTIMEOUT) - if req.status_code < 300: - result = req.content - return result - except Exception as ex: - LOG.error('ERROR: TOX DNS loading error with requests: ' + str(ex)) - - elif not self._settings['proxy_type']: # no proxy - for url in urls: - try: - return self._send_request(url, data) - except Exception as ex: - LOG.error('ERROR: TOX DNS ' + 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: - LOG.error('ERROR: TOX DNS ' + str(ex)) - - return None # error diff --git a/toxygen/notifications.py b/toxygen/notifications.py new file mode 100644 index 0000000..81a8a05 --- /dev/null +++ b/toxygen/notifications.py @@ -0,0 +1,75 @@ +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +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 QtGui.QSystemTrayIcon.isSystemTrayAvailable(): + if len(text) > 30: + text = text[:27] + '...' + tray.showMessage(title, text, QtGui.QSystemTrayIcon.NoIcon, 3000) + QtGui.QApplication.alert(window, 0) + + def message_clicked(): + window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + window.activateWindow() + tray.connect(tray, QtCore.SIGNAL("messageClicked()"), 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 deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/notifications/sound.py b/toxygen/notifications/sound.py deleted file mode 100644 index 9df46b2..0000000 --- a/toxygen/notifications/sound.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os.path -import wave - -import utils.util - -import toxygen_wrapper.tests.support_testing as ts -with ts.ignoreStderr(): - import pyaudio - -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) - -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) - try: - while data: - self.stream.write(data) - data = self.wf.readframes(self.chunk) - except Exception as e: - LOG.error(f"Error during AudioFile play {e}") - LOG.debug("Error during AudioFile play " \ - +' rate=' +str(self.wf.getframerate()) \ - + 'format=' +str(self.p.get_format_from_width(self.wf.getsampwidth())) \ - +' channels=' +str(self.wf.getnchannels()) \ - ) - - raise - - def close(self): - self.stream.close() - 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 deleted file mode 100644 index 0a6bca3..0000000 --- a/toxygen/notifications/tray.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy 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 tray and 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/ui/password_screen.py b/toxygen/passwordscreen.py similarity index 56% rename from toxygen/ui/password_screen.py rename to toxygen/passwordscreen.py index 57f7b95..dcd9d05 100644 --- a/toxygen/ui/password_screen.py +++ b/toxygen/passwordscreen.py @@ -1,33 +1,28 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from widgets import CenteredWidget, LineEdit +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui -import logging -from qtpy import QtCore, QtWidgets - -from ui.widgets import CenteredWidget, LineEdit, DialogWithResult -import utils.ui as util_ui - -global LOG -LOG = logging.getLogger('app.'+__name__) class PasswordArea(LineEdit): def __init__(self, parent): - super().__init__(parent) - self._parent = parent - self.setEchoMode(QtWidgets.QLineEdit.Password) + super(PasswordArea, self).__init__(parent) + self.parent = parent + self.setEchoMode(QtGui.QLineEdit.EchoMode.Password) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Return: - self._parent.button_click() + self.parent.button_click() else: - super().keyPressEvent(event) + super(PasswordArea, self).keyPressEvent(event) -class PasswordScreenBase(CenteredWidget, DialogWithResult): +class PasswordScreenBase(CenteredWidget): def __init__(self, encrypt): - CenteredWidget.__init__(self) - DialogWithResult.__init__(self) + super(PasswordScreenBase, self).__init__() self._encrypt = encrypt self.initUI() @@ -36,18 +31,18 @@ class PasswordScreenBase(CenteredWidget, DialogWithResult): self.setMinimumSize(QtCore.QSize(360, 170)) self.setMaximumSize(QtCore.QSize(360, 170)) - self.enter_pass = QtWidgets.QLabel(self) + self.enter_pass = QtGui.QLabel(self) self.enter_pass.setGeometry(QtCore.QRect(30, 10, 300, 30)) self.password = PasswordArea(self) self.password.setGeometry(QtCore.QRect(30, 50, 300, 30)) - self.button = QtWidgets.QPushButton(self) + self.button = QtGui.QPushButton(self) self.button.setGeometry(QtCore.QRect(30, 90, 300, 30)) - self.button.setText(util_ui.tr('OK')) + self.button.setText('OK') self.button.clicked.connect(self.button_click) - self.warning = QtWidgets.QLabel(self) + self.warning = QtGui.QLabel(self) self.warning.setGeometry(QtCore.QRect(30, 130, 300, 30)) self.warning.setStyleSheet('QLabel { color: #F70D1A; }') self.warning.setVisible(False) @@ -66,28 +61,28 @@ class PasswordScreenBase(CenteredWidget, DialogWithResult): super(PasswordScreenBase, self).keyPressEvent(event) def retranslateUi(self): - self.setWindowTitle(util_ui.tr('Enter password')) - self.enter_pass.setText(util_ui.tr('Password:')) - self.warning.setText(util_ui.tr('Incorrect password')) + self.setWindowTitle(QtGui.QApplication.translate("pass", "Enter password", None, QtGui.QApplication.UnicodeUTF8)) + self.enter_pass.setText(QtGui.QApplication.translate("pass", "Password:", None, QtGui.QApplication.UnicodeUTF8)) + self.warning.setText(QtGui.QApplication.translate("pass", "Incorrect password", None, QtGui.QApplication.UnicodeUTF8)) class PasswordScreen(PasswordScreenBase): def __init__(self, encrypt, data): - super().__init__(encrypt) + super(PasswordScreen, self).__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) + new_data = self._encrypt.pass_decrypt(self._data[0]) except Exception as ex: self.warning.setVisible(True) - LOG.error(f"Decryption error: {ex}") print('Decryption error:', ex) else: - self.close_with_result(new_data) + self._data[0] = new_data + self.close() class UnlockAppScreen(PasswordScreenBase): @@ -121,31 +116,37 @@ class SetProfilePasswordScreen(CenteredWidget): self.setMaximumSize(QtCore.QSize(700, 200)) self.password = LineEdit(self) self.password.setGeometry(QtCore.QRect(40, 10, 300, 30)) - self.password.setEchoMode(QtWidgets.QLineEdit.Password) + self.password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) self.confirm_password = LineEdit(self) self.confirm_password.setGeometry(QtCore.QRect(40, 50, 300, 30)) - self.confirm_password.setEchoMode(QtWidgets.QLineEdit.Password) - self.set_password = QtWidgets.QPushButton(self) + self.confirm_password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) + self.set_password = QtGui.QPushButton(self) self.set_password.setGeometry(QtCore.QRect(40, 100, 300, 30)) self.set_password.clicked.connect(self.new_password) - self.not_match = QtWidgets.QLabel(self) + self.not_match = QtGui.QLabel(self) self.not_match.setGeometry(QtCore.QRect(350, 50, 300, 30)) self.not_match.setVisible(False) self.not_match.setStyleSheet('QLabel { color: #BC1C1C; }') - self.warning = QtWidgets.QLabel(self) + self.warning = QtGui.QLabel(self) self.warning.setGeometry(QtCore.QRect(40, 160, 500, 30)) self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') def retranslateUi(self): - self.setWindowTitle(util_ui.tr('Profile password')) + self.setWindowTitle(QtGui.QApplication.translate("PasswordScreen", "Profile password", None, + QtGui.QApplication.UnicodeUTF8)) self.password.setPlaceholderText( - util_ui.tr('Password (at least 8 symbols)')) + QtGui.QApplication.translate("PasswordScreen", "Password (at least 8 symbols)", None, + QtGui.QApplication.UnicodeUTF8)) self.confirm_password.setPlaceholderText( - util_ui.tr('Confirm password')) + QtGui.QApplication.translate("PasswordScreen", "Confirm password", None, + QtGui.QApplication.UnicodeUTF8)) self.set_password.setText( - 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')) + QtGui.QApplication.translate("PasswordScreen", "Set password", None, QtGui.QApplication.UnicodeUTF8)) + self.not_match.setText(QtGui.QApplication.translate("PasswordScreen", "Passwords do not match", None, + QtGui.QApplication.UnicodeUTF8)) + self.warning.setText( + QtGui.QApplication.translate("PasswordScreen", "There is no way to recover lost passwords", None, + QtGui.QApplication.UnicodeUTF8)) def new_password(self): if self.password.text() == self.confirm_password.text(): @@ -153,8 +154,11 @@ class SetProfilePasswordScreen(CenteredWidget): self._encrypt.set_password(self.password.text()) self.close() else: - self.not_match.setText(util_ui.tr('Password must be at least 8 symbols')) + self.not_match.setText( + QtGui.QApplication.translate("PasswordScreen", "Password must be at least 8 symbols", None, + QtGui.QApplication.UnicodeUTF8)) self.not_match.setVisible(True) else: - self.not_match.setText(util_ui.tr('Passwords do not match')) + self.not_match.setText(QtGui.QApplication.translate("PasswordScreen", "Passwords do not match", None, + QtGui.QApplication.UnicodeUTF8)) self.not_match.setVisible(True) diff --git a/toxygen/plugin_support.py b/toxygen/plugin_support.py new file mode 100644 index 0000000..a4f5891 --- /dev/null +++ b/toxygen/plugin_support.py @@ -0,0 +1,167 @@ +import util +import profile +import os +import importlib +import inspect +import plugins.plugin_super_class as pl +import toxencryptsave +import sys + + +class PluginLoader(util.Singleton): + + def __init__(self, tox, settings): + super().__init__() + self._profile = profile.Profile.get_instance() + self._settings = settings + self._plugins = {} # dict. key - plugin unique short name, value - tuple (plugin instance, is active) + self._tox = tox + self._encr = toxencryptsave.ToxEncryptSave.get_instance() + + def set_tox(self, tox): + """ + New tox instance + """ + self._tox = tox + for value in self._plugins.values(): + value[0].set_tox(tox) + + def load(self): + """ + Load all plugins in plugins folder + """ + path = util.curr_directory() + '/plugins/' + if not os.path.exists(path): + util.log('Plugin dir not found') + return + else: + sys.path.append(path) + files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] + for fl in files: + if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): + continue + name = fl[:-3] # module name without .py + try: + module = importlib.import_module(name) # import plugin + except ImportError: + util.log('Import error in module ' + name) + continue + except Exception as ex: + util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + continue + for elem in dir(module): + obj = getattr(module, elem) + if inspect.isclass(obj) and hasattr(obj, 'is_plugin') and obj.is_plugin: # looking for plugin class in module + 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 + + def callback_lossless(self, friend_number, data, length): + """ + New incoming custom lossless packet (callback) + """ + 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:length]), friend_number) + + def callback_lossy(self, friend_number, data, length): + """ + New incoming custom lossy packet (callback) + """ + 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:length]), 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) + + def get_plugins_list(self): + """ + Returns list of all plugins + """ + result = [] + for data in self._plugins.values(): + 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 + return result + + def plugin_window(self, key): + """ + Return window or None for specified plugin + """ + return self._plugins[key][0].get_window() + + def toggle_plugin(self, key): + """ + Enable/disable plugin + :param key: plugin short name + """ + plugin = self._plugins[key] + if plugin[1]: + plugin[0].stop() + else: + plugin[0].start() + plugin[1] = not plugin[1] + if plugin[1]: + self._settings['plugins'].append(key) + else: + self._settings['plugins'].remove(key) + self._settings.save() + + def command(self, text): + """ + New command for plugin + """ + 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:]) + + def get_menu(self, menu, 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 + 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 + return result + + def stop(self): + """ + App is closing, stop all plugins + """ + for key in list(self._plugins.keys()): + if self._plugins[key][1]: + self._plugins[key][0].close() + del self._plugins[key] diff --git a/toxygen/plugin_support/__init__.py b/toxygen/plugin_support/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/plugin_support/plugin_support.py b/toxygen/plugin_support/plugin_support.py deleted file mode 100644 index f180e4d..0000000 --- a/toxygen/plugin_support/plugin_support.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import os -import importlib -import inspect -import sys -import logging - -import utils.util as util -import plugins.plugin_super_class as pl - -# LOG=util.log -global LOG -LOG = logging.getLogger('plugin_support') -def trace(msg, *args, **kwargs): LOG._log(0, msg, []) -LOG.trace = trace - -log = lambda x: LOG.info(x) - -class Plugin: - - 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._app = app - self._plugins = {} # dict. key - plugin unique short name, value - Plugin instance - - def set_tox(self, tox) -> None: - """ - New tox instance - """ - for plugin in self._plugins.values(): - plugin.instance.set_tox(tox) - - def load(self) -> None: - """ - Load all plugins in plugins folder - """ - path = util.get_plugins_directory() - if not os.path.exists(path): - self._app._LOG('WARN: Plugin directory not found: ' + path) - return - - sys.path.append(path) - files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] - for fl in files: - if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): - continue - base_name = fl[:-3] # module name without .py - try: - module = importlib.import_module(base_name) # import plugin - LOG.trace('Imported module: ' +base_name +' file: ' +fl) - except ImportError as e: - LOG.warn(f"Import error: {e}" +' file: ' +fl) - continue - except Exception as ex: - LOG.error('importing ' + base_name + ' Exception: ' + str(ex)) - continue - for elem in dir(module): - obj = getattr(module, elem) - # looking for plugin class in module - if not inspect.isclass(obj) or not hasattr(obj, 'is_plugin') or not obj.is_plugin: - continue - try: # create instance of plugin class - instance = obj(self._app) # name, short_name, app - # needed by bday... - instance._profile=self._app._ms._profile - instance._settings=self._settings - short_name = instance.get_short_name() - is_active = short_name in self._settings['plugins'] - if is_active: - try: - instance.start() - self._app._log('INFO: Started Plugin ' +short_name) - except Exception as e: - self._app._log.error(f"Starting Plugin ' +short_name +' {e}") - # else: LOG.info('Defined Plugin ' +short_name) - except Exception as ex: - LOG.error('in module ' + short_name + ' Exception: ' + str(ex)) - continue - short_name = instance.get_short_name() - self._plugins[short_name] = Plugin(instance, is_active) - LOG.info('Added plugin: ' +short_name +' from file: ' +fl) - break - - def callback_lossless(self, friend_number, data) -> None: - """ - New incoming custom lossless packet (callback) - """ - 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].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): - """ - New incoming custom lossy packet (callback) - """ - 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].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:int) -> None: - """ - Friend with specified number is online - """ - for plugin in self._plugins.values(): - if plugin.is_active: - plugin.instance.friend_connected(friend_number) - - def get_plugins_list(self) -> list: - """ - Returns list of all plugins - """ - result = [] - for plugin in self._plugins.values(): - try: - 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 - """ - try: - if key in self._plugins and hasattr(self._plugins[key], 'instance'): - return self._plugins[key].instance.get_window() - except Exception as e: - self._app._log('WARN: ' +key +' _plugins no slot instance: ' +str(e)) - - return None - - def toggle_plugin(self, key) -> None: - """ - Enable/disable plugin - :param key: plugin short name - """ - plugin = self._plugins[key] - if plugin.is_active: - plugin.instance.stop() - else: - 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) - self._settings.save() - - def command(self, text) -> None: - """ - New command for plugin - """ - text = text.strip() - name = text.split()[0] - if name in self._plugins and self._plugins[name].is_active: - self._plugins[name].instance.command(text[len(name) + 1:]) - - def get_menu(self, num): - """ - Return list of items for menu - """ - result = [] - 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 plugin in self._plugins.values(): - if not plugin.is_active: - continue - if not hasattr(plugin.instance, 'get_message_menu'): - name = plugin.instance.get_short_name() - self._app._log('WARN: get_message_menu not found: ' + name) - continue - try: - result.extend(plugin.instance.get_message_menu(menu, selected_text)) - except: - pass - return result - - def stop(self) -> None: - """ - App is closing, stop all plugins - """ - for key in list(self._plugins.keys()): - if self._plugins[key].is_active: - self._plugins[key].instance.close() - del self._plugins[key] - - def reload(self) -> None: - path = util.get_plugins_directory() - if not os.path.exists(path): - self._app._log('WARN: Plugin directory not found: ' + path) - return - - self.stop() - self._app._log('INFO: Reloading plugins from ' +path) - self.load() diff --git a/toxygen/plugins/README.md b/toxygen/plugins/README.md deleted file mode 100644 index 12ed7b0..0000000 --- a/toxygen/plugins/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Plugins - -Repo with plugins for [Toxygen](https://macaw.me/emdee/toxygen/) - -For more info visit [plugins.md](https://macaw.me/emdee/toxygen/blob/master/docs/plugins.md) and [plugin_api.md](https://github.com/toxygen-project[/toxygen/blob/master/docs/plugin-api.md) - -# Plugins list: - -- ToxId - share your Tox ID and copy friend's Tox ID easily. -- MarqueeStatus - create ticker from your status message. -- BirthDay - get notifications on your friends' birthdays. -- Bot - bot which can communicate with your friends when you are away. -- SearchPlugin - select text in message and find it in search engine. -- AutoAwayStatusLinux - sets "Away" status when user is inactive (Linux only). -- AutoAwayStatusWindows - sets "Away" status when user is inactive (Windows only). -- Chess - play chess with your friends using Tox. -- Garland - changes your status like it's garland. -- AutoAnswer - calls auto answering. -- uToxInlineSending - send inlines with the same name as uTox does. -- AvatarEncryption - encrypt all avatars using profile password - -## Hard fork - -Not all of these are working... - -Work on this project is suspended until the -[MultiDevice](https://git.plastiras.org/emdee/tox_profile/wiki/MultiDevice-Announcements-POC) problem is solved. Fork me! diff --git a/toxygen/plugins/ae.py b/toxygen/plugins/ae.py deleted file mode 100644 index b30ea66..0000000 --- a/toxygen/plugins/ae.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import json -import os -from qtpy import QtWidgets - -from bootstrap.bootstrap import get_user_config_path -from user_data import settings -import plugin_super_class - -class AvatarEncryption(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super(AvatarEncryption, self).__init__('AvatarEncryption', 'ae', *args) - self._path = os.path.join(get_user_config_path(), 'avatars') - self._app = args[0] - self._profile = self._app._ms._profile - self._window = None - #was self._contacts = self._profile._contacts[:] - self._contacts = self._profile._contacts_provider.get_all_friends() - - def get_description(self): - return QtWidgets.QApplication.translate("AvatarEncryption", 'Encrypt all avatars using profile password.') - - def close(self): - if not self._encrypt_save.has_password(): - return - i, data = 1, {} - - self.save_contact_avatar(data, self._profile, 0) - for friend in self._contacts: - self.save_contact_avatar(data, friend, i) - i += 1 - self.save_settings(json.dumps(data)) - - def start(self): - if not self._encrypt_save.has_password(): - return - data = json.loads(self.load_settings()) - - self.load_contact_avatar(data, self._profile) - for friend in self._contacts: - self.load_contact_avatar(data, friend) - self._profile.update() - - def save_contact_avatar(self, data, contact, i): - tox_id = contact.tox_id[:64] - data[str(tox_id)] = str(i) - path = os.path.join(self._path, tox_id + '.png') - if os.path.isfile(path): - with open(path, 'rb') as fl: - avatar = fl.read() - encr_avatar = self._encrypt_save.pass_encrypt(avatar) - with open(os.path.join(self._path, self._settings.name + '_' + str(i) + '.png'), 'wb') as fl: - fl.write(encr_avatar) - os.remove(path) - - def load_contact_avatar(self, data, contact): - tox_id = str(contact.tox_id[:64]) - if tox_id not in data: - return - path = os.path.join(self._path, self._settings.name + '_' + data[tox_id] + '.png') - if os.path.isfile(path): - with open(path, 'rb') as fl: - avatar = fl.read() - decr_avatar = self._encrypt_save.pass_decrypt(avatar) - with open(os.path.join(self._path, str(tox_id) + '.png'), 'wb') as fl: - fl.write(decr_avatar) - os.remove(path) - contact.load_avatar() - - def load_settings(self): - try: - with open(plugin_super_class.path_to_data(self._short_name) + self._settings.name + '.json', 'rb') as fl: - data = fl.read() - return str(self._encrypt_save.pass_decrypt(data), 'utf-8') if data else '{}' - except: - return '{}' - - def save_settings(self, data): - try: - data = self._encrypt_save.pass_encrypt(bytes(data, 'utf-8')) - with open(plugin_super_class.path_to_data(self._short_name) + self._settings.name + '.json', 'wb') as fl: - fl.write(data) - except: - pass diff --git a/toxygen/plugins/awayl.py b/toxygen/plugins/awayl.py deleted file mode 100644 index 9b63720..0000000 --- a/toxygen/plugins/awayl.py +++ /dev/null @@ -1,114 +0,0 @@ -import plugin_super_class -import threading -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import json -from subprocess import check_output -import time - -from qtpy import QtCore, QtWidgets - - -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 AutoAwayStatusLinux(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super().__init__('AutoAwayStatusLinux', 'awayl', *args) - self._thread = None - self._exec = None - self._active = False - self._time = json.loads(self.load_settings())['time'] - self._prev_status = 0 - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def get_description(self): - return QApplication.translate("AutoAwayStatusLinux", 'sets "Away" status when user is inactive (Linux only).') - - def close(self): - self.stop() - - def stop(self): - self._exec = False - if self._active: - self._thread.join() - - def start(self): - self._exec = True - self._thread = threading.Thread(target=self.loop) - self._thread.start() - - def save(self): - self.save_settings('{"time": ' + str(self._time) + '}') - - def change_status(self, status=1): - if self._profile.status in (0, 2): - self._prev_status = self._profile.status - if status is not None: - invoke_in_main_thread(self._profile.set_status, status) - - def get_window(self): - inst = self - - class Window(QtWidgets.QWidget): - def __init__(self): - super(Window, self).__init__() - self.setGeometry(QtCore.QRect(450, 300, 350, 100)) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(20, 0, 310, 35)) - self.label.setText(QtWidgets.QApplication.translate("AutoAwayStatusLinux", "Auto away time in minutes\n(0 - to disable)")) - self.time = QtWidgets.QLineEdit(self) - self.time.setGeometry(QtCore.QRect(20, 40, 310, 25)) - self.time.setText(str(inst._time)) - self.setWindowTitle("AutoAwayStatusLinux") - self.ok = QtWidgets.QPushButton(self) - self.ok.setGeometry(QtCore.QRect(20, 70, 310, 25)) - self.ok.setText( - QtWidgets.QApplication.translate("AutoAwayStatusLinux", "Save")) - self.ok.clicked.connect(self.update) - - def update(self): - try: - t = int(self.time.text()) - except: - t = 0 - inst._time = t - inst.save() - self.close() - - return Window() - - def loop(self): - self._active = True - while self._exec: - time.sleep(5) - d = check_output(['xprintidle']) - d = int(d) // 1000 - if self._time: - if d > 60 * self._time: - self.change_status() - elif self._profile.status == 1: - self.change_status(self._prev_status) diff --git a/toxygen/plugins/awayw.py.windows b/toxygen/plugins/awayw.py.windows deleted file mode 100644 index 5c4b768..0000000 --- a/toxygen/plugins/awayw.py.windows +++ /dev/null @@ -1,115 +0,0 @@ -import plugin_super_class -import threading -import time -from PyQt5 import QtCore, QtWidgets -from ctypes import Structure, windll, c_uint, sizeof, byref -import json - - -class LASTINPUTINFO(Structure): - _fields_ = [('cbSize', c_uint), ('dwTime', c_uint)] - - -def get_idle_duration(): - lastInputInfo = LASTINPUTINFO() - lastInputInfo.cbSize = sizeof(lastInputInfo) - windll.user32.GetLastInputInfo(byref(lastInputInfo)) - millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime - return millis / 1000.0 - - -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 AutoAwayStatusWindows(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super().__init__('AutoAwayStatusWindows', 'awayw', *args) - self._thread = None - self._exec = None - self._active = False - self._time = json.loads(self.load_settings())['time'] - self._prev_status = 0 - - def close(self): - self.stop() - - def stop(self): - self._exec = False - if self._active: - self._thread.join() - - def start(self): - self._exec = True - self._thread = threading.Thread(target=self.loop) - self._thread.start() - - def save(self): - self.save_settings('{"time": ' + str(self._time) + '}') - - def change_status(self, status=1): - if self._profile.status != 1: - self._prev_status = self._profile.status - invoke_in_main_thread(self._profile.set_status, status) - - def get_window(self): - inst = self - - class Window(QtWidgets.QWidget): - def __init__(self): - super(Window, self).__init__() - self.setGeometry(QtCore.QRect(450, 300, 350, 100)) - self.label = QtWidgets.QLabel(self) - self.label.setGeometry(QtCore.QRect(20, 0, 310, 35)) - self.label.setText(QtWidgets.QApplication.translate("AutoAwayStatusWindows", "Auto away time in minutes\n(0 - to disable)")) - self.time = QtWidgets.QLineEdit(self) - self.time.setGeometry(QtCore.QRect(20, 40, 310, 25)) - self.time.setText(str(inst._time)) - self.setWindowTitle("AutoAwayStatusWindows") - self.ok = QtWidgets.QPushButton(self) - self.ok.setGeometry(QtCore.QRect(20, 70, 310, 25)) - self.ok.setText( - QtWidgets.QApplication.translate("AutoAwayStatusWindows", "Save")) - self.ok.clicked.connect(self.update) - - def update(self): - try: - t = int(self.time.text()) - except: - t = 0 - inst._time = t - inst.save() - self.close() - - return Window() - - def loop(self): - self._active = True - while self._exec: - time.sleep(5) - d = get_idle_duration() - if self._time: - if d > 60 * self._time: - self.change_status() - elif self._profile.status == 1: - self.change_status(self._prev_status) diff --git a/toxygen/plugins/bday.pro b/toxygen/plugins/bday.pro deleted file mode 100644 index 7393e95..0000000 --- a/toxygen/plugins/bday.pro +++ /dev/null @@ -1,2 +0,0 @@ -SOURCES = bday.py -TRANSLATIONS = bday/en_GB.ts bday/en_US.ts bday/ru_RU.ts diff --git a/toxygen/plugins/bday.py b/toxygen/plugins/bday.py deleted file mode 100644 index 8563638..0000000 --- a/toxygen/plugins/bday.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import json -import importlib - -from qtpy import QtWidgets, QtCore - -import plugin_super_class - -class BirthDay(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - # Constructor. In plugin __init__ should take only 1 last argument - super(BirthDay, self).__init__('BirthDay', 'bday', *args) - self._data = json.loads(self.load_settings()) - self._datetime = importlib.import_module('datetime') - self._timers = [] - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def start(self) -> None: - now = self._datetime.datetime.now() - today = {} - x = self._profile.tox_id[:64] - for key in self._data: - if key != x and key != 'send_date': - arr = self._data[key].split('.') - if int(arr[0]) == now.day and int(arr[1]) == now.month: - today[key] = now.year - int(arr[2]) - if len(today): - msgbox = QtWidgets.QMessageBox() - title = QtWidgets.QApplication.translate('BirthDay', "Birthday!") - msgbox.setWindowTitle(title) - text = ', '.join(self._profile.get_friend_by_number(self._tox.friend_by_public_key(x)).name + ' ({})'.format(today[x]) for x in today) - msgbox.setText('Birthdays: ' + text) - msgbox.exec_() - - def get_description(self): - return QtWidgets.QApplication.translate("BirthDay", "Send and get notifications on your friends' birthdays.") - - def get_window(self) -> None: - inst = self - x = self._profile.tox_id[:64] - - class Window(QtWidgets.QWidget): - - def __init__(self): - super(Window, self).__init__() - self.setGeometry(QtCore.QRect(450, 300, 350, 150)) - self.send = QtWidgets.QCheckBox(self) - self.send.setGeometry(QtCore.QRect(20, 10, 310, 25)) - self.send.setText(QtWidgets.QApplication.translate('BirthDay', "Send my birthday date to contacts")) - self.setWindowTitle(QtWidgets.QApplication.translate('BirthDay', "Birthday")) - self.send.clicked.connect(self.update) - self.send.setChecked(inst._data['send_date']) - self.date = QtWidgets.QLineEdit(self) - self.date.setGeometry(QtCore.QRect(20, 50, 310, 25)) - self.date.setPlaceholderText(QtWidgets.QApplication.translate('BirthDay', "Date in format dd.mm.yyyy")) - self.set_date = QtWidgets.QPushButton(self) - self.set_date.setGeometry(QtCore.QRect(20, 90, 310, 25)) - self.set_date.setText(QtWidgets.QApplication.translate('BirthDay', "Save date")) - self.set_date.clicked.connect(self.save_curr_date) - self.date.setText(inst._data[x] if x in inst._data else '') - - def save_curr_date(self): - inst._data[x] = self.date.text() - inst.save_settings(json.dumps(inst._data)) - self.close() - - def update(self): - inst._data['send_date'] = self.send.isChecked() - inst.save_settings(json.dumps(inst._data)) - - if not hasattr(self, '_window') or not self._window: - self._window = Window() - return self._window - - def lossless_packet(self, data, friend_number) -> None: - if len(data): - friend = self._profile.get_friend_by_number(friend_number) - self._data[friend.tox_id] = data - self.save_settings(json.dumps(self._data)) - elif self._data['send_date'] and self._profile.tox_id[:64] in self._data: - self.send_lossless(self._data[self._profile.tox_id[:64]], friend_number) - - def friend_connected(self, friend_number:int) -> None: - timer = QtCore.QTimer() - timer.timeout.connect(lambda: self.timer(friend_number)) - timer.start(10000) - self._timers.append(timer) - - def timer(self, friend_number:int) -> None: - timer = self._timers.pop() - timer.stop() - if self._profile.get_friend_by_number(friend_number).tox_id not in self._data: - self.send_lossless('', friend_number) - diff --git a/toxygen/plugins/bot.py b/toxygen/plugins/bot.py deleted file mode 100644 index 71db5a0..0000000 --- a/toxygen/plugins/bot.py +++ /dev/null @@ -1,83 +0,0 @@ -import time - -from qtpy import QtCore, QtWidgets - -import plugin_super_class - - -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 Bot(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super(Bot, self).__init__('Bot', 'bot', *args) - self._callback = None - self._mode = 0 - self._message = "I'm away, will back soon" - self._timer = QtCore.QTimer() - self._timer.timeout.connect(self.initialize) - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def get_description(self): - return QtWidgets.QApplication.translate("Bot", 'Plugin to answer bot to your friends.') - - def start(self): - self._timer.start(10000) - - def command(self, command): - if command.startswith('mode '): - self._mode = int(command.split(' ')[-1]) - elif command.startswith('message '): - self._message = command[8:] - else: - super().command(command) - - def initialize(self): - self._timer.stop() - self._callback = self._tox.friend_message_cb - - def incoming_message(tox, friend_number, message_type, message, size, user_data): - self._callback(tox, friend_number, message_type, message, size, user_data) - if self._profile.status == 1: # TOX_USER_STATUS['AWAY'] - self.answer(friend_number, str(message, 'utf-8')) - - self._tox.callback_friend_message(incoming_message) # , None - - def stop(self): - if not self._callback: return - try: - # TypeError: argument must be callable or integer function address - self._tox.callback_friend_message(self._callback) # , None - except: pass - - def close(self): - self.stop() - - def answer(self, friend_number, message): - if not self._mode: - message = self._message - invoke_in_main_thread(self._profile.send_message, message, friend_number) - diff --git a/toxygen/plugins/chess.py b/toxygen/plugins/chess.py deleted file mode 100644 index f5c6feb..0000000 --- a/toxygen/plugins/chess.py +++ /dev/null @@ -1,1696 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import collections -import re -import math - -from qtpy import QtWidgets -from qtpy.QtCore import * -from qtpy.QtWidgets import * -from qtpy.QtGui import * -from qtpy.QtSvg import * - -import plugin_super_class - -START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" - - -def opposite_color(color): - """:return: The opposite color. - - :param color: - "w", "white, "b" or "black". - """ - if color == "w": - return "b" - elif color == "white": - return "black" - elif color == "b": - return "w" - elif color == "black": - return "white" - else: - raise ValueError("Expected w, b, white or black, got: %s." % color) - - -class Piece(object): - - __cache = dict() - - def __init__(self, symbol): - self.__symbol = symbol - - self.__color = "w" if symbol != symbol.lower() else "b" - self.__full_color = "white" if self.__color == "w" else "black" - - self.__type = symbol.lower() - if self.__type == "p": - self.__full_type = "pawn" - elif self.__type == "n": - self.__full_type = "knight" - elif self.__type == "b": - self.__full_type = "bishop" - elif self.__type == "r": - self.__full_type = "rook" - elif self.__type == "q": - self.__full_type = "queen" - elif self.__type == "k": - self.__full_type = "king" - else: - raise ValueError("Expected valid piece symbol, got: %s." % symbol) - - self.__hash = ord(self.__symbol) - - @classmethod - def from_color_and_type(cls, color, type): - """Creates a piece object from color and type. - """ - if type == "p" or type == "pawn": - symbol = "p" - elif type == "n" or type == "knight": - symbol = "n" - elif type == "b" or type == "bishop": - symbol = "b" - elif type == "r" or type == "rook": - symbol = "r" - elif type == "q" or type == "queen": - symbol = "q" - elif type == "k" or type == "king": - symbol = "k" - else: - raise ValueError("Expected piece type, got: %s." % type) - - if color == "w" or color == "white": - return cls(symbol.upper()) - elif color == "b" or color == "black": - return cls(symbol) - else: - raise ValueError("Expected w, b, white or black, got: %s." % color) - - @property - def symbol(self): - return self.__symbol - - @property - def color(self): - """The color of the piece as `"b"` or `"w"`.""" - return self.__color - - @property - def full_color(self): - """The full color of the piece as `"black"` or `"white`.""" - return self.__full_color - - @property - def type(self): - """The type of the piece as `"p"`, `"b"`, `"n"`, `"r"`, `"k"`, - or `"q"` for pawn, bishop, knight, rook, king or queen. - """ - return self.__type - - @property - def full_type(self): - """The full type of the piece as `"pawn"`, `"bishop"`, - `"knight"`, `"rook"`, `"king"` or `"queen"`. - """ - return self.__full_type - - def __str__(self): - return self.__symbol - - def __repr__(self): - return "Piece('%s')" % self.__symbol - - def __eq__(self, other): - return isinstance(other, Piece) and self.__symbol == other.symbol - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return self.__hash - - -class Square(object): - """Represents a square on the chess board. - - :param name: The name of the square in algebraic notation. - - Square objects that represent the same square compare as equal. - """ - - __cache = dict() - - def __init__(self, name): - if not len(name) == 2: - raise ValueError("Expected square name, got: %s." % repr(name)) - self.__name = name - - if not name[0] in ["a", "b", "c", "d", "e", "f", "g", "h"]: - raise ValueError("Expected file, got: %s." % repr(name[0])) - self.__file = name[0] - self.__x = ord(self.__name[0]) - ord("a") - - if not name[1] in ["1", "2", "3", "4", "5", "6", "7", "8"]: - raise ValueError("Expected rank, got: %s." % repr(name[1])) - self.__rank = int(name[1]) - self.__y = ord(self.__name[1]) - ord("1") - - self.__x88 = self.__x + 16 * (7 - self.__y) - - @classmethod - def from_x88(cls, x88): - """Creates a square object from an `x88 `_ - index. - - :param x88: - The x88 index as integer between 0 and 128. - """ - if x88 < 0 or x88 > 128: - raise ValueError("x88 index is out of range: %s." % repr(x88)) - - if x88 & 0x88: - raise ValueError("x88 is not on the board: %s." % repr(x88)) - - return cls("abcdefgh"[x88 & 7] + "87654321"[x88 >> 4]) - - @classmethod - def from_rank_and_file(cls, rank, file): - """Creates a square object from rank and file. - - :param rank: - An integer between 1 and 8. - :param file: - The rank as a letter between `"a"` and `"h"`. - """ - if rank < 1 or rank > 8: - raise ValueError("Expected rank to be between 1 and 8: %s." % repr(rank)) - - if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]: - raise ValueError("Expected the file to be a letter between 'a' and 'h': %s." % repr(file)) - - return cls(file + str(rank)) - - @classmethod - def from_x_and_y(cls, x, y): - """Creates a square object from x and y coordinates. - - :param x: - An integer between 0 and 7 where 0 is the a-file. - :param y: - An integer between 0 and 7 where 0 is the first rank. - """ - return cls("abcdefgh"[x] + "12345678"[y]) - - @property - def name(self): - """The algebraic name of the square.""" - return self.__name - - @property - def file(self): - """The file as a letter between `"a"` and `"h"`.""" - return self.__file - - @property - def x(self): - """The x-coordinate, starting with 0 for the a-file.""" - return self.__x - - @property - def rank(self): - """The rank as an integer between 1 and 8.""" - return self.__rank - - @property - def y(self): - """The y-coordinate, starting with 0 for the first rank.""" - return self.__y - - @property - def x88(self): - """The `x88 `_ - index of the square.""" - return self.__x88 - - def is_dark(self): - """:return: Whether it is a dark square.""" - return (self.__x - self.__y % 2) == 0 - - def is_light(self): - """:return: Whether it is a light square.""" - return not self.is_dark() - - def is_backrank(self): - """:return: Whether the square is on either sides backrank.""" - return self.__y == 0 or self.__y == 7 - - def __str__(self): - return self.__name - - def __repr__(self): - return "Square('%s')" % self.__name - - def __eq__(self, other): - return isinstance(other, Square) and self.__name == other.name - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return self.__x88 - - -class Move(object): - """Represents a move. - """ - - __uci_move_regex = re.compile(r"^([a-h][1-8])([a-h][1-8])([rnbq]?)$") - - def __init__(self, source, target, promotion=None): - if not isinstance(source, Square): - raise TypeError("Expected source to be a Square.") - self.__source = source - - if not isinstance(target, Square): - raise TypeError("Expected target to be a Square.") - self.__target = target - - if not promotion: - self.__promotion = None - self.__full_promotion = None - else: - promotion = promotion.lower() - if promotion == "n" or promotion == "knight": - self.__promotion = "n" - self.__full_promotion = "knight" - elif promotion == "b" or promotion == "bishop": - self.__promotion = "b" - self.__full_promotion = "bishop" - elif promotion == "r" or promotion == "rook": - self.__promotion = "r" - self.__full_promotion = "rook" - elif promotion == "q" or promotion == "queen": - self.__promotion = "q" - self.__full_promotion = "queen" - else: - raise ValueError("Expected promotion type, got: %s." % repr(promotion)) - - @classmethod - def from_uci(cls, uci): - """The UCI move string like `"a1a2"` or `"b7b8q"`.""" - if uci == "0000": - return cls.get_null() - - match = cls.__uci_move_regex.match(uci) - - return cls( - source=Square(match.group(1)), - target=Square(match.group(2)), - promotion=match.group(3) or None) - - @classmethod - def get_null(cls): - """:return: A null move.""" - return cls(Square("a1"), Square("a1")) - - @property - def source(self): - """The source square.""" - return self.__source - - @property - def target(self): - """The target square.""" - return self.__target - - @property - def promotion(self): - """The promotion type as `None`, `"r"`, `"n"`, `"b"` or `"q"`.""" - return self.__promotion - - @property - def full_promotion(self): - """Like `promotion`, but with full piece type names.""" - return self.__full_promotion - - @property - def uci(self): - """The UCI move string like `"a1a2"` or `"b7b8q"`.""" - if self.is_null(): - return "0000" - else: - if self.__promotion: - return self.__source.name + self.__target.name + self.__promotion - else: - return self.__source.name + self.__target.name - - def is_null(self): - """:return: Whether the move is a null move.""" - return self.__source == self.__target - - def __nonzero__(self): - return not self.is_null() - - def __str__(self): - return self.uci - - def __repr__(self): - return "Move.from_uci(%s)" % repr(self.uci) - - def __eq__(self, other): - return isinstance(other, Move) and self.uci == other.uci - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.uci) - - -MoveInfo = collections.namedtuple("MoveInfo", [ - "move", - "piece", - "captured", - "san", - "is_enpassant", - "is_king_side_castle", - "is_queen_side_castle", - "is_castle", - "is_check", - "is_checkmate"]) - - -class Position(object): - """Represents a chess position. - - :param fen: - Optional. The FEN of the position. Defaults to the standard - chess start position. - """ - - __san_regex = re.compile('^([NBKRQ])?([a-h])?([1-8])?x?([a-h][1-8])(=[NBRQ])?(\+|#)?$') - - def __init__(self, fen=START_FEN): - self.__castling = "KQkq" - self.fen = fen - - def copy(self): - """Gets a copy of the position. The copy will not change when the - original instance is changed. - - :return: - An exact copy of the positon. - """ - return Position(self.fen) - - def __get_square_index(self, square_or_int): - if type(square_or_int) is int: - # Validate the index by passing it through the constructor. - return Square.from_x88(square_or_int).x88 - elif isinstance(square_or_int, str): - return Square(square_or_int).x88 - elif type(square_or_int) is Square: - return square_or_int.x88 - else: - raise TypeError( - "Expected integer or Square, got: %s." % repr(square_or_int)) - - def __getitem__(self, key): - return self.__board[self.__get_square_index(key)] - - def __setitem__(self, key, value): - if value is None or type(value) is Piece: - self.__board[self.__get_square_index(key)] = value - else: - raise TypeError("Expected Piece or None, got: %s." % repr(value)) - - def __delitem__(self, key): - self.__board[self.__get_square_index(key)] = None - - def clear_board(self): - """Removes all pieces from the board.""" - self.__board = [None] * 128 - - def reset(self): - """Resets to the standard chess start position.""" - self.set_fen(START_FEN) - - def __get_disambiguator(self, move): - same_rank = False - same_file = False - piece = self[move.source] - - for m in self.get_legal_moves(): - ambig_piece = self[m.source] - if (piece == ambig_piece and move.source != m.source and - move.target == m.target): - if move.source.rank == m.source.rank: - same_rank = True - - if move.source.file == m.source.file: - same_file = True - - if same_rank and same_file: - break - - if same_rank and same_file: - return move.source.name - elif same_file: - return str(move.source.rank) - elif same_rank: - return move.source.file - else: - return "" - - def get_move_from_san(self, san): - """Gets a move from standard algebraic notation. - - :param san: - A move string in standard algebraic notation. - - :return: - A Move object. - - :raise Exception: - If not exactly one legal move matches. - """ - # Castling moves. - if san == "O-O" or san == "O-O-O": - rank = 1 if self.turn == "w" else 8 - if san == "O-O": - return Move( - source=Square.from_rank_and_file(rank, 'e'), - target=Square.from_rank_and_file(rank, 'g')) - else: - return Move( - source=Square.from_rank_and_file(rank, 'e'), - target=Square.from_rank_and_file(rank, 'c')) - # Regular moves. - else: - matches = Position.__san_regex.match(san) - if not matches: - raise ValueError("Invalid SAN: %s." % repr(san)) - - piece = Piece.from_color_and_type( - color=self.turn, - type=matches.group(1).lower() if matches.group(1) else 'p') - target = Square(matches.group(4)) - - source = None - for m in self.get_legal_moves(): - if self[m.source] != piece or m.target != target: - continue - - if matches.group(2) and matches.group(2) != m.source.file: - continue - if matches.group(3) and matches.group(3) != str(m.source.rank): - continue - - # Move matches. Assert it is not ambiguous. - if source: - raise Exception( - "Move is ambiguous: %s matches %s and %s." - % san, source, m) - source = m.source - - if not source: - raise Exception("No legal move matches %s." % san) - - return Move(source, target, matches.group(5) or None) - - def get_move_info(self, move): - """Gets information about a move. - - :param move: - The move to get information about. - - :return: - A named tuple with these properties: - - `move`: - The move object. - `piece`: - The piece that has been moved. - `san`: - The standard algebraic notation of the move. - `captured`: - The piece that has been captured or `None`. - `is_enpassant`: - A boolean indicating if the move is an en-passant - capture. - `is_king_side_castle`: - Whether it is a king-side castling move. - `is_queen_side_castle`: - Whether it is a queen-side castling move. - `is_castle`: - Whether it is a castling move. - `is_check`: - Whether the move gives check. - `is_checkmate`: - Whether the move gives checkmate. - - :raise Exception: - If the move is not legal in the position. - """ - resulting_position = self.copy().make_move(move) - - capture = self[move.target] - piece = self[move.source] - - # Pawn moves. - enpassant = False - if piece.type == "p": - # En-passant. - if move.target.file != move.source.file and not capture: - enpassant = True - capture = Piece.from_color_and_type( - color=resulting_position.turn, type='p') - - # Castling. - if piece.type == "k": - is_king_side_castle = move.target.x - move.source.x == 2 - is_queen_side_castle = move.target.x - move.source.x == -2 - else: - is_king_side_castle = is_queen_side_castle = False - - # Checks. - is_check = resulting_position.is_check() - is_checkmate = resulting_position.is_checkmate() - - # Generate the SAN. - san = "" - if is_king_side_castle: - san += "o-o" - elif is_queen_side_castle: - san += "o-o-o" - else: - if piece.type != 'p': - san += piece.type.upper() - - san += self.__get_disambiguator(move) - - if capture: - if piece.type == 'p': - san += move.source.file - san += "x" - san += move.target.name - - if move.promotion: - san += "=" - san += move.promotion.upper() - - if is_checkmate: - san += "#" - elif is_check: - san += "+" - - if enpassant: - san += " (e.p.)" - - # Return the named tuple. - return MoveInfo( - move=move, - piece=piece, - captured=capture, - san=san, - is_enpassant=enpassant, - is_king_side_castle=is_king_side_castle, - is_queen_side_castle=is_queen_side_castle, - is_castle=is_king_side_castle or is_queen_side_castle, - is_check=is_check, - is_checkmate=is_checkmate) - - def make_move(self, move, validate=True): - """Makes a move. - - :param move: - The move to make. - :param validate: - Defaults to `True`. Whether the move should be validated. - - :return: - Making a move changes the position object. The same - (changed) object is returned for chainability. - - :raise Exception: - If the validate parameter is `True` and the move is not - legal in the position. - """ - if validate and move not in self.get_legal_moves(): - raise Exception( - "%s is not a legal move in the position %s." % (move, self.fen)) - piece = self[move.source] - capture = self[move.target] - - # Move the piece. - self[move.target] = self[move.source] - del self[move.source] - - # It is the next players turn. - self.toggle_turn() - - # Pawn moves. - self.ep_file = None - if piece.type == "p": - # En-passant. - if move.target.file != move.source.file and not capture: - if self.turn == "w": - self[move.target.x88 - 16] = None - else: - self[move.target.x88 + 16] = None - capture = True - # If big pawn move, set the en-passant file. - if abs(move.target.rank - move.source.rank) == 2: - if self.get_theoretical_ep_right(move.target.file): - self.ep_file = move.target.file - - # Promotion. - if move.promotion: - self[move.target] = Piece.from_color_and_type( - color=piece.color, type=move.promotion) - - # Potential castling. - if piece.type == "k": - steps = move.target.x - move.source.x - if abs(steps) == 2: - # Queen-side castling. - if steps == -2: - rook_target = move.target.x88 + 1 - rook_source = move.target.x88 - 2 - # King-side castling. - else: - rook_target = move.target.x88 - 1 - rook_source = move.target.x88 + 1 - self[rook_target] = self[rook_source] - del self[rook_source] - - # Increment the half move counter. - if piece.type == "p" or capture: - self.half_moves = 0 - else: - self.half_moves += 1 - - # Increment the move number. - if self.turn == "w": - self.ply += 1 - - # Update castling rights. - for type in ["K", "Q", "k", "q"]: - if not self.get_theoretical_castling_right(type): - self.set_castling_right(type, False) - - return self - - @property - def turn(self): - """Whos turn it is as `"w"` or `"b"`.""" - return self.__turn - - @turn.setter - def turn(self, value): - if value not in ["w", "b"]: - raise ValueError( - "Expected 'w' or 'b' for turn, got: %s." % repr(value)) - self.__turn = value - - def toggle_turn(self): - """Toggles whos turn it is.""" - self.turn = opposite_color(self.turn) - - def get_castling_right(self, type): - """Checks the castling rights. - - :param type: - The castling move to check. "K" for king-side castling of - the white player, "Q" for queen-side castling of the white - player. "k" and "q" for the corresponding castling moves of - the black player. - - :return: - A boolean indicating whether the player has that castling - right. - """ - if not type in ["K", "Q", "k", "q"]: - raise KeyError( - "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." % repr(type)) - return type in self.__castling - - def get_theoretical_castling_right(self, type): - """Checks if a player could have a castling right in theory from - looking just at the piece positions. - - :param type: - The castling move to check. See - `Position.get_castling_right(type)` for values. - - :return: - A boolean indicating whether the player could theoretically - have that castling right. - """ - if not type in ["K", "Q", "k", "q"]: - raise KeyError( - "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." - % repr(type)) - if type == "K" or type == "Q": - if self["e1"] != Piece("K"): - return False - if type == "K": - return self["h1"] == Piece("R") - elif type == "Q": - return self["a1"] == Piece("R") - elif type == "k" or type == "q": - if self["e8"] != Piece("k"): - return False - if type == "k": - return self["h8"] == Piece("r") - elif type == "q": - return self["a8"] == Piece("r") - - def get_theoretical_ep_right(self, file): - """Checks if a player could have an ep-move in theory from - looking just at the piece positions. - - :param file: - The file to check as a letter between `"a"` and `"h"`. - - :return: - A boolean indicating whether the player could theoretically - have that en-passant move. - """ - if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]: - raise KeyError( - "Expected a letter between 'a' and 'h' for the file, got: %s." - % repr(file)) - - # Check there is a pawn. - pawn_square = Square.from_rank_and_file( - rank=4 if self.turn == "b" else 5, file=file) - opposite_color_pawn = Piece.from_color_and_type( - color=opposite_color(self.turn), type="p") - if self[pawn_square] != opposite_color_pawn: - return False - - # Check the square below is empty. - square_below = Square.from_rank_and_file( - rank=3 if self.turn == "b" else 6, file=file) - if self[square_below]: - return False - - # Check there is a pawn of the other color on a neighbor file. - f = ord(file) - ord("a") - p = Piece("p") - P = Piece("P") - if self.turn == "b": - if f > 0 and self[Square.from_x_and_y(f - 1, 3)] == p: - return True - elif f < 7 and self[Square.from_x_and_y(f + 1, 3)] == p: - return True - else: - if f > 0 and self[Square.from_x_and_y(f - 1, 4)] == P: - return True - elif f < 7 and self[Square.from_x_and_y(f + 1, 4)] == P: - return True - return False - - def set_castling_right(self, type, status): - """Sets a castling right. - - :param type: - `"K"`, `"Q"`, `"k"`, or `"q"` as used by - `Position.get_castling_right(type)`. - :param status: - A boolean indicating whether that castling right should be - granted or denied. - """ - if not type in ["K", "Q", "k", "q"]: - raise KeyError( - "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." - % repr(type)) - - castling = "" - for t in ["K", "Q", "k", "q"]: - if type == t: - if status: - castling += t - elif self.get_castling_right(t): - castling += t - self.__castling = castling - - @property - def ep_file(self): - """The en-passant file as a lowercase letter between `"a"` and - `"h"` or `None`.""" - return self.__ep_file - - @ep_file.setter - def ep_file(self, value): - if not value in ["a", "b", "c", "d", "e", "f", "g", "h", None]: - raise ValueError( - "Expected None or a letter between 'a' and 'h' for the " - "en-passant file, got: %s." % repr(value)) - - self.__ep_file = value - - @property - def half_moves(self): - """The number of half-moves since the last capture or pawn move.""" - return self.__half_moves - - @half_moves.setter - def half_moves(self, value): - if type(value) is not int: - raise TypeError( - "Expected integer for half move count, got: %s." % repr(value)) - if value < 0: - raise ValueError("Half move count must be >= 0.") - - self.__half_moves = value - - @property - def ply(self): - """The number of this move. The game starts at 1 and the counter - is incremented every time white moves. - """ - return self.__ply - - @ply.setter - def ply(self, value): - if type(value) is not int: - raise TypeError( - "Expected integer for ply count, got: %s." % repr(value)) - if value < 1: - raise ValueError("Ply count must be >= 1.") - self.__ply = value - - def get_piece_counts(self, color = "wb"): - """Counts the pieces on the board. - - :param color: - Defaults to `"wb"`. A color to filter the pieces by. Valid - values are "w", "b", "wb" and "bw". - - :return: - A dictionary of piece counts, keyed by lowercase piece type - letters. - """ - if not color in ["w", "b", "wb", "bw"]: - raise KeyError( - "Expected color filter to be one of 'w', 'b', 'wb', 'bw', " - "got: %s." % repr(color)) - - counts = { - "p": 0, - "b": 0, - "n": 0, - "r": 0, - "k": 0, - "q": 0, - } - for piece in self.__board: - if piece and piece.color in color: - counts[piece.type] += 1 - return counts - - def get_king(self, color): - """Gets the square of the king. - - :param color: - `"w"` for the white players king. `"b"` for the black - players king. - - :return: - The first square with a matching king or `None` if that - player has no king. - """ - if not color in ["w", "b"]: - raise KeyError("Invalid color: %s." % repr(color)) - - for x88, piece in enumerate(self.__board): - if piece and piece.color == color and piece.type == "k": - return Square.from_x88(x88) - - @property - def fen(self): - """The FEN string representing the position.""" - # Board setup. - empty = 0 - fen = "" - for y in range(7, -1, -1): - for x in range(0, 8): - square = Square.from_x_and_y(x, y) - - # Add pieces. - if not self[square]: - empty += 1 - else: - if empty > 0: - fen += str(empty) - empty = 0 - fen += self[square].symbol - - # Boarder of the board. - if empty > 0: - fen += str(empty) - if not (x == 7 and y == 0): - fen += "/" - empty = 0 - - if self.ep_file and self.get_theoretical_ep_right(self.ep_file): - ep_square = self.ep_file + ("3" if self.turn == "b" else "6") - else: - ep_square = "-" - - # Join the parts together. - return " ".join([ - fen, - self.turn, - self.__castling if self.__castling else "-", - ep_square, - str(self.half_moves), - str(self.__ply)]) - - @fen.setter - def fen(self, fen): - # Split into 6 parts. - tokens = fen.split() - if len(tokens) != 6: - raise Exception("A FEN does not consist of 6 parts.") - - # Check that the position part is valid. - rows = tokens[0].split("/") - assert len(rows) == 8 - for row in rows: - field_sum = 0 - previous_was_number = False - for char in row: - if char in "12345678": - if previous_was_number: - raise Exception( - "Position part of the FEN is invalid: " - "Multiple numbers immediately after each other.") - field_sum += int(char) - previous_was_number = True - elif char in "pnbrkqPNBRKQ": - field_sum += 1 - previous_was_number = False - else: - raise Exception( - "Position part of the FEN is invalid: " - "Invalid character in the position part of the FEN.") - - if field_sum != 8: - Exception( - "Position part of the FEN is invalid: " - "Row with invalid length.") - - # Check that the other parts are valid. - if not tokens[1] in ["w", "b"]: - raise Exception( - "Turn part of the FEN is invalid: Expected b or w.") - if not re.compile(r"^(KQ?k?q?|Qk?q?|kq?|q|-)$").match(tokens[2]): - raise Exception("Castling part of the FEN is invalid.") - if not re.compile(r"^(-|[a-h][36])$").match(tokens[3]): - raise Exception("En-passant part of the FEN is invalid.") - if not re.compile(r"^(0|[1-9][0-9]*)$").match(tokens[4]): - raise Exception("Half move part of the FEN is invalid.") - if not re.compile(r"^[1-9][0-9]*$").match(tokens[5]): - raise Exception("Ply part of the FEN is invalid.") - - # Set pieces on the board. - self.__board = [None] * 128 - i = 0 - for symbol in tokens[0]: - if symbol == "/": - i += 8 - elif symbol in "12345678": - i += int(symbol) - else: - self.__board[i] = Piece(symbol) - i += 1 - - # Set the turn. - self.__turn = tokens[1] - - # Set the castling rights. - for type in ["K", "Q", "k", "q"]: - self.set_castling_right(type, type in tokens[2]) - - # Set the en-passant file. - if tokens[3] == "-": - self.__ep_file = None - else: - self.__ep_file = tokens[3][0] - - # Set the move counters. - self.__half_moves = int(tokens[4]) - self.__ply = int(tokens[5]) - - def is_king_attacked(self, color): - """:return: Whether the king of the given color is attacked. - - :param color: `"w"` or `"b"`. - """ - square = self.get_king(color) - if square: - return self.is_attacked(opposite_color(color), square) - else: - return False - - def get_pseudo_legal_moves(self): - """:yield: Pseudo legal moves in the current position.""" - PAWN_OFFSETS = { - "b": [16, 32, 17, 15], - "w": [-16, -32, -17, -15] - } - - PIECE_OFFSETS = { - "n": [-18, -33, -31, -14, 18, 33, 31, 14], - "b": [-17, -15, 17, 15], - "r": [-16, 1, 16, -1], - "q": [-17, -16, -15, 1, 17, 16, 15, -1], - "k": [-17, -16, -15, 1, 17, 16, 15, -1] - } - - for x88, piece in enumerate(self.__board): - # Skip pieces of the opponent. - if not piece or piece.color != self.turn: - continue - - square = Square.from_x88(x88) - - # Pawn moves. - if piece.type == "p": - # Single square ahead. Do not capture. - target = Square.from_x88(x88 + PAWN_OFFSETS[self.turn][0]) - if not self[target]: - # Promotion. - if target.is_backrank(): - for promote_to in "bnrq": - yield Move(square, target, promote_to) - else: - yield Move(square, target) - - # Two squares ahead. Do not capture. - if (self.turn == "w" and square.rank == 2) or (self.turn == "b" and square.rank == 7): - target = Square.from_x88(square.x88 + PAWN_OFFSETS[self.turn][1]) - if not self[target]: - yield Move(square, target) - - # Pawn captures. - for j in [2, 3]: - target_index = square.x88 + PAWN_OFFSETS[self.turn][j] - if target_index & 0x88: - continue - target = Square.from_x88(target_index) - if self[target] and self[target].color != self.turn: - # Promotion. - if target.is_backrank(): - for promote_to in "bnrq": - yield Move(square, target, promote_to) - else: - yield Move(square, target) - # En-passant. - elif not self[target] and target.file == self.ep_file: - yield Move(square, target) - # Other pieces. - else: - for offset in PIECE_OFFSETS[piece.type]: - target_index = square.x88 - while True: - target_index += offset - if target_index & 0x88: - break - target = Square.from_x88(target_index) - if not self[target]: - yield Move(square, target) - else: - if self[target].color == self.turn: - break - yield Move(square, target) - break - - # Knight and king do not go multiple times in their - # direction. - if piece.type in ["n", "k"]: - break - - opponent = opposite_color(self.turn) - - # King-side castling. - k = "k" if self.turn == "b" else "K" - if self.get_castling_right(k): - of = self.get_king(self.turn).x88 - to = of + 2 - if not self[of + 1] and not self[to] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of + 1)) and not self.is_attacked(opponent, Square.from_x88(to)): - yield Move(Square.from_x88(of), Square.from_x88(to)) - - # Queen-side castling - q = "q" if self.turn == "b" else "Q" - if self.get_castling_right(q): - of = self.get_king(self.turn).x88 - to = of - 2 - - if not self[of - 1] and not self[of - 2] and not self[of - 3] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of - 1)) and not self.is_attacked(opponent, Square.from_x88(to)): - yield Move(Square.from_x88(of), Square.from_x88(to)) - - def get_legal_moves(self): - """:yield: All legal moves in the current position.""" - for move in self.get_pseudo_legal_moves(): - potential_position = self.copy() - potential_position.make_move(move, False) - if not potential_position.is_king_attacked(self.turn): - yield move - - def get_attackers(self, color, square): - """Gets the attackers of a specific square. - - :param color: - Filter attackers by this piece color. - :param square: - The square to check for. - - :yield: - Source squares of the attack. - """ - if color not in ["b", "w"]: - raise KeyError("Invalid color: %s." % repr(color)) - - ATTACKS = [ - 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0, - 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, - 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, - 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, - 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, - 24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0, - 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, - 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, - 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, - 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, - 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20 - ] - - RAYS = [ - 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, - 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, - 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, - 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, - 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0, - 0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0, - 0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0, - 0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0, - -15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17 - ] - - SHIFTS = { - "p": 0, - "n": 1, - "b": 2, - "r": 3, - "q": 4, - "k": 5 - } - - for x88, piece in enumerate(self.__board): - if not piece or piece.color != color: - continue - source = Square.from_x88(x88) - - difference = source.x88 - square.x88 - index = difference + 119 - - if ATTACKS[index] & (1 << SHIFTS[piece.type]): - # Handle pawns. - if piece.type == "p": - if difference > 0: - if piece.color == "w": - yield source - else: - if piece.color == "b": - yield source - continue - - # Handle knights and king. - if piece.type in ["n", "k"]: - yield source - - # Handle the others. - offset = RAYS[index] - j = source.x88 + offset - blocked = False - while j != square.x88: - if self[j]: - blocked = True - break - j += offset - if not blocked: - yield source - - def is_attacked(self, color, square): - """Checks whether a square is attacked. - - :param color: - Check if this player is attacking. - :param square: - The square the player might be attacking. - - :return: - A boolean indicating whether the given square is attacked - by the player of the given color. - """ - x = list(self.get_attackers(color, square)) - return len(x) > 0 - - def is_check(self): - """:return: Whether the current player is in check.""" - return self.is_king_attacked(self.turn) - - def is_checkmate(self): - """:return: Whether the current player has been checkmated.""" - if not self.is_check(): - return False - else: - arr = list(self.get_legal_moves()) - return len(arr) == 0 - - def is_stalemate(self): - """:return: Whether the current player is in stalemate.""" - if self.is_check(): - return False - else: - arr = list(self.get_legal_moves()) - return len(arr) == 0 - - def is_insufficient_material(self): - """Checks if there is sufficient material to mate. - - Mating is impossible in: - - * A king versus king endgame. - * A king with bishop versus king endgame. - * A king with knight versus king endgame. - * A king with bishop versus king with bishop endgame, where both - bishops are on the same color. Same goes for additional - bishops on the same color. - - Assumes that the position is valid and each player has exactly - one king. - - :return: - Whether there is insufficient material to mate. - """ - piece_counts = self.get_piece_counts() - if sum(piece_counts.values()) == 2: - # King versus king. - return True - elif sum(piece_counts.values()) == 3: - # King and knight or bishop versus king. - if piece_counts["b"] == 1 or piece_counts["n"] == 1: - return True - elif sum(piece_counts.values()) == 2 + piece_counts["b"]: - # Each player with only king and any number of bishops, where all - # bishops are on the same color. - white_has_bishop = self.get_piece_counts("w")["b"] != 0 - black_has_bishop = self.get_piece_counts("b")["b"] != 0 - if white_has_bishop and black_has_bishop: - color = None - for x88, piece in enumerate(self.__board): - if piece and piece.type == "b": - square = Square.from_x88(x88) - if color is not None and color != square.is_light(): - return False - color = square.is_light() - return True - return False - - def is_game_over(self): - """Checks if the game is over. - - :return: - Whether the game is over by the rules of chess, - disregarding that players can agree on a draw, claim a draw - or resign. - """ - return (self.is_checkmate() or self.is_stalemate() or - self.is_insufficient_material()) - - def __str__(self): - return self.fen - - def __repr__(self): - return "Position.from_fen(%s)" % repr(self.fen) - - def __eq__(self, other): - return self.fen == other.fen - - def __ne__(self, other): - return self.fen != other.fen - - -class Board(QWidget): - - def __init__(self, parent): - super(Board, self).__init__() - self.margin = 0.1 - self.padding = 0.06 - self.showCoordinates = True - self.lightSquareColor = QColor(255, 255, 255) - self.darkSquareColor = QColor(100, 100, 255) - self.borderColor = QColor(100, 100, 200) - self.shadowWidth = 2 - self.rotation = 0 - self.ply = 1 - self.setWindowTitle('Chess') - self.backgroundPixmap = QPixmap(plugin_super_class.path_to_data('chess') + "background.png") - - self.draggedSquare = None - self.dragPosition = None - - self.position = Position() - - self.parent = parent - - # Load piece set. - self.pieceRenderers = dict() - for symbol in "PNBRQKpnbrqk": - piece = Piece(symbol) - self.pieceRenderers[piece] = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type)) - - def update_title(self, my_move=False): - if self.position.is_checkmate(): - self.setWindowTitle('Checkmate') - elif self.position.is_stalemate(): - self.setWindowTitle('Stalemate') - else: - self.setWindowTitle('Chess' + (' [Your move]' if my_move else '')) - - def mousePressEvent(self, e): - self.dragPosition = e.pos() - square = self.squareAt(e.pos()) - if self.canDragSquare(square): - self.draggedSquare = square - - def mouseMoveEvent(self, e): - if self.draggedSquare: - self.dragPosition = e.pos() - self.repaint() - - def mouseReleaseEvent(self, e): - if self.draggedSquare: - dropSquare = self.squareAt(e.pos()) - if dropSquare == self.draggedSquare: - self.onSquareClicked(self.draggedSquare) - elif dropSquare: - move = self.moveFromDragDrop(self.draggedSquare, dropSquare) - if move: - self.position.make_move(move) - self.parent.move(move) - self.ply += 1 - self.draggedSquare = None - self.repaint() - - def closeEvent(self, *args): - self.parent.stop_game() - - def paintEvent(self, event): - painter = QPainter() - painter.begin(self) - - # Light shines from upper left. - if math.cos(math.radians(self.rotation)) >= 0: - lightBorderColor = self.borderColor.lighter() - darkBorderColor = self.borderColor.darker() - else: - lightBorderColor = self.borderColor.darker() - darkBorderColor = self.borderColor.lighter() - - # Draw the background. - backgroundBrush = QBrush(Qt.red, self.backgroundPixmap) - backgroundBrush.setStyle(Qt.TexturePattern) - painter.fillRect(QRect(QPoint(0, 0), self.size()), backgroundBrush) - - # Do the rotation. - painter.save() - painter.translate(self.width() / 2, self.height() / 2) - painter.rotate(self.rotation) - - # Draw the border. - frameSize = min(self.width(), self.height()) * (1 - self.margin * 2) - borderSize = min(self.width(), self.height()) * self.padding - painter.translate(-frameSize / 2, -frameSize / 2) - painter.fillRect(QRect(0, 0, frameSize, frameSize), self.borderColor) - painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth)) - painter.drawLine(0, 0, 0, frameSize) - painter.drawLine(0, 0, frameSize, 0) - painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth)) - painter.drawLine(frameSize, 0, frameSize, frameSize) - painter.drawLine(0, frameSize, frameSize, frameSize) - - # Draw the squares. - painter.translate(borderSize, borderSize) - squareSize = (frameSize - 2 * borderSize) / 8.0 - for x in range(0, 8): - for y in range(0, 8): - rect = QRect(x * squareSize, y * squareSize, squareSize, squareSize) - if (x - y) % 2 == 0: - painter.fillRect(rect, QBrush(self.lightSquareColor)) - else: - painter.fillRect(rect, QBrush(self.darkSquareColor)) - - # Draw the inset. - painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth)) - painter.drawLine(0, 0, 0, squareSize * 8) - painter.drawLine(0, 0, squareSize * 8, 0) - painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth)) - painter.drawLine(squareSize * 8, 0, squareSize * 8, squareSize * 8) - painter.drawLine(0, squareSize * 8, squareSize * 8, squareSize * 8) - - # Display coordinates. - if self.showCoordinates: - painter.setPen(QPen(QBrush(self.borderColor.lighter()), self.shadowWidth)) - coordinateSize = min(borderSize, squareSize) - font = QFont() - font.setPixelSize(coordinateSize * 0.6) - painter.setFont(font) - for i, rank in enumerate(["8", "7", "6", "5", "4", "3", "2", "1"]): - pos = QRect(-borderSize, squareSize * i, borderSize, squareSize).center() - painter.save() - painter.translate(pos.x(), pos.y()) - painter.rotate(-self.rotation) - painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, rank) - painter.restore() - for i, file in enumerate(["a", "b", "c", "d", "e", "f", "g", "h"]): - pos = QRect(squareSize * i, squareSize * 8, squareSize, borderSize).center() - painter.save() - painter.translate(pos.x(), pos.y()) - painter.rotate(-self.rotation) - painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, file) - painter.restore() - - # Draw pieces. - for x in range(0, 8): - for y in range(0, 8): - square = Square.from_x_and_y(x, 7 - y) - piece = self.position[square] - if piece and square != self.draggedSquare: - painter.save() - painter.translate((x + 0.5) * squareSize, (y + 0.5) * squareSize) - painter.rotate(-self.rotation) - self.pieceRenderers[piece].render(painter, QRectF(-squareSize / 2, -squareSize / 2, squareSize, squareSize)) - painter.restore() - - # Draw a floating piece. - painter.restore() - if self.draggedSquare: - piece = self.position[self.draggedSquare] - if piece: - painter.save() - painter.translate(self.dragPosition.x(), self.dragPosition.y()) - painter.rotate(-self.rotation) - self.pieceRenderers[piece].render(painter, QRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize)) - painter.restore() - - painter.end() - - def squareAt(self, point): - # Undo the rotation. - transform = QTransform() - transform.translate(self.width() / 2, self.height() / 2) - transform.rotate(self.rotation) - logicalPoint = transform.inverted()[0].map(point) - - frameSize = min(self.width(), self.height()) * (1 - self.margin * 2) - borderSize = min(self.width(), self.height()) * self.padding - squareSize = (frameSize - 2 * borderSize) / 8.0 - x = int(logicalPoint.x() / squareSize + 4) - y = 7 - int(logicalPoint.y() / squareSize + 4) - try: - return Square.from_x_and_y(x, y) - except IndexError: - return None - - def canDragSquare(self, square): - if (self.ply % 2 == 0 and self.parent.white) or (self.ply % 2 == 1 and not self.parent.white): - return False - for move in self.position.get_legal_moves(): - if move.source == square: - return True - return False - - def onSquareClicked(self, square): - pass - - def moveFromDragDrop(self, source, target): - for move in self.position.get_legal_moves(): - if move.source == source and move.target == target: - if move.promotion: - dialog = PromotionDialog(self.position[move.source].color, self) - if dialog.exec_(): - return Move(source, target, dialog.selectedType()) - else: - return move - return move - - -class PromotionDialog(QDialog): - - def __init__(self, color, parent=None): - super(PromotionDialog, self).__init__(parent) - - self.promotionTypes = ["q", "n", "r", "b"] - - grid = QGridLayout() - hbox = QHBoxLayout() - grid.addLayout(hbox, 0, 0) - - # Add the piece buttons. - self.buttonGroup = QButtonGroup(self) - for i, promotionType in enumerate(self.promotionTypes): - # Create an icon for the piece. - piece = Piece.from_color_and_type(color, promotionType) - renderer = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type)) - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter() - painter.begin(pixmap) - renderer.render(painter, QRect(0, 0, 32, 32)) - painter.end() - - # Add the button. - button = QPushButton(QIcon(pixmap), '', self) - button.setCheckable(True) - self.buttonGroup.addButton(button, i) - hbox.addWidget(button) - - self.buttonGroup.button(0).setChecked(True) - - # Add the ok and cancel buttons. - buttons = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) - buttons.rejected.connect(self.reject) - buttons.accepted.connect(self.accept) - grid.addWidget(buttons, 1, 0) - - self.setLayout(grid) - self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) - - def selectedType(self): - return self.promotionTypes[self.buttonGroup.checkedId()] - - -class Chess(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super(Chess, self).__init__('Chess', 'chess', *args) - self.game = -1 - self.board = None - self.white = True - self.pre = None - self.last_move = None - self.is_my_move = False - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def get_description(self): - return QtWidgets.QApplication.translate("Chess", 'Plugin which allows you to play chess with your friends.') - - def get_window(self): - inst = self - if not self.board: - self.board = Board(self) - if not hasattr(self, '_window') or not self._window: - self._window = self.board - return self.board - - def lossless_packet(self, data, friend_number): - if data == 'new': - self.pre = None - friend = self._profile.get_friend_by_number(friend_number) - reply = QMessageBox.question(None, - 'New chess game', - 'Friend {} wants to play chess game against you. Start?'.format(friend.name), - QMessageBox.Yes, - QMessageBox.No) - if reply != QMessageBox.Yes: - self.send_lossless('no', friend_number) - else: - self.send_lossless('yes', friend_number) - self.board = Board(self) - self.board.show() - self.game = friend_number - self.white = False - self.is_my_move = False - elif data == 'yes' and friend_number == self.game: - self.board = Board(self) - self.board.show() - self.board.update_title(True) - self.is_my_move = True - self.last_move = None - elif data == 'no': - self.game = -1 - elif data != self.pre: # move - self.pre = data - self.is_my_move = True - self.last_move = None - a = Square.from_x_and_y(ord(data[0]) - ord('a'), ord(data[1]) - ord('1')) - b = Square.from_x_and_y(ord(data[2]) - ord('a'), ord(data[3]) - ord('1')) - self.board.position.make_move(Move(a, b, data[4] if len(data) == 5 else None)) - self.board.repaint() - self.board.update_title(True) - self.board.ply += 1 - - def start_game(self, num): - self.white = True - self.send_lossless('new', num) - self.game = num - - def resend_move(self): - if self.is_my_move or self.last_move is None: - return - self.send_lossless(str(self.last_move), self.game) - QTimer.singleShot(1000, self.resend_move) - - def stop_game(self): - self.last_move = None - - def move(self, move): - self.is_my_move = False - self.last_move = move - self.send_lossless(str(move), self.game) - self.board.update_title() - QTimer.singleShot(1000, self.resend_move) - - def get_menu(self, menu, num): - act = QAction(QtWidgets.QApplication.translate("Chess", "Start chess game"), menu) - act.triggered.connect(lambda: self.start_game(num)) - return [act] diff --git a/toxygen/plugins/en_GB.ts b/toxygen/plugins/en_GB.ts deleted file mode 100644 index b7be07c..0000000 --- a/toxygen/plugins/en_GB.ts +++ /dev/null @@ -1,31 +0,0 @@ - - - - BirthDay - - - Birthday! - - - - - Send my birthday date to contacts - - - - - Birthday - - - - - Date in format dd.mm.yyyy - - - - - Save date - - - - diff --git a/toxygen/plugins/en_US.ts b/toxygen/plugins/en_US.ts deleted file mode 100644 index b7be07c..0000000 --- a/toxygen/plugins/en_US.ts +++ /dev/null @@ -1,31 +0,0 @@ - - - - BirthDay - - - Birthday! - - - - - Send my birthday date to contacts - - - - - Birthday - - - - - Date in format dd.mm.yyyy - - - - - Save date - - - - diff --git a/toxygen/plugins/garland.py b/toxygen/plugins/garland.py deleted file mode 100644 index d6e1a0d..0000000 --- a/toxygen/plugins/garland.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import threading -import time - -from qtpy import QtCore, QtWidgets - -from plugins.plugin_super_class import PluginSuperClass - -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 Garland(PluginSuperClass): - - def __init__(self, *args): - super(Garland, self).__init__('Garland', 'grlnd', *args) - self._thread = None - self._exec = None - self._time = 3 - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def get_description(self): - return QtWidgets.QApplication.translate("Garland", "Changes your status like it's garland.") - - def close(self): - self.stop() - - def stop(self): - self._exec = False - self._thread.join() - - def start(self): - self._exec = True - self._thread = threading.Thread(target=self.change_status) - self._thread.start() - - def command(self, command): - if command.startswith('time'): - self._time = max(int(command.split(' ')[1]), 300) / 1000 - else: - super().command(command) - - def update(self): - if hasattr(self, '_profile'): - if not hasattr(self._profile, 'status') or not self._profile.status: - retval = 0 - else: - retval = (self._profile.status + 1) % 3 - self._profile.set_status(retval) - - def change_status(self): - time.sleep(5) - while self._exec: - invoke_in_main_thread(self.update) - time.sleep(self._time) - diff --git a/toxygen/plugins/mrq.py b/toxygen/plugins/mrq.py deleted file mode 100644 index db718fe..0000000 --- a/toxygen/plugins/mrq.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import threading -import time - -from qtpy import QtCore, QtWidgets - -import plugin_super_class - -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 MarqueeStatus(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super(MarqueeStatus, self).__init__('MarqueeStatus', 'mrq', *args) - self._thread = None - self._exec = None - self.active = False - self.left = True - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def get_description(self): - return QtWidgets.QApplication.translate("MarqueeStatus", 'Create ticker from your status message.') - - def close(self): - self.stop() - - def stop(self): - self._exec = False - if self.active: - self._thread.join() - - def start(self): - self._exec = True - self._thread = threading.Thread(target=self.change_status) - self._thread.start() - - def command(self, command): - if command == 'rev': - self.left = not self.left - else: - super(MarqueeStatus, self).command(command) - - def set_status_message(self): - message = str(self._profile.status_message) - if self.left: - self._profile.set_status_message(bytes(message[1:] + message[0], 'utf-8')) - else: - self._profile.set_status_message(bytes(message[-1] + message[:-1], 'utf-8')) - - def init_status(self): - self._profile.status_message = bytes(self._profile.status_message.strip() + ' ', 'utf-8') - - def change_status(self): - self.active = True - if hasattr(self, '_profile'): - tmp = self._profile.status_message - time.sleep(10) - invoke_in_main_thread(self.init_status) - while self._exec: - time.sleep(1) - if self._profile.status is not None: - invoke_in_main_thread(self.set_status_message) - invoke_in_main_thread(self._profile.set_status_message, bytes(tmp, 'utf-8')) - self.active = False - diff --git a/toxygen/plugins/plugin_super_class.py b/toxygen/plugins/plugin_super_class.py index 4c6287d..84c4a3e 100644 --- a/toxygen/plugins/plugin_super_class.py +++ b/toxygen/plugins/plugin_super_class.py @@ -1,10 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import os +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui -from qtpy import QtCore, QtWidgets - -import utils.ui as util_ui -import common.tox_save as tox_save MAX_SHORT_NAME_LENGTH = 5 @@ -12,6 +11,7 @@ LOSSY_FIRST_BYTE = 200 LOSSLESS_FIRST_BYTE = 160 + def path_to_data(name): """ :param name: plugin unique name @@ -20,40 +20,46 @@ def path_to_data(name): return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/' -def log(name, data=''): +def log(name, data): """ :param name: plugin unique name :param data: data for saving in log """ with open(path_to_data(name) + 'logs.txt', 'a') as fl: - fl.write(str(data) + '\n') + fl.write(bytes(data, 'utf-8') + b'\n') -class PluginSuperClass(tox_save.ToxSave): +class PluginSuperClass: """ - Superclass for all plugins. Plugin is Python3 module with at least one class derived from PluginSuperClass. + Superclass for all plugins. Plugin is python module with at least one class derived from PluginSuperClass. """ is_plugin = True - def __init__(self, name, short_name, app): + def __init__(self, name, short_name, tox=None, profile=None, settings=None, encrypt_save=None): """ - Constructor. In plugin __init__ should take only 1 last argument + Constructor. In plugin __init__ should take only 4 last arguments :param name: plugin full name :param short_name: plugin unique short name (length of short name should not exceed MAX_SHORT_NAME_LENGTH) - :param app: App instance + :param tox: tox instance + :param profile: profile instance + :param settings: profile settings + :param encrypt_save: LibToxEncryptSave instance. """ - tox = getattr(app, '_tox') - super().__init__(tox) - self._settings = getattr(app, '_settings') + self._settings = settings + self._profile = profile + self._tox = tox name = name.strip() short_name = short_name.strip() if not name or not short_name: - raise NameError('Wrong name or not name or not short_name') + raise NameError('Wrong name') 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 + # ----------------------------------------------------------------------------------------------------------------- def get_name(self): """ @@ -73,11 +79,12 @@ class PluginSuperClass(tox_save.ToxSave): """ return self.__doc__ - def get_menu(self, menu, row_number=None): + def get_menu(self, menu, 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 tuples (text, handler) + :return list of QAction's """ return [] @@ -96,7 +103,15 @@ class PluginSuperClass(tox_save.ToxSave): """ return None + def set_tox(self, tox): + """ + New tox instance + """ + self._tox = tox + + # ----------------------------------------------------------------------------------------------------------------- # Plugin was stopped, started or new command received + # ----------------------------------------------------------------------------------------------------------------- def start(self): """ @@ -114,7 +129,7 @@ class PluginSuperClass(tox_save.ToxSave): """ App is closing """ - self.stop() + pass def command(self, command): """ @@ -122,17 +137,21 @@ class PluginSuperClass(tox_save.ToxSave): :param command: string with command """ if command == 'help': - 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) + msgbox = QtGui.QMessageBox() + title = QtGui.QApplication.translate("PluginWindow", "List of commands for plugin {}", None, QtGui.QApplication.UnicodeUTF8) + msgbox.setWindowTitle(title.format(self._name)) + msgbox.setText(QtGui.QApplication.translate("PluginWindow", "No commands available", None, QtGui.QApplication.UnicodeUTF8)) + msgbox.exec_() + # ----------------------------------------------------------------------------------------------------------------- # Translations support + # ----------------------------------------------------------------------------------------------------------------- def load_translator(self): """ This method loads translations for GUI """ - app = QtWidgets.QApplication.instance() + app = QtGui.QApplication.instance() langs = self._settings.supported_languages() curr_lang = self._settings['language'] if curr_lang in langs: @@ -143,12 +162,13 @@ class PluginSuperClass(tox_save.ToxSave): self._translator.load(path_to_data(self._short_name) + lang_path) app.installTranslator(self._translator) + # ----------------------------------------------------------------------------------------------------------------- # Settings loading and saving + # ----------------------------------------------------------------------------------------------------------------- def load_settings(self): """ This method loads settings of plugin and returns raw data - If file doesn't exist this method raises exception """ with open(path_to_data(self._short_name) + 'settings.json', 'rb') as fl: data = fl.read() @@ -162,7 +182,9 @@ class PluginSuperClass(tox_save.ToxSave): with open(path_to_data(self._short_name) + 'settings.json', 'wb') as fl: fl.write(bytes(data, 'utf-8')) + # ----------------------------------------------------------------------------------------------------------------- # Callbacks + # ----------------------------------------------------------------------------------------------------------------- def lossless_packet(self, data, friend_number): """ @@ -180,13 +202,15 @@ class PluginSuperClass(tox_save.ToxSave): """ pass - def friend_connected(self, friend_number:int): + def friend_connected(self, friend_number): """ Friend with specified number is online now """ pass + # ----------------------------------------------------------------------------------------------------------------- # Custom packets sending + # ----------------------------------------------------------------------------------------------------------------- def send_lossless(self, data, friend_number): """ diff --git a/toxygen/plugins/ru_RU.qm b/toxygen/plugins/ru_RU.qm deleted file mode 100644 index 6ba937c..0000000 Binary files a/toxygen/plugins/ru_RU.qm and /dev/null differ diff --git a/toxygen/plugins/ru_RU.ts b/toxygen/plugins/ru_RU.ts deleted file mode 100644 index d5b0374..0000000 --- a/toxygen/plugins/ru_RU.ts +++ /dev/null @@ -1,32 +0,0 @@ - - - - - BirthDay - - - Birthday! - День рождения! - - - - Send my birthday date to contacts - Отправлять дату моего рождения контактам - - - - Birthday - День рождения - - - - Date in format dd.mm.yyyy - Дата в формате дд.мм.гггг - - - - Save date - Сохранить дату - - - diff --git a/toxygen/plugins/srch.pro b/toxygen/plugins/srch.pro deleted file mode 100644 index d071285..0000000 --- a/toxygen/plugins/srch.pro +++ /dev/null @@ -1,2 +0,0 @@ -SOURCES = srch.py -TRANSLATIONS = srch/en_GB.ts srch/en_US.ts srch/ru_RU.ts diff --git a/toxygen/plugins/srch.py b/toxygen/plugins/srch.py deleted file mode 100644 index 5dcf8d3..0000000 --- a/toxygen/plugins/srch.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import QtGui, QtCore, QtWidgets - -import plugin_super_class - -class SearchPlugin(plugin_super_class.PluginSuperClass): - - def __init__(self, *args): - super(SearchPlugin, self).__init__('SearchPlugin', 'srch', *args) - - def get_description(self): - return QtWidgets.QApplication.translate("SearchPlugin", 'Plugin search with search engines.') - - def get_message_menu(self, menu, text): - google = QtWidgets.QAction( - QtWidgets.QApplication.translate("srch", "Find in Google"), - menu) - google.triggered.connect(lambda: self.google(text)) - - duck = QtWidgets.QAction( - QtWidgets.QApplication.translate("srch", "Find in DuckDuckGo"), - menu) - duck.triggered.connect(lambda: self.duck(text)) - - yandex = QtWidgets.QAction( - QtWidgets.QApplication.translate("srch", "Find in Yandex"), - menu) - yandex.triggered.connect(lambda: self.yandex(text)) - - bing = QtWidgets.QAction( - QtWidgets.QApplication.translate("srch", "Find in Bing"), - menu) - bing.triggered.connect(lambda: self.bing(text)) - - return [duck, google, yandex, bing] - - def google(self, text): - url = QtCore.QUrl('https://www.google.com/search?q=' + text) - self.open_url(url) - - def duck(self, text): - url = QtCore.QUrl('https://duckduckgo.com/?q=' + text) - self.open_url(url) - - def yandex(self, text): - url = QtCore.QUrl('https://yandex.com/search/?text=' + text) - self.open_url(url) - - def bing(self, text): - url = QtCore.QUrl('https://www.bing.com/search?q=' + text) - self.open_url(url) - - def open_url(self, url): - QtGui.QDesktopServices.openUrl(url) - diff --git a/toxygen/plugins/toxid.pro b/toxygen/plugins/toxid.pro deleted file mode 100644 index 3b1cc64..0000000 --- a/toxygen/plugins/toxid.pro +++ /dev/null @@ -1,2 +0,0 @@ -SOURCES = toxid.py -TRANSLATIONS = toxid/en_GB.ts toxid/en_US.ts toxid/ru_RU.ts diff --git a/toxygen/plugins/toxid.py b/toxygen/plugins/toxid.py deleted file mode 100644 index e604092..0000000 --- a/toxygen/plugins/toxid.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import json - -from qtpy import QtCore, QtWidgets - -from plugins.plugin_super_class import PluginSuperClass - -class CopyableToxId(PluginSuperClass): - - def __init__(self, *args): - super(CopyableToxId, self).__init__('CopyableToxId', 'toxid', *args) - self._data = json.loads(self.load_settings()) - self._copy = False - self._curr = -1 - self._timer = QtCore.QTimer() - self._timer.timeout.connect(lambda: self.timer()) - self.load_translator() - self._app = args[0] - self._profile=self._app._ms._profile - self._window = None - - def get_description(self): - return QtWidgets.QApplication.translate("TOXID", 'Plugin which allows you to copy TOX ID of your friends easily.') - - def get_window(self): - inst = self - - class Window(QtWidgets.QWidget): - - def __init__(self): - super(Window, self).__init__() - self.setGeometry(QtCore.QRect(450, 300, 350, 100)) - self.send = QtWidgets.QCheckBox(self) - self.send.setGeometry(QtCore.QRect(20, 10, 310, 25)) - self.send.setText(QtWidgets.QApplication.translate("TOXID", "Send my TOX ID to contacts")) - self.setWindowTitle(QtWidgets.QApplication.translate("TOXID", "CopyableToxID")) - self.send.clicked.connect(self.update) - self.send.setChecked(inst._data['send_id']) - self.help = QtWidgets.QPushButton(self) - self.help.setGeometry(QtCore.QRect(20, 40, 200, 25)) - self.help.setText(QtWidgets.QApplication.translate("TOXID", "List of commands")) - self.help.clicked.connect(lambda: inst.command('help')) - - def update(self): - inst._data['send_id'] = self.send.isChecked() - inst.save_settings(json.dumps(inst._data)) - - if not hasattr(self, '_window') or not self._window: - self._window = Window() - return self._window - - def lossless_packet(self, data, friend_number) -> None: - if len(data): - self._data['id'] = list(filter(lambda x: not x.startswith(data[:64]), self._data['id'])) - self._data['id'].append(data) - if self._copy: - self._timer.stop() - self._copy = False - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(data) - self.save_settings(json.dumps(self._data)) - elif self._data['send_id']: - self.send_lossless(self._tox.self_get_address(), friend_number) - - def error(self) -> None: - msgbox = QtWidgets.QMessageBox() - title = QtWidgets.QApplication.translate("TOXID", "Error") - msgbox.setWindowTitle(title.format(self._name)) - text = QtWidgets.QApplication.translate("TOXID", "Tox ID cannot be copied") - msgbox.setText(text) - msgbox.exec_() - - def timer(self) -> None: - self._copy = False - if self._curr + 1: - public_key = self._tox.friend_get_public_key(self._curr) - self._curr = -1 - arr = list(filter(lambda x: x.startswith(public_key), self._data['id'])) - if len(arr): - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(arr[0]) - else: - self.error() - else: - self.error() - self._timer.stop() - - def friend_connected(self, friend_number:int): - self.send_lossless('', friend_number) - - def command(self, text) -> None: - if text == 'copy': - num = self._profile.get_active_number() - if num == -1: - return - elif text.startswith('copy '): - num = int(text[5:]) - if num < 0: - return - elif text == 'enable': - self._copy = True - return - elif text == 'disable': - self._copy = False - return - elif text == 'help': - msgbox = QtWidgets.QMessageBox() - title = QtWidgets.QApplication.translate("TOXID", "List of commands for plugin CopyableToxID") - msgbox.setWindowTitle(title) - text = QtWidgets.QApplication.translate("TOXID", """Commands: -copy: copy TOX ID of current friend -copy : copy TOX ID of friend with specified number -enable: allow send your TOX ID to friends -disable: disallow send your TOX ID to friends -help: show this help""") - msgbox.setText(text) - msgbox.exec_() - return - else: - return - public_key = self._tox.friend_get_public_key(num) - arr = list(filter(lambda x: x.startswith(public_key), self._data['id'])) - if self._profile.get_friend_by_number(num).status is None and len(arr): - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(arr[0]) - elif self._profile.get_friend_by_number(num).status is not None: - self._copy = True - self._curr = num - self.send_lossless('', num) - self._timer.start(2000) - else: - self.error() - - def get_menu(self, menu, num) -> list: - act = QtWidgets.QAction(QtWidgets.QApplication.translate("TOXID", "Copy TOX ID"), menu) - friend = self._profile.get_friend(num) - act.connect(act, QtCore.Signal("triggered()"), - lambda: self.command('copy ' + str(friend.number))) - return [act] diff --git a/toxygen/profile.py b/toxygen/profile.py new file mode 100644 index 0000000..ff271d1 --- /dev/null +++ b/toxygen/profile.py @@ -0,0 +1,1273 @@ +from list_items import * +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +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 + + +class Profile(contact.Contact, 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 + """ + contact.Contact.__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._incoming_calls = set() + self._load_history = True + settings = Settings.get_instance() + self._show_online = settings['show_online_friends'] + self._show_avatars = settings['show_avatars'] + 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._show_online)) + aliases = settings['friends_aliases'] + data = tox.self_get_friend_list() + self._history = History(tox.self_get_public_key()) # connection to db + self._friends, 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._friends.append(friend) + self.filtration(self._show_online) + + # ----------------------------------------------------------------------------------------------------------------- + # 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) + else: + QtCore.QTimer.singleShot(30000, 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 = QtGui.QApplication.translate("MainWindow", 'User {} is now known as {}', None, + QtGui.QApplication.UnicodeUTF8) + message = message.format(tmp, value) + for friend in self._friends: + 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(self, show_online=True, filter_str=''): + """ + Filtration of friends list + :param show_online: show online only contacts + :param filter_str: show contacts which name contains this substring + """ + filter_str = filter_str.lower() + settings = Settings.get_instance() + for index, friend in enumerate(self._friends): + friend.visibility = (friend.status is not None or not show_online) 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._show_online, self._filter_string = show_online, filter_str + settings['show_online_friends'] = self._show_online + settings.save() + + def update_filtration(self): + """ + Update list of contacts when 1 of friends change connection status + """ + self.filtration(self._show_online, self._filter_string) + + def get_friend_by_number(self, num): + return list(filter(lambda x: x.number == num, self._friends))[0] + + def get_friend(self, num): + return self._friends[num] + + # ----------------------------------------------------------------------------------------------------------------- + # 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._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._friends[self._active_friend].curr_text = self._screen.messageEdit.toPlainText() + except: + pass + friend = self._friends[value] + if self._active_friend != value: + self._screen.messageEdit.setPlainText(friend.curr_text) + self._active_friend = value + self._friends[value].reset_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) + 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()) + else: # info message + data = message.get_data() + self.create_message_item(data[0], + data[2], + '', + 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._friends[self._active_friend] + + self._screen.account_name.setText(friend.name) + self._screen.account_status.setText(friend.status_message) + 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)) + self.update_filtration() + except Exception as ex: # no friend found. ignore + log('Friend value: ' + str(value)) + log('Error: ' + str(ex)) + raise + + active_friend = property(get_active, set_active) + + def get_last_message(self): + return self._friends[self._active_friend].get_last_message_text() + + def get_active_number(self): + return self._friends[self._active_friend].number if self._active_friend + 1 else -1 + + def get_active_name(self): + return self._friends[self._active_friend].name if self._active_friend + 1 else '' + + def is_active_online(self): + return self._active_friend + 1 and self._friends[self._active_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 = QtGui.QApplication.translate("MainWindow", 'User {} is now known as {}', None, QtGui.QApplication.UnicodeUTF8) + 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) + 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(): + 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: + 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._friends[self._active_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(): + 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: + pass + + def split_and_send(self, number, message_type, message): + """ + Message splitting + :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 ' ' in last_part: + index = last_part.index(' ') + elif ',' in last_part: + index = last_part.index(',') + elif '.' in last_part: + index = last_part.index('.') + 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(): # add message to list + t = time.time() + self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) + self._messages.scrollToBottom() + self._friends[self._active_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 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')) + if friend.number == self.get_active_number(): + t = time.time() + 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._friends[self._active_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 self._friends: + 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._friends[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._friends)): + 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._friends[self._active_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) + 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._friends[num] + if _range is None: + friend.load_all_corr() + if _range is None: + 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) + return s + + # ----------------------------------------------------------------------------------------------------------------- + # Factories for friend, message and file transfer items + # ----------------------------------------------------------------------------------------------------------------- + + def create_friend_item(self): + """ + Method-factory + :return: new widget for friend instance + """ + item = ContactItem() + elem = QtGui.QListWidgetItem(self._screen.friends_list) + elem.setSizeHint(QtCore.QSize(250, item.height())) + self._screen.friends_list.addItem(elem) + self._screen.friends_list.setItemWidget(elem, item) + return 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 + item = MessageItem(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], message_type, self._messages) + if self._show_avatars: + item.set_avatar(self._friends[self._active_friend].get_pixmap() if owner == MESSAGE_OWNER[ + 'FRIEND'] else self.get_pixmap()) + elem = QtGui.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) + + 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 + data.append(self._messages.width()) + item = FileTransferItem(*data) + elem = QtGui.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_unsent_file_item(self, message, append=True): + data = message.get_data() + item = UnsentFileItem(os.path.basename(data[0]), + os.path.getsize(data[0]) if data[1] is None else len(data[1]), + self.name, + data[2], + self._messages.width()) + elem = QtGui.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) + + def create_inline_item(self, data, append=True): + elem = QtGui.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) + + # ----------------------------------------------------------------------------------------------------------------- + # Work with friends (remove, block, set alias, get public key) + # ----------------------------------------------------------------------------------------------------------------- + + def set_alias(self, num): + """ + Set new alias for friend + """ + friend = self._friends[num] + name = friend.name + dialog = QtGui.QApplication.translate('MainWindow', + "Enter new alias for friend {} or leave empty to use friend's name:", + None, QtGui.QApplication.UnicodeUTF8) + dialog = dialog.format(name) + title = QtGui.QApplication.translate('MainWindow', + 'Set alias', + None, QtGui.QApplication.UnicodeUTF8) + text, ok = QtGui.QInputDialog.getText(None, + title, + dialog, + QtGui.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(): + self.update() + + def friend_public_key(self, num): + return self._friends[num].tox_id + + def delete_friend(self, num): + """ + Removes friend from contact list + :param num: number of friend in list + """ + friend = self._friends[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._friends[num] + self._screen.friends_list.takeItem(num) + if num == self._active_friend: # active friend was deleted + if not len(self._friends): # 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._friends.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 = QtGui.QMessageBox() + msgBox.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Friend added", None, QtGui.QApplication.UnicodeUTF8)) + text = (QtGui.QApplication.translate("MainWindow", 'Friend added without sending friend request', None, QtGui.QApplication.UnicodeUTF8)) + 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._friends.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 = QtGui.QApplication.translate('MainWindow', 'User {} wants to add you to contact list. Message:\n{}', None, QtGui.QApplication.UnicodeUTF8) + info = text.format(tox_id, message) + fr_req = QtGui.QApplication.translate('MainWindow', 'Friend request', None, QtGui.QApplication.UnicodeUTF8) + reply = QtGui.QMessageBox.question(None, fr_req, info, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + if reply == QtGui.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 friend in self._friends: + self.friend_exit(friend.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._friends: + friend.number = self._tox.friend_by_public_key(friend.tox_id) # numbers update + self.update_filtration() + + def reconnect(self): + if self.status is None or all(list(map(lambda x: x.status is None, self._friends))): + self.reset(self._screen.reset) + QtCore.QTimer.singleShot(30000, self.reconnect) + + def close(self): + for friend in self._friends: + self.friend_exit(friend.number) + for i in range(len(self._friends)): + del self._friends[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 = (file_name in ALLOWED_FILES) 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(): + item = self.create_file_transfer_item(tm) + if accepted: + self._file_transfers[(friend_number, file_number)].set_state_changed_handler(item.update) + 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(): + tmp = self._messages.count() + i + if tmp >= 0: + self._messages.itemWidget(self._messages.item(tmp)).update(TOX_FILE_TRANSFER_STATE['CANCELLED'], + 0, -1) + + def cancel_not_started_transfer(self, time): + self._friends[self._active_friend].delete_one_unsent_file(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) + 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) + 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) + self._messages.scrollToBottom() + self._friends[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) + + @QtCore.Slot(int, int) + 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() + 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(): + count = self._messages.count() + if count + i + 1 >= 0: + elem = QtGui.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) + 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 + else: + self.get_friend_by_number(friend_number).load_avatar() + if self.get_active_number() == friend_number: + 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._friends): + 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._friends): + 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 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 = QtGui.QApplication.translate("incoming_call", "Outgoing video call", None, + QtGui.QApplication.UnicodeUTF8) + else: + text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None, + QtGui.QApplication.UnicodeUTF8) + self._friends[self._active_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. Only audio is supported now + """ + if not Settings.get_instance().audio['enabled']: + return + friend = self.get_friend_by_number(friend_number) + if video: + text = QtGui.QApplication.translate("incoming_call", "Incoming video call", None, + QtGui.QApplication.UnicodeUTF8) + else: + text = QtGui.QApplication.translate("incoming_call", "Incoming audio call", None, + QtGui.QApplication.UnicodeUTF8) + 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 + # TODO: dict of widgets + self._call_widget = avwidgets.IncomingCallWidget(friend_number, text, friend.name) + self._call_widget.set_pixmap(friend.get_pixmap()) + self._call_widget.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() + self._incoming_calls.remove(friend_number) + if hasattr(self, '_call_widget'): + del self._call_widget + + 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 = QtGui.QApplication.translate("incoming_call", "Call declined", None, QtGui.QApplication.UnicodeUTF8) + else: + text = QtGui.QApplication.translate("incoming_call", "Call finished", None, QtGui.QApplication.UnicodeUTF8) + self._screen.call_finished() + self._call.finish_call(friend_number, by_friend) # finish or decline call + if hasattr(self, '_call_widget'): + self._call_widget.close() + del self._call_widget + 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() + + +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() + 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/settings.py b/toxygen/settings.py new file mode 100644 index 0000000..3623d69 --- /dev/null +++ b/toxygen/settings.py @@ -0,0 +1,291 @@ +from platform import system +import json +import os +from util import Singleton, curr_directory, log, copy +import pyaudio +from toxencryptsave import ToxEncryptSave +import smileys + + +class Settings(dict, Singleton): + """ + 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: + data = fl.read() + inst = ToxEncryptSave.get_instance() + try: + if inst.is_data_encrypted(data): + data = inst.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() + else: + super(Settings, self).__init__(Settings.get_default_settings()) + self.save() + smileys.SmileyLoader(self) + self.locked = False + self.closing = False + p = pyaudio.PyAudio() + input_devices = output_devices = 0 + for i in range(p.get_device_count()): + device = p.get_device_info_by_index(i) + if device["maxInputChannels"]: + input_devices += 1 + if device["maxOutputChannels"]: + output_devices += 1 + self.audio = {'input': p.get_default_input_device_info()['index'] if input_devices else -1, + 'output': p.get_default_output_device_info()['index'] if output_devices else -1, + 'enabled': input_devices and output_devices} + + @staticmethod + def get_auto_profile(): + path = Settings.get_default_path() + 'toxygen.json' + if os.path.isfile(path): + with open(path) as fl: + data = fl.read() + auto = json.loads(data) + if 'path' in auto and 'name' in auto: + return str(auto['path']), str(auto['name']) + return '', '' + + @staticmethod + def set_auto_profile(path, name): + p = Settings.get_default_path() + 'toxygen.json' + with open(p) as fl: + data = fl.read() + data = json.loads(data) + data['path'] = str(path) + data['name'] = str(name) + with open(p, 'w') as fl: + fl.write(json.dumps(data)) + + @staticmethod + def reset_auto_profile(): + p = Settings.get_default_path() + 'toxygen.json' + with open(p) as fl: + data = fl.read() + data = json.loads(data) + if 'path' in data: + del data['path'] + del data['name'] + with open(p, 'w') as fl: + fl.write(json.dumps(data)) + + @staticmethod + def is_active_profile(path, name): + path = path + name + '.tox' + settings = Settings.get_default_path() + 'toxygen.json' + if os.path.isfile(settings): + with open(settings) as fl: + data = fl.read() + data = json.loads(data) + if 'active_profile' in data: + return path in data['active_profile'] + return False + + @staticmethod + def get_default_settings(): + """ + Default profile settings + """ + return { + 'theme': 'default', + 'ipv6_enabled': True, + 'udp_enabled': True, + 'proxy_type': 0, + 'proxy_host': '127.0.0.1', + 'proxy_port': 9050, + 'start_port': 0, + 'end_port': 0, + 'tcp_port': 0, + 'notifications': True, + 'sound_notifications': False, + 'language': 'English', + 'save_history': False, + 'allow_inline': True, + 'allow_auto_accept': True, + 'auto_accept_path': None, + 'show_online_friends': False, + 'auto_accept_from_friends': [], + 'paused_file_transfers': {}, + 'resend_files': True, + 'friends_aliases': [], + 'show_avatars': False, + 'typing_notifications': False, + 'calls_sound': True, + 'blocked': [], + 'plugins': [], + 'notes': {}, + 'smileys': True, + 'smiley_pack': 'default', + 'mirror_mode': False, + 'width': 920, + 'height': 500, + 'x': 400, + 'y': 400, + 'message_font_size': 14, + 'unread_color': 'red', + 'save_unsent_only': False, + 'compact_mode': False, + 'show_welcome_screen': True, + 'close_to_tray': False, + 'font': 'Times New Roman' + } + + @staticmethod + def supported_languages(): + return { + 'English': 'en_EN', + 'Russian': 'ru_RU', + 'French': 'fr_FR' + } + + 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 = ToxEncryptSave.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): + path = Settings.get_default_path() + 'toxygen.json' + if os.path.isfile(path): + with open(path) as fl: + data = fl.read() + app_settings = json.loads(data) + try: + app_settings['active_profile'].remove(str(ProfileHelper.get_path() + self.name + '.tox')) + except: + pass + data = json.dumps(app_settings) + with open(path, 'w') as fl: + fl.write(data) + + def set_active_profile(self): + """ + Mark current profile as active + """ + path = Settings.get_default_path() + 'toxygen.json' + if os.path.isfile(path): + with open(path) as fl: + data = fl.read() + app_settings = json.loads(data) + else: + app_settings = {} + if 'active_profile' not in app_settings: + app_settings['active_profile'] = [] + profilepath = ProfileHelper.get_path() + app_settings['active_profile'].append(str(profilepath + str(self.name) + '.tox')) + data = json.dumps(app_settings) + with open(path, 'w') as fl: + fl.write(data) + + 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_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) + 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 = ToxEncryptSave.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/smileys/smileys.py b/toxygen/smileys.py similarity index 70% rename from toxygen/smileys/smileys.py rename to toxygen/smileys.py index 604e681..9143a0b 100644 --- a/toxygen/smileys/smileys.py +++ b/toxygen/smileys.py @@ -1,20 +1,14 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - +import util import json -import logging import os from collections import OrderedDict +try: + from PySide import QtCore +except ImportError: + from PyQt4 import QtCore -from qtpy import QtCore -from utils import util - -# LOG=util.log -global LOG -LOG = logging.getLogger('app.'+__name__) -log = lambda x: LOG.info(x) - -class SmileyLoader: +class SmileyLoader(util.Singleton): """ Class which loads smileys packs and insert smileys into messages """ @@ -34,16 +28,16 @@ class SmileyLoader: pack_name = self._settings['smiley_pack'] if self._settings['smileys'] and self._curr_pack != pack_name: self._curr_pack = pack_name - path = util.join_path(self.get_smileys_path(), 'config.json') + path = self.get_smileys_path() + 'config.json' try: with open(path, encoding='utf8') as fl: self._smileys = json.loads(fl.read()) fl.seek(0) tmp = json.loads(fl.read(), object_pairs_hook=OrderedDict) - LOG.info('Smiley pack {} loaded'.format(pack_name)) + print('Smiley pack {} loaded'.format(pack_name)) keys, values, self._list = [], [], [] for key, value in tmp.items(): - value = util.join_path(self.get_smileys_path(), value) + value = self.get_smileys_path() + value if value not in values: keys.append(key) values.append(value) @@ -51,14 +45,13 @@ class SmileyLoader: except Exception as ex: self._smileys = {} self._list = [] - LOG.error('Smiley pack {} was not loaded. Error: {}'.format(pack_name, str(ex))) + print('Smiley pack {} was not loaded. Error: {}'.format(pack_name, ex)) def get_smileys_path(self): - return util.join_path(util.get_smileys_directory(), self._curr_pack) if self._curr_pack is not None else None + return util.curr_directory() + '/smileys/' + self._curr_pack + '/' - @staticmethod - def get_packs_list(): - d = util.get_smileys_directory() + def get_packs_list(self): + d = util.curr_directory() + '/smileys/' return [x[1] for x in os.walk(d)][0] def get_smileys(self): @@ -81,3 +74,18 @@ class SmileyLoader: 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/smileys/__init__.py b/toxygen/smileys/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/smileys/default/003020E3.png b/toxygen/smileys/default/003020E3.png index e64ea3a..a196fa1 100644 Binary files a/toxygen/smileys/default/003020E3.png and b/toxygen/smileys/default/003020E3.png differ diff --git a/toxygen/smileys/default/003120E3.png b/toxygen/smileys/default/003120E3.png index 9501bdf..26d6754 100644 Binary files a/toxygen/smileys/default/003120E3.png and b/toxygen/smileys/default/003120E3.png differ diff --git a/toxygen/smileys/default/003220E3.png b/toxygen/smileys/default/003220E3.png index 8c44746..645c904 100644 Binary files a/toxygen/smileys/default/003220E3.png and b/toxygen/smileys/default/003220E3.png differ diff --git a/toxygen/smileys/default/003320E3.png b/toxygen/smileys/default/003320E3.png index ab5b4bc..1674b69 100644 Binary files a/toxygen/smileys/default/003320E3.png and b/toxygen/smileys/default/003320E3.png differ diff --git a/toxygen/smileys/default/003420E3.png b/toxygen/smileys/default/003420E3.png index 4ecbce7..ef64830 100644 Binary files a/toxygen/smileys/default/003420E3.png and b/toxygen/smileys/default/003420E3.png differ diff --git a/toxygen/smileys/default/003520E3.png b/toxygen/smileys/default/003520E3.png index c3d3077..782ee47 100644 Binary files a/toxygen/smileys/default/003520E3.png and b/toxygen/smileys/default/003520E3.png differ diff --git a/toxygen/smileys/default/003620E3.png b/toxygen/smileys/default/003620E3.png index 617d2ca..07f549a 100644 Binary files a/toxygen/smileys/default/003620E3.png and b/toxygen/smileys/default/003620E3.png differ diff --git a/toxygen/smileys/default/003720E3.png b/toxygen/smileys/default/003720E3.png index 7fce639..5093629 100644 Binary files a/toxygen/smileys/default/003720E3.png and b/toxygen/smileys/default/003720E3.png differ diff --git a/toxygen/smileys/default/003820E3.png b/toxygen/smileys/default/003820E3.png index 3ecb8fc..aea2c90 100644 Binary files a/toxygen/smileys/default/003820E3.png and b/toxygen/smileys/default/003820E3.png differ diff --git a/toxygen/smileys/default/003920E3.png b/toxygen/smileys/default/003920E3.png index f1d6641..5a19d1b 100644 Binary files a/toxygen/smileys/default/003920E3.png and b/toxygen/smileys/default/003920E3.png differ diff --git a/toxygen/smileys/default/00A9.png b/toxygen/smileys/default/00A9.png index 57666e9..5f52426 100644 Binary files a/toxygen/smileys/default/00A9.png and b/toxygen/smileys/default/00A9.png differ diff --git a/toxygen/smileys/default/00AE.png b/toxygen/smileys/default/00AE.png index 98fb62a..ebc7dd9 100644 Binary files a/toxygen/smileys/default/00AE.png and b/toxygen/smileys/default/00AE.png differ diff --git a/toxygen/smileys/default/203C.png b/toxygen/smileys/default/203C.png index 36d8dcf..e1c3057 100644 Binary files a/toxygen/smileys/default/203C.png and b/toxygen/smileys/default/203C.png differ diff --git a/toxygen/smileys/default/2049.png b/toxygen/smileys/default/2049.png index feb5368..0bacbe9 100644 Binary files a/toxygen/smileys/default/2049.png and b/toxygen/smileys/default/2049.png differ diff --git a/toxygen/smileys/default/2122.png b/toxygen/smileys/default/2122.png index 5119eae..8b5e91a 100644 Binary files a/toxygen/smileys/default/2122.png and b/toxygen/smileys/default/2122.png differ diff --git a/toxygen/smileys/default/2139.png b/toxygen/smileys/default/2139.png index 4393f8a..89e6eb4 100644 Binary files a/toxygen/smileys/default/2139.png and b/toxygen/smileys/default/2139.png differ diff --git a/toxygen/smileys/default/2194.png b/toxygen/smileys/default/2194.png index c8f4d49..87aa873 100644 Binary files a/toxygen/smileys/default/2194.png and b/toxygen/smileys/default/2194.png differ diff --git a/toxygen/smileys/default/2195.png b/toxygen/smileys/default/2195.png index 7d49587..beb8b2c 100644 Binary files a/toxygen/smileys/default/2195.png and b/toxygen/smileys/default/2195.png differ diff --git a/toxygen/smileys/default/2196.png b/toxygen/smileys/default/2196.png index 210315d..a1769d4 100644 Binary files a/toxygen/smileys/default/2196.png and b/toxygen/smileys/default/2196.png differ diff --git a/toxygen/smileys/default/2197.png b/toxygen/smileys/default/2197.png index b7f91c4..2b637fe 100644 Binary files a/toxygen/smileys/default/2197.png and b/toxygen/smileys/default/2197.png differ diff --git a/toxygen/smileys/default/2198.png b/toxygen/smileys/default/2198.png index e128d70..d868dd7 100644 Binary files a/toxygen/smileys/default/2198.png and b/toxygen/smileys/default/2198.png differ diff --git a/toxygen/smileys/default/2199.png b/toxygen/smileys/default/2199.png index 34cbf64..3775673 100644 Binary files a/toxygen/smileys/default/2199.png and b/toxygen/smileys/default/2199.png differ diff --git a/toxygen/smileys/default/21A9.png b/toxygen/smileys/default/21A9.png index d8a6c0b..9f9af80 100644 Binary files a/toxygen/smileys/default/21A9.png and b/toxygen/smileys/default/21A9.png differ diff --git a/toxygen/smileys/default/21AA.png b/toxygen/smileys/default/21AA.png index 588acf5..c13226b 100644 Binary files a/toxygen/smileys/default/21AA.png and b/toxygen/smileys/default/21AA.png differ diff --git a/toxygen/smileys/default/231A.png b/toxygen/smileys/default/231A.png index dbe2607..699dddd 100644 Binary files a/toxygen/smileys/default/231A.png and b/toxygen/smileys/default/231A.png differ diff --git a/toxygen/smileys/default/231B.png b/toxygen/smileys/default/231B.png index 060cf43..b69f1ed 100644 Binary files a/toxygen/smileys/default/231B.png and b/toxygen/smileys/default/231B.png differ diff --git a/toxygen/smileys/default/23E9.png b/toxygen/smileys/default/23E9.png index 4fc83f7..f4b575a 100644 Binary files a/toxygen/smileys/default/23E9.png and b/toxygen/smileys/default/23E9.png differ diff --git a/toxygen/smileys/default/23EA.png b/toxygen/smileys/default/23EA.png index 4909b06..557b09f 100644 Binary files a/toxygen/smileys/default/23EA.png and b/toxygen/smileys/default/23EA.png differ diff --git a/toxygen/smileys/default/23EB.png b/toxygen/smileys/default/23EB.png index 3240476..80b209b 100644 Binary files a/toxygen/smileys/default/23EB.png and b/toxygen/smileys/default/23EB.png differ diff --git a/toxygen/smileys/default/23EC.png b/toxygen/smileys/default/23EC.png index 9996c1a..36688b2 100644 Binary files a/toxygen/smileys/default/23EC.png and b/toxygen/smileys/default/23EC.png differ diff --git a/toxygen/smileys/default/23F0.png b/toxygen/smileys/default/23F0.png index 63485f6..c8ec471 100644 Binary files a/toxygen/smileys/default/23F0.png and b/toxygen/smileys/default/23F0.png differ diff --git a/toxygen/smileys/default/23F3.png b/toxygen/smileys/default/23F3.png index 0958429..eadb18c 100644 Binary files a/toxygen/smileys/default/23F3.png and b/toxygen/smileys/default/23F3.png differ diff --git a/toxygen/smileys/default/24C2.png b/toxygen/smileys/default/24C2.png index bccac5e..8af2206 100644 Binary files a/toxygen/smileys/default/24C2.png and b/toxygen/smileys/default/24C2.png differ diff --git a/toxygen/smileys/default/25AA.png b/toxygen/smileys/default/25AA.png index 54992ea..baed686 100644 Binary files a/toxygen/smileys/default/25AA.png and b/toxygen/smileys/default/25AA.png differ diff --git a/toxygen/smileys/default/25AB.png b/toxygen/smileys/default/25AB.png index a957fca..34a504f 100644 Binary files a/toxygen/smileys/default/25AB.png and b/toxygen/smileys/default/25AB.png differ diff --git a/toxygen/smileys/default/25B6.png b/toxygen/smileys/default/25B6.png index 9c291f6..7ffe84e 100644 Binary files a/toxygen/smileys/default/25B6.png and b/toxygen/smileys/default/25B6.png differ diff --git a/toxygen/smileys/default/25C0.png b/toxygen/smileys/default/25C0.png index 0ab4d16..ea2a965 100644 Binary files a/toxygen/smileys/default/25C0.png and b/toxygen/smileys/default/25C0.png differ diff --git a/toxygen/smileys/default/25FB.png b/toxygen/smileys/default/25FB.png index 02b39c8..1a9b1e4 100644 Binary files a/toxygen/smileys/default/25FB.png and b/toxygen/smileys/default/25FB.png differ diff --git a/toxygen/smileys/default/25FC.png b/toxygen/smileys/default/25FC.png index c1ba9c6..8ae60bf 100644 Binary files a/toxygen/smileys/default/25FC.png and b/toxygen/smileys/default/25FC.png differ diff --git a/toxygen/smileys/default/25FD.png b/toxygen/smileys/default/25FD.png index 0aab847..66144a8 100644 Binary files a/toxygen/smileys/default/25FD.png and b/toxygen/smileys/default/25FD.png differ diff --git a/toxygen/smileys/default/25FE.png b/toxygen/smileys/default/25FE.png index 1de985b..300b92d 100644 Binary files a/toxygen/smileys/default/25FE.png and b/toxygen/smileys/default/25FE.png differ diff --git a/toxygen/smileys/default/2600.png b/toxygen/smileys/default/2600.png index fcbfe56..ad91b05 100644 Binary files a/toxygen/smileys/default/2600.png and b/toxygen/smileys/default/2600.png differ diff --git a/toxygen/smileys/default/2601.png b/toxygen/smileys/default/2601.png index d1f979d..14ee8fd 100644 Binary files a/toxygen/smileys/default/2601.png and b/toxygen/smileys/default/2601.png differ diff --git a/toxygen/smileys/default/260E.png b/toxygen/smileys/default/260E.png index ef1b3c5..ae88c82 100644 Binary files a/toxygen/smileys/default/260E.png and b/toxygen/smileys/default/260E.png differ diff --git a/toxygen/smileys/default/2611.png b/toxygen/smileys/default/2611.png index c4b49d6..21f462c 100644 Binary files a/toxygen/smileys/default/2611.png and b/toxygen/smileys/default/2611.png differ diff --git a/toxygen/smileys/default/2614.png b/toxygen/smileys/default/2614.png index 2dad11e..154540c 100644 Binary files a/toxygen/smileys/default/2614.png and b/toxygen/smileys/default/2614.png differ diff --git a/toxygen/smileys/default/2615.png b/toxygen/smileys/default/2615.png index 944af22..f3ac6c9 100644 Binary files a/toxygen/smileys/default/2615.png and b/toxygen/smileys/default/2615.png differ diff --git a/toxygen/smileys/default/261D.png b/toxygen/smileys/default/261D.png index 8458b0e..caf5e7f 100644 Binary files a/toxygen/smileys/default/261D.png and b/toxygen/smileys/default/261D.png differ diff --git a/toxygen/smileys/default/263A.png b/toxygen/smileys/default/263A.png index 5db95a5..8a3409d 100644 Binary files a/toxygen/smileys/default/263A.png and b/toxygen/smileys/default/263A.png differ diff --git a/toxygen/smileys/default/2648.png b/toxygen/smileys/default/2648.png index 9d529e5..36ca321 100644 Binary files a/toxygen/smileys/default/2648.png and b/toxygen/smileys/default/2648.png differ diff --git a/toxygen/smileys/default/2649.png b/toxygen/smileys/default/2649.png index d67cb84..4e0687e 100644 Binary files a/toxygen/smileys/default/2649.png and b/toxygen/smileys/default/2649.png differ diff --git a/toxygen/smileys/default/264A.png b/toxygen/smileys/default/264A.png index 92fa0e7..1e131e0 100644 Binary files a/toxygen/smileys/default/264A.png and b/toxygen/smileys/default/264A.png differ diff --git a/toxygen/smileys/default/264B.png b/toxygen/smileys/default/264B.png index 0753593..b02cca6 100644 Binary files a/toxygen/smileys/default/264B.png and b/toxygen/smileys/default/264B.png differ diff --git a/toxygen/smileys/default/264C.png b/toxygen/smileys/default/264C.png index 7a44286..6354aa4 100644 Binary files a/toxygen/smileys/default/264C.png and b/toxygen/smileys/default/264C.png differ diff --git a/toxygen/smileys/default/264D.png b/toxygen/smileys/default/264D.png index f45f5f0..19cd5dc 100644 Binary files a/toxygen/smileys/default/264D.png and b/toxygen/smileys/default/264D.png differ diff --git a/toxygen/smileys/default/264E.png b/toxygen/smileys/default/264E.png index 80fa2eb..e000b39 100644 Binary files a/toxygen/smileys/default/264E.png and b/toxygen/smileys/default/264E.png differ diff --git a/toxygen/smileys/default/264F.png b/toxygen/smileys/default/264F.png index ad41780..82eb8eb 100644 Binary files a/toxygen/smileys/default/264F.png and b/toxygen/smileys/default/264F.png differ diff --git a/toxygen/smileys/default/2650.png b/toxygen/smileys/default/2650.png index a4cf9d8..2b4fa50 100644 Binary files a/toxygen/smileys/default/2650.png and b/toxygen/smileys/default/2650.png differ diff --git a/toxygen/smileys/default/2651.png b/toxygen/smileys/default/2651.png index 7e40bc3..ce713d8 100644 Binary files a/toxygen/smileys/default/2651.png and b/toxygen/smileys/default/2651.png differ diff --git a/toxygen/smileys/default/2652.png b/toxygen/smileys/default/2652.png index 09f6d3b..0032211 100644 Binary files a/toxygen/smileys/default/2652.png and b/toxygen/smileys/default/2652.png differ diff --git a/toxygen/smileys/default/2653.png b/toxygen/smileys/default/2653.png index a4da181..85c701a 100644 Binary files a/toxygen/smileys/default/2653.png and b/toxygen/smileys/default/2653.png differ diff --git a/toxygen/smileys/default/2660.png b/toxygen/smileys/default/2660.png index e2a9757..3ed0373 100644 Binary files a/toxygen/smileys/default/2660.png and b/toxygen/smileys/default/2660.png differ diff --git a/toxygen/smileys/default/2663.png b/toxygen/smileys/default/2663.png index 43b0f13..4dd8e0b 100644 Binary files a/toxygen/smileys/default/2663.png and b/toxygen/smileys/default/2663.png differ diff --git a/toxygen/smileys/default/2665.png b/toxygen/smileys/default/2665.png index 7fd68db..1088ec5 100644 Binary files a/toxygen/smileys/default/2665.png and b/toxygen/smileys/default/2665.png differ diff --git a/toxygen/smileys/default/2666.png b/toxygen/smileys/default/2666.png index e123db3..0fa97e6 100644 Binary files a/toxygen/smileys/default/2666.png and b/toxygen/smileys/default/2666.png differ diff --git a/toxygen/smileys/default/2668.png b/toxygen/smileys/default/2668.png index 6e148ea..244e954 100644 Binary files a/toxygen/smileys/default/2668.png and b/toxygen/smileys/default/2668.png differ diff --git a/toxygen/smileys/default/267B.png b/toxygen/smileys/default/267B.png index 4c7b51d..39e485d 100644 Binary files a/toxygen/smileys/default/267B.png and b/toxygen/smileys/default/267B.png differ diff --git a/toxygen/smileys/default/267F.png b/toxygen/smileys/default/267F.png index 1a33390..8e0341d 100644 Binary files a/toxygen/smileys/default/267F.png and b/toxygen/smileys/default/267F.png differ diff --git a/toxygen/smileys/default/2693.png b/toxygen/smileys/default/2693.png index f87253d..0a20950 100644 Binary files a/toxygen/smileys/default/2693.png and b/toxygen/smileys/default/2693.png differ diff --git a/toxygen/smileys/default/26A0.png b/toxygen/smileys/default/26A0.png index ec5d59f..da04fd6 100644 Binary files a/toxygen/smileys/default/26A0.png and b/toxygen/smileys/default/26A0.png differ diff --git a/toxygen/smileys/default/26A1.png b/toxygen/smileys/default/26A1.png index a753ef4..aa730a7 100644 Binary files a/toxygen/smileys/default/26A1.png and b/toxygen/smileys/default/26A1.png differ diff --git a/toxygen/smileys/default/26AA.png b/toxygen/smileys/default/26AA.png index d6dc51e..5a7d5c3 100644 Binary files a/toxygen/smileys/default/26AA.png and b/toxygen/smileys/default/26AA.png differ diff --git a/toxygen/smileys/default/26AB.png b/toxygen/smileys/default/26AB.png index d603714..4cf2098 100644 Binary files a/toxygen/smileys/default/26AB.png and b/toxygen/smileys/default/26AB.png differ diff --git a/toxygen/smileys/default/26BD.png b/toxygen/smileys/default/26BD.png index 27ff3f8..7bfd040 100644 Binary files a/toxygen/smileys/default/26BD.png and b/toxygen/smileys/default/26BD.png differ diff --git a/toxygen/smileys/default/26BE.png b/toxygen/smileys/default/26BE.png index aa929b6..ef49d55 100644 Binary files a/toxygen/smileys/default/26BE.png and b/toxygen/smileys/default/26BE.png differ diff --git a/toxygen/smileys/default/26C4.png b/toxygen/smileys/default/26C4.png index f7e509b..93bef58 100644 Binary files a/toxygen/smileys/default/26C4.png and b/toxygen/smileys/default/26C4.png differ diff --git a/toxygen/smileys/default/26C5.png b/toxygen/smileys/default/26C5.png index 8677043..eb04e5d 100644 Binary files a/toxygen/smileys/default/26C5.png and b/toxygen/smileys/default/26C5.png differ diff --git a/toxygen/smileys/default/26CE.png b/toxygen/smileys/default/26CE.png index 35c5af5..0ad227f 100644 Binary files a/toxygen/smileys/default/26CE.png and b/toxygen/smileys/default/26CE.png differ diff --git a/toxygen/smileys/default/26D4.png b/toxygen/smileys/default/26D4.png index dcfb49e..e28fada 100644 Binary files a/toxygen/smileys/default/26D4.png and b/toxygen/smileys/default/26D4.png differ diff --git a/toxygen/smileys/default/26EA.png b/toxygen/smileys/default/26EA.png index d606692..17727e0 100644 Binary files a/toxygen/smileys/default/26EA.png and b/toxygen/smileys/default/26EA.png differ diff --git a/toxygen/smileys/default/26F2.png b/toxygen/smileys/default/26F2.png index 2a86834..720ad23 100644 Binary files a/toxygen/smileys/default/26F2.png and b/toxygen/smileys/default/26F2.png differ diff --git a/toxygen/smileys/default/26F3.png b/toxygen/smileys/default/26F3.png index c51400b..50e4a27 100644 Binary files a/toxygen/smileys/default/26F3.png and b/toxygen/smileys/default/26F3.png differ diff --git a/toxygen/smileys/default/26F5.png b/toxygen/smileys/default/26F5.png index 5fd1cfb..4a5c029 100644 Binary files a/toxygen/smileys/default/26F5.png and b/toxygen/smileys/default/26F5.png differ diff --git a/toxygen/smileys/default/26FA.png b/toxygen/smileys/default/26FA.png index 053a098..516ad10 100644 Binary files a/toxygen/smileys/default/26FA.png and b/toxygen/smileys/default/26FA.png differ diff --git a/toxygen/smileys/default/26FD.png b/toxygen/smileys/default/26FD.png index fcf14e6..dbd528b 100644 Binary files a/toxygen/smileys/default/26FD.png and b/toxygen/smileys/default/26FD.png differ diff --git a/toxygen/smileys/default/2702.png b/toxygen/smileys/default/2702.png index 15ea13b..0822488 100644 Binary files a/toxygen/smileys/default/2702.png and b/toxygen/smileys/default/2702.png differ diff --git a/toxygen/smileys/default/2705.png b/toxygen/smileys/default/2705.png index 9c849f7..0f82df9 100644 Binary files a/toxygen/smileys/default/2705.png and b/toxygen/smileys/default/2705.png differ diff --git a/toxygen/smileys/default/2708.png b/toxygen/smileys/default/2708.png index c053b6a..509fa7b 100644 Binary files a/toxygen/smileys/default/2708.png and b/toxygen/smileys/default/2708.png differ diff --git a/toxygen/smileys/default/2709.png b/toxygen/smileys/default/2709.png index dfeac0a..4035754 100644 Binary files a/toxygen/smileys/default/2709.png and b/toxygen/smileys/default/2709.png differ diff --git a/toxygen/smileys/default/270A.png b/toxygen/smileys/default/270A.png index 51eefbb..43d7ca8 100644 Binary files a/toxygen/smileys/default/270A.png and b/toxygen/smileys/default/270A.png differ diff --git a/toxygen/smileys/default/270B.png b/toxygen/smileys/default/270B.png index 3f03100..984f829 100644 Binary files a/toxygen/smileys/default/270B.png and b/toxygen/smileys/default/270B.png differ diff --git a/toxygen/smileys/default/270C.png b/toxygen/smileys/default/270C.png index 437fc21..7fe482f 100644 Binary files a/toxygen/smileys/default/270C.png and b/toxygen/smileys/default/270C.png differ diff --git a/toxygen/smileys/default/270F.png b/toxygen/smileys/default/270F.png index 2aa3ee0..a86cf25 100644 Binary files a/toxygen/smileys/default/270F.png and b/toxygen/smileys/default/270F.png differ diff --git a/toxygen/smileys/default/2712.png b/toxygen/smileys/default/2712.png index 97c1072..cc6c6ab 100644 Binary files a/toxygen/smileys/default/2712.png and b/toxygen/smileys/default/2712.png differ diff --git a/toxygen/smileys/default/2714.png b/toxygen/smileys/default/2714.png index df3540f..b675396 100644 Binary files a/toxygen/smileys/default/2714.png and b/toxygen/smileys/default/2714.png differ diff --git a/toxygen/smileys/default/2716.png b/toxygen/smileys/default/2716.png index 041e6c2..7fac672 100644 Binary files a/toxygen/smileys/default/2716.png and b/toxygen/smileys/default/2716.png differ diff --git a/toxygen/smileys/default/2728.png b/toxygen/smileys/default/2728.png index 5e7c381..82aad35 100644 Binary files a/toxygen/smileys/default/2728.png and b/toxygen/smileys/default/2728.png differ diff --git a/toxygen/smileys/default/2733.png b/toxygen/smileys/default/2733.png index 334a066..d9b1f08 100644 Binary files a/toxygen/smileys/default/2733.png and b/toxygen/smileys/default/2733.png differ diff --git a/toxygen/smileys/default/2734.png b/toxygen/smileys/default/2734.png index 93a995f..f95730e 100644 Binary files a/toxygen/smileys/default/2734.png and b/toxygen/smileys/default/2734.png differ diff --git a/toxygen/smileys/default/2744.png b/toxygen/smileys/default/2744.png index bf4d09e..f88a35d 100644 Binary files a/toxygen/smileys/default/2744.png and b/toxygen/smileys/default/2744.png differ diff --git a/toxygen/smileys/default/2747.png b/toxygen/smileys/default/2747.png index 1eaf5d2..6179ee0 100644 Binary files a/toxygen/smileys/default/2747.png and b/toxygen/smileys/default/2747.png differ diff --git a/toxygen/smileys/default/274C.png b/toxygen/smileys/default/274C.png index 2bf2526..64036e1 100644 Binary files a/toxygen/smileys/default/274C.png and b/toxygen/smileys/default/274C.png differ diff --git a/toxygen/smileys/default/274E.png b/toxygen/smileys/default/274E.png index 8bbf629..9a337af 100644 Binary files a/toxygen/smileys/default/274E.png and b/toxygen/smileys/default/274E.png differ diff --git a/toxygen/smileys/default/2753.png b/toxygen/smileys/default/2753.png index fd4a4f8..303b2f5 100644 Binary files a/toxygen/smileys/default/2753.png and b/toxygen/smileys/default/2753.png differ diff --git a/toxygen/smileys/default/2754.png b/toxygen/smileys/default/2754.png index c1afcb2..ce83bb6 100644 Binary files a/toxygen/smileys/default/2754.png and b/toxygen/smileys/default/2754.png differ diff --git a/toxygen/smileys/default/2755.png b/toxygen/smileys/default/2755.png index 80cc60e..74b34e0 100644 Binary files a/toxygen/smileys/default/2755.png and b/toxygen/smileys/default/2755.png differ diff --git a/toxygen/smileys/default/2757.png b/toxygen/smileys/default/2757.png index 5ce95cb..0932319 100644 Binary files a/toxygen/smileys/default/2757.png and b/toxygen/smileys/default/2757.png differ diff --git a/toxygen/smileys/default/2764.png b/toxygen/smileys/default/2764.png index 20b145d..db9de9e 100644 Binary files a/toxygen/smileys/default/2764.png and b/toxygen/smileys/default/2764.png differ diff --git a/toxygen/smileys/default/2795.png b/toxygen/smileys/default/2795.png index fedda3d..33bb432 100644 Binary files a/toxygen/smileys/default/2795.png and b/toxygen/smileys/default/2795.png differ diff --git a/toxygen/smileys/default/2796.png b/toxygen/smileys/default/2796.png index 0341ac4..ca89edf 100644 Binary files a/toxygen/smileys/default/2796.png and b/toxygen/smileys/default/2796.png differ diff --git a/toxygen/smileys/default/2797.png b/toxygen/smileys/default/2797.png index d820089..04ca489 100644 Binary files a/toxygen/smileys/default/2797.png and b/toxygen/smileys/default/2797.png differ diff --git a/toxygen/smileys/default/27A1.png b/toxygen/smileys/default/27A1.png index f0dd357..36fad95 100644 Binary files a/toxygen/smileys/default/27A1.png and b/toxygen/smileys/default/27A1.png differ diff --git a/toxygen/smileys/default/27B0.png b/toxygen/smileys/default/27B0.png index 3ce1cc0..8460f2e 100644 Binary files a/toxygen/smileys/default/27B0.png and b/toxygen/smileys/default/27B0.png differ diff --git a/toxygen/smileys/default/27BF.png b/toxygen/smileys/default/27BF.png index 798387c..1245f99 100644 Binary files a/toxygen/smileys/default/27BF.png and b/toxygen/smileys/default/27BF.png differ diff --git a/toxygen/smileys/default/2934.png b/toxygen/smileys/default/2934.png index 3517e59..7b52ecd 100644 Binary files a/toxygen/smileys/default/2934.png and b/toxygen/smileys/default/2934.png differ diff --git a/toxygen/smileys/default/2935.png b/toxygen/smileys/default/2935.png index 857caf4..0aba0d0 100644 Binary files a/toxygen/smileys/default/2935.png and b/toxygen/smileys/default/2935.png differ diff --git a/toxygen/smileys/default/2B05.png b/toxygen/smileys/default/2B05.png index 368e2fa..8bacdda 100644 Binary files a/toxygen/smileys/default/2B05.png and b/toxygen/smileys/default/2B05.png differ diff --git a/toxygen/smileys/default/2B06.png b/toxygen/smileys/default/2B06.png index 56bb954..b394430 100644 Binary files a/toxygen/smileys/default/2B06.png and b/toxygen/smileys/default/2B06.png differ diff --git a/toxygen/smileys/default/2B07.png b/toxygen/smileys/default/2B07.png index ed86d82..bc9532a 100644 Binary files a/toxygen/smileys/default/2B07.png and b/toxygen/smileys/default/2B07.png differ diff --git a/toxygen/smileys/default/2B1B.png b/toxygen/smileys/default/2B1B.png index 9f51c6b..6a833f5 100644 Binary files a/toxygen/smileys/default/2B1B.png and b/toxygen/smileys/default/2B1B.png differ diff --git a/toxygen/smileys/default/2B1C.png b/toxygen/smileys/default/2B1C.png index 25ce49a..94275fd 100644 Binary files a/toxygen/smileys/default/2B1C.png and b/toxygen/smileys/default/2B1C.png differ diff --git a/toxygen/smileys/default/2B50.png b/toxygen/smileys/default/2B50.png index d08be34..358da2b 100644 Binary files a/toxygen/smileys/default/2B50.png and b/toxygen/smileys/default/2B50.png differ diff --git a/toxygen/smileys/default/2B55.png b/toxygen/smileys/default/2B55.png index bb71bcc..ff62f1b 100644 Binary files a/toxygen/smileys/default/2B55.png and b/toxygen/smileys/default/2B55.png differ diff --git a/toxygen/smileys/default/3030.png b/toxygen/smileys/default/3030.png index 9a8d53a..aeb952e 100644 Binary files a/toxygen/smileys/default/3030.png and b/toxygen/smileys/default/3030.png differ diff --git a/toxygen/smileys/default/303D.png b/toxygen/smileys/default/303D.png index 09639ea..6701f76 100644 Binary files a/toxygen/smileys/default/303D.png and b/toxygen/smileys/default/303D.png differ diff --git a/toxygen/smileys/default/D83CDC04.png b/toxygen/smileys/default/D83CDC04.png index fb1f1f6..6521d64 100644 Binary files a/toxygen/smileys/default/D83CDC04.png and b/toxygen/smileys/default/D83CDC04.png differ diff --git a/toxygen/smileys/default/D83CDCCF.png b/toxygen/smileys/default/D83CDCCF.png index 3ea5b82..754d3c2 100644 Binary files a/toxygen/smileys/default/D83CDCCF.png and b/toxygen/smileys/default/D83CDCCF.png differ diff --git a/toxygen/smileys/default/D83CDD70.png b/toxygen/smileys/default/D83CDD70.png index 75ea41b..dd82624 100644 Binary files a/toxygen/smileys/default/D83CDD70.png and b/toxygen/smileys/default/D83CDD70.png differ diff --git a/toxygen/smileys/default/D83CDD71.png b/toxygen/smileys/default/D83CDD71.png index 13c53fc..84f20f3 100644 Binary files a/toxygen/smileys/default/D83CDD71.png and b/toxygen/smileys/default/D83CDD71.png differ diff --git a/toxygen/smileys/default/D83CDD7E.png b/toxygen/smileys/default/D83CDD7E.png index eb5ebf8..9a56329 100644 Binary files a/toxygen/smileys/default/D83CDD7E.png and b/toxygen/smileys/default/D83CDD7E.png differ diff --git a/toxygen/smileys/default/D83CDD7F.png b/toxygen/smileys/default/D83CDD7F.png index c8f6432..aa5dca1 100644 Binary files a/toxygen/smileys/default/D83CDD7F.png and b/toxygen/smileys/default/D83CDD7F.png differ diff --git a/toxygen/smileys/default/D83CDD8E.png b/toxygen/smileys/default/D83CDD8E.png index f615013..3e3a43e 100644 Binary files a/toxygen/smileys/default/D83CDD8E.png and b/toxygen/smileys/default/D83CDD8E.png differ diff --git a/toxygen/smileys/default/D83CDD91.png b/toxygen/smileys/default/D83CDD91.png index 09b02e0..2f37aac 100644 Binary files a/toxygen/smileys/default/D83CDD91.png and b/toxygen/smileys/default/D83CDD91.png differ diff --git a/toxygen/smileys/default/D83CDD92.png b/toxygen/smileys/default/D83CDD92.png index fefcd76..6727be3 100644 Binary files a/toxygen/smileys/default/D83CDD92.png and b/toxygen/smileys/default/D83CDD92.png differ diff --git a/toxygen/smileys/default/D83CDD93.png b/toxygen/smileys/default/D83CDD93.png index 2294126..47a754e 100644 Binary files a/toxygen/smileys/default/D83CDD93.png and b/toxygen/smileys/default/D83CDD93.png differ diff --git a/toxygen/smileys/default/D83CDD94.png b/toxygen/smileys/default/D83CDD94.png index 5681f7a..0b710e1 100644 Binary files a/toxygen/smileys/default/D83CDD94.png and b/toxygen/smileys/default/D83CDD94.png differ diff --git a/toxygen/smileys/default/D83CDD95.png b/toxygen/smileys/default/D83CDD95.png index 4b2176b..25149a7 100644 Binary files a/toxygen/smileys/default/D83CDD95.png and b/toxygen/smileys/default/D83CDD95.png differ diff --git a/toxygen/smileys/default/D83CDD96.png b/toxygen/smileys/default/D83CDD96.png index 1835eec..14d0d3f 100644 Binary files a/toxygen/smileys/default/D83CDD96.png and b/toxygen/smileys/default/D83CDD96.png differ diff --git a/toxygen/smileys/default/D83CDD97.png b/toxygen/smileys/default/D83CDD97.png index 4c5e3dc..8ffef12 100644 Binary files a/toxygen/smileys/default/D83CDD97.png and b/toxygen/smileys/default/D83CDD97.png differ diff --git a/toxygen/smileys/default/D83CDD98.png b/toxygen/smileys/default/D83CDD98.png index 4cd5f0c..7288cbb 100644 Binary files a/toxygen/smileys/default/D83CDD98.png and b/toxygen/smileys/default/D83CDD98.png differ diff --git a/toxygen/smileys/default/D83CDD99.png b/toxygen/smileys/default/D83CDD99.png index ab809ad..6d7180d 100644 Binary files a/toxygen/smileys/default/D83CDD99.png and b/toxygen/smileys/default/D83CDD99.png differ diff --git a/toxygen/smileys/default/D83CDD9A.png b/toxygen/smileys/default/D83CDD9A.png index 91e0db3..7c34f12 100644 Binary files a/toxygen/smileys/default/D83CDD9A.png and b/toxygen/smileys/default/D83CDD9A.png differ diff --git a/toxygen/smileys/default/D83CDE01.png b/toxygen/smileys/default/D83CDE01.png index f3aed09..93c9689 100644 Binary files a/toxygen/smileys/default/D83CDE01.png and b/toxygen/smileys/default/D83CDE01.png differ diff --git a/toxygen/smileys/default/D83CDF00.png b/toxygen/smileys/default/D83CDF00.png index f920a25..bec5f14 100644 Binary files a/toxygen/smileys/default/D83CDF00.png and b/toxygen/smileys/default/D83CDF00.png differ diff --git a/toxygen/smileys/default/D83CDF01.png b/toxygen/smileys/default/D83CDF01.png index a834fd3..ac39b56 100644 Binary files a/toxygen/smileys/default/D83CDF01.png and b/toxygen/smileys/default/D83CDF01.png differ diff --git a/toxygen/smileys/default/D83CDF02.png b/toxygen/smileys/default/D83CDF02.png index c668d0d..c4c1867 100644 Binary files a/toxygen/smileys/default/D83CDF02.png and b/toxygen/smileys/default/D83CDF02.png differ diff --git a/toxygen/smileys/default/D83CDF03.png b/toxygen/smileys/default/D83CDF03.png index 67e776a..cfbe0c0 100644 Binary files a/toxygen/smileys/default/D83CDF03.png and b/toxygen/smileys/default/D83CDF03.png differ diff --git a/toxygen/smileys/default/D83CDF04.png b/toxygen/smileys/default/D83CDF04.png index 8b5e8fe..fdc05fe 100644 Binary files a/toxygen/smileys/default/D83CDF04.png and b/toxygen/smileys/default/D83CDF04.png differ diff --git a/toxygen/smileys/default/D83CDF05.png b/toxygen/smileys/default/D83CDF05.png index 8a9b125..4ee1bf4 100644 Binary files a/toxygen/smileys/default/D83CDF05.png and b/toxygen/smileys/default/D83CDF05.png differ diff --git a/toxygen/smileys/default/D83CDF06.png b/toxygen/smileys/default/D83CDF06.png index f456a1d..47856c7 100644 Binary files a/toxygen/smileys/default/D83CDF06.png and b/toxygen/smileys/default/D83CDF06.png differ diff --git a/toxygen/smileys/default/D83CDF07.png b/toxygen/smileys/default/D83CDF07.png index 0627f7d..235c6bb 100644 Binary files a/toxygen/smileys/default/D83CDF07.png and b/toxygen/smileys/default/D83CDF07.png differ diff --git a/toxygen/smileys/default/D83CDF08.png b/toxygen/smileys/default/D83CDF08.png index 0f9f281..428c842 100644 Binary files a/toxygen/smileys/default/D83CDF08.png and b/toxygen/smileys/default/D83CDF08.png differ diff --git a/toxygen/smileys/default/D83CDF09.png b/toxygen/smileys/default/D83CDF09.png index 3075763..ebe76dc 100644 Binary files a/toxygen/smileys/default/D83CDF09.png and b/toxygen/smileys/default/D83CDF09.png differ diff --git a/toxygen/smileys/default/D83CDF0A.png b/toxygen/smileys/default/D83CDF0A.png index fb65924..f844fde 100644 Binary files a/toxygen/smileys/default/D83CDF0A.png and b/toxygen/smileys/default/D83CDF0A.png differ diff --git a/toxygen/smileys/default/D83CDF0B.png b/toxygen/smileys/default/D83CDF0B.png index 362aea0..76fecab 100644 Binary files a/toxygen/smileys/default/D83CDF0B.png and b/toxygen/smileys/default/D83CDF0B.png differ diff --git a/toxygen/smileys/default/D83CDF0C.png b/toxygen/smileys/default/D83CDF0C.png index 222ef58..8f50380 100644 Binary files a/toxygen/smileys/default/D83CDF0C.png and b/toxygen/smileys/default/D83CDF0C.png differ diff --git a/toxygen/smileys/default/D83CDF0D.png b/toxygen/smileys/default/D83CDF0D.png index b76776d..ab306db 100644 Binary files a/toxygen/smileys/default/D83CDF0D.png and b/toxygen/smileys/default/D83CDF0D.png differ diff --git a/toxygen/smileys/default/D83CDF0E.png b/toxygen/smileys/default/D83CDF0E.png index 8a21855..3ccaf4f 100644 Binary files a/toxygen/smileys/default/D83CDF0E.png and b/toxygen/smileys/default/D83CDF0E.png differ diff --git a/toxygen/smileys/default/D83CDF0F.png b/toxygen/smileys/default/D83CDF0F.png index 3cb44be..5d3be08 100644 Binary files a/toxygen/smileys/default/D83CDF0F.png and b/toxygen/smileys/default/D83CDF0F.png differ diff --git a/toxygen/smileys/default/D83CDF10.png b/toxygen/smileys/default/D83CDF10.png index 1b50e85..b5f35fb 100644 Binary files a/toxygen/smileys/default/D83CDF10.png and b/toxygen/smileys/default/D83CDF10.png differ diff --git a/toxygen/smileys/default/D83CDF11.png b/toxygen/smileys/default/D83CDF11.png index 031d83f..078260d 100644 Binary files a/toxygen/smileys/default/D83CDF11.png and b/toxygen/smileys/default/D83CDF11.png differ diff --git a/toxygen/smileys/default/D83CDF12.png b/toxygen/smileys/default/D83CDF12.png index 1833939..d0a0f72 100644 Binary files a/toxygen/smileys/default/D83CDF12.png and b/toxygen/smileys/default/D83CDF12.png differ diff --git a/toxygen/smileys/default/D83CDF13.png b/toxygen/smileys/default/D83CDF13.png index 37c2f24..2c72896 100644 Binary files a/toxygen/smileys/default/D83CDF13.png and b/toxygen/smileys/default/D83CDF13.png differ diff --git a/toxygen/smileys/default/D83CDF14.png b/toxygen/smileys/default/D83CDF14.png index 68e1e8a..66696d8 100644 Binary files a/toxygen/smileys/default/D83CDF14.png and b/toxygen/smileys/default/D83CDF14.png differ diff --git a/toxygen/smileys/default/D83CDF15.png b/toxygen/smileys/default/D83CDF15.png index 8a91553..ff5c8e0 100644 Binary files a/toxygen/smileys/default/D83CDF15.png and b/toxygen/smileys/default/D83CDF15.png differ diff --git a/toxygen/smileys/default/D83CDF16.png b/toxygen/smileys/default/D83CDF16.png index efcf233..63734dd 100644 Binary files a/toxygen/smileys/default/D83CDF16.png and b/toxygen/smileys/default/D83CDF16.png differ diff --git a/toxygen/smileys/default/D83CDF17.png b/toxygen/smileys/default/D83CDF17.png index 18e5714..97e3de6 100644 Binary files a/toxygen/smileys/default/D83CDF17.png and b/toxygen/smileys/default/D83CDF17.png differ diff --git a/toxygen/smileys/default/D83CDF18.png b/toxygen/smileys/default/D83CDF18.png index eb66d26..13f8d9c 100644 Binary files a/toxygen/smileys/default/D83CDF18.png and b/toxygen/smileys/default/D83CDF18.png differ diff --git a/toxygen/smileys/default/D83CDF19.png b/toxygen/smileys/default/D83CDF19.png index 6092dfa..4443ab3 100644 Binary files a/toxygen/smileys/default/D83CDF19.png and b/toxygen/smileys/default/D83CDF19.png differ diff --git a/toxygen/smileys/default/D83CDF1A.png b/toxygen/smileys/default/D83CDF1A.png index edfbed2..48cf54e 100644 Binary files a/toxygen/smileys/default/D83CDF1A.png and b/toxygen/smileys/default/D83CDF1A.png differ diff --git a/toxygen/smileys/default/D83CDF1B.png b/toxygen/smileys/default/D83CDF1B.png index 42516ba..3f93634 100644 Binary files a/toxygen/smileys/default/D83CDF1B.png and b/toxygen/smileys/default/D83CDF1B.png differ diff --git a/toxygen/smileys/default/D83CDF1C.png b/toxygen/smileys/default/D83CDF1C.png index 048e306..a57bf54 100644 Binary files a/toxygen/smileys/default/D83CDF1C.png and b/toxygen/smileys/default/D83CDF1C.png differ diff --git a/toxygen/smileys/default/D83CDF1D.png b/toxygen/smileys/default/D83CDF1D.png index 3c2f76a..c982949 100644 Binary files a/toxygen/smileys/default/D83CDF1D.png and b/toxygen/smileys/default/D83CDF1D.png differ diff --git a/toxygen/smileys/default/D83CDF1E.png b/toxygen/smileys/default/D83CDF1E.png index 888b5c9..a78f6b1 100644 Binary files a/toxygen/smileys/default/D83CDF1E.png and b/toxygen/smileys/default/D83CDF1E.png differ diff --git a/toxygen/smileys/default/D83CDF1F.png b/toxygen/smileys/default/D83CDF1F.png index 1350976..a5aa959 100644 Binary files a/toxygen/smileys/default/D83CDF1F.png and b/toxygen/smileys/default/D83CDF1F.png differ diff --git a/toxygen/smileys/default/D83CDF20.png b/toxygen/smileys/default/D83CDF20.png index ea8ff38..502a017 100644 Binary files a/toxygen/smileys/default/D83CDF20.png and b/toxygen/smileys/default/D83CDF20.png differ diff --git a/toxygen/smileys/default/D83CDF30.png b/toxygen/smileys/default/D83CDF30.png index 37e573e..ca0e78a 100644 Binary files a/toxygen/smileys/default/D83CDF30.png and b/toxygen/smileys/default/D83CDF30.png differ diff --git a/toxygen/smileys/default/D83CDF31.png b/toxygen/smileys/default/D83CDF31.png index 036d056..e2a1224 100644 Binary files a/toxygen/smileys/default/D83CDF31.png and b/toxygen/smileys/default/D83CDF31.png differ diff --git a/toxygen/smileys/default/D83CDF32.png b/toxygen/smileys/default/D83CDF32.png index d0658c0..ea51b2b 100644 Binary files a/toxygen/smileys/default/D83CDF32.png and b/toxygen/smileys/default/D83CDF32.png differ diff --git a/toxygen/smileys/default/D83CDF33.png b/toxygen/smileys/default/D83CDF33.png index d9130ec..25ad311 100644 Binary files a/toxygen/smileys/default/D83CDF33.png and b/toxygen/smileys/default/D83CDF33.png differ diff --git a/toxygen/smileys/default/D83CDF34.png b/toxygen/smileys/default/D83CDF34.png index 60a8055..5d1fc87 100644 Binary files a/toxygen/smileys/default/D83CDF34.png and b/toxygen/smileys/default/D83CDF34.png differ diff --git a/toxygen/smileys/default/D83CDF35.png b/toxygen/smileys/default/D83CDF35.png index d72047e..d9f1ebe 100644 Binary files a/toxygen/smileys/default/D83CDF35.png and b/toxygen/smileys/default/D83CDF35.png differ diff --git a/toxygen/smileys/default/D83CDF37.png b/toxygen/smileys/default/D83CDF37.png index 59a7b43..58f4f4c 100644 Binary files a/toxygen/smileys/default/D83CDF37.png and b/toxygen/smileys/default/D83CDF37.png differ diff --git a/toxygen/smileys/default/D83CDF38.png b/toxygen/smileys/default/D83CDF38.png index ab096d3..3e76c62 100644 Binary files a/toxygen/smileys/default/D83CDF38.png and b/toxygen/smileys/default/D83CDF38.png differ diff --git a/toxygen/smileys/default/D83CDF39.png b/toxygen/smileys/default/D83CDF39.png index 5c285fa..aec58de 100644 Binary files a/toxygen/smileys/default/D83CDF39.png and b/toxygen/smileys/default/D83CDF39.png differ diff --git a/toxygen/smileys/default/D83CDF3A.png b/toxygen/smileys/default/D83CDF3A.png index 6058a37..1b83115 100644 Binary files a/toxygen/smileys/default/D83CDF3A.png and b/toxygen/smileys/default/D83CDF3A.png differ diff --git a/toxygen/smileys/default/D83CDF3B.png b/toxygen/smileys/default/D83CDF3B.png index 6ff1d5d..97da792 100644 Binary files a/toxygen/smileys/default/D83CDF3B.png and b/toxygen/smileys/default/D83CDF3B.png differ diff --git a/toxygen/smileys/default/D83CDF3C.png b/toxygen/smileys/default/D83CDF3C.png index 18e7026..cc737e7 100644 Binary files a/toxygen/smileys/default/D83CDF3C.png and b/toxygen/smileys/default/D83CDF3C.png differ diff --git a/toxygen/smileys/default/D83CDF3D.png b/toxygen/smileys/default/D83CDF3D.png index 853a69f..648a283 100644 Binary files a/toxygen/smileys/default/D83CDF3D.png and b/toxygen/smileys/default/D83CDF3D.png differ diff --git a/toxygen/smileys/default/D83CDF3E.png b/toxygen/smileys/default/D83CDF3E.png index e63cc58..ecbf4cd 100644 Binary files a/toxygen/smileys/default/D83CDF3E.png and b/toxygen/smileys/default/D83CDF3E.png differ diff --git a/toxygen/smileys/default/D83CDF3F.png b/toxygen/smileys/default/D83CDF3F.png index 789498b..dd5399e 100644 Binary files a/toxygen/smileys/default/D83CDF3F.png and b/toxygen/smileys/default/D83CDF3F.png differ diff --git a/toxygen/smileys/default/D83CDF40.png b/toxygen/smileys/default/D83CDF40.png index 9699a95..86ac7ed 100644 Binary files a/toxygen/smileys/default/D83CDF40.png and b/toxygen/smileys/default/D83CDF40.png differ diff --git a/toxygen/smileys/default/D83CDF41.png b/toxygen/smileys/default/D83CDF41.png index a2876b5..e2a9cdd 100644 Binary files a/toxygen/smileys/default/D83CDF41.png and b/toxygen/smileys/default/D83CDF41.png differ diff --git a/toxygen/smileys/default/D83CDF42.png b/toxygen/smileys/default/D83CDF42.png index d2b2b31..640daa0 100644 Binary files a/toxygen/smileys/default/D83CDF42.png and b/toxygen/smileys/default/D83CDF42.png differ diff --git a/toxygen/smileys/default/D83CDF43.png b/toxygen/smileys/default/D83CDF43.png index 0be4af5..94773f8 100644 Binary files a/toxygen/smileys/default/D83CDF43.png and b/toxygen/smileys/default/D83CDF43.png differ diff --git a/toxygen/smileys/default/D83CDF44.png b/toxygen/smileys/default/D83CDF44.png index 73218b9..f1114e7 100644 Binary files a/toxygen/smileys/default/D83CDF44.png and b/toxygen/smileys/default/D83CDF44.png differ diff --git a/toxygen/smileys/default/D83CDF45.png b/toxygen/smileys/default/D83CDF45.png index 4b3fae0..d11e096 100644 Binary files a/toxygen/smileys/default/D83CDF45.png and b/toxygen/smileys/default/D83CDF45.png differ diff --git a/toxygen/smileys/default/D83CDF46.png b/toxygen/smileys/default/D83CDF46.png index cce4962..a0ea6fc 100644 Binary files a/toxygen/smileys/default/D83CDF46.png and b/toxygen/smileys/default/D83CDF46.png differ diff --git a/toxygen/smileys/default/D83CDF47.png b/toxygen/smileys/default/D83CDF47.png index 05ba907..ffe08fe 100644 Binary files a/toxygen/smileys/default/D83CDF47.png and b/toxygen/smileys/default/D83CDF47.png differ diff --git a/toxygen/smileys/default/D83CDF48.png b/toxygen/smileys/default/D83CDF48.png index 3e50ffc..dd86e85 100644 Binary files a/toxygen/smileys/default/D83CDF48.png and b/toxygen/smileys/default/D83CDF48.png differ diff --git a/toxygen/smileys/default/D83CDF49.png b/toxygen/smileys/default/D83CDF49.png index 1110ede..45f804c 100644 Binary files a/toxygen/smileys/default/D83CDF49.png and b/toxygen/smileys/default/D83CDF49.png differ diff --git a/toxygen/smileys/default/D83CDF4A.png b/toxygen/smileys/default/D83CDF4A.png index 9747153..7b3689a 100644 Binary files a/toxygen/smileys/default/D83CDF4A.png and b/toxygen/smileys/default/D83CDF4A.png differ diff --git a/toxygen/smileys/default/D83CDF4B.png b/toxygen/smileys/default/D83CDF4B.png index f88a92e..3fa9c85 100644 Binary files a/toxygen/smileys/default/D83CDF4B.png and b/toxygen/smileys/default/D83CDF4B.png differ diff --git a/toxygen/smileys/default/D83CDF4C.png b/toxygen/smileys/default/D83CDF4C.png index 8843766..700ff44 100644 Binary files a/toxygen/smileys/default/D83CDF4C.png and b/toxygen/smileys/default/D83CDF4C.png differ diff --git a/toxygen/smileys/default/D83CDF4D.png b/toxygen/smileys/default/D83CDF4D.png index 7e96d5f..9f1070e 100644 Binary files a/toxygen/smileys/default/D83CDF4D.png and b/toxygen/smileys/default/D83CDF4D.png differ diff --git a/toxygen/smileys/default/D83CDF4E.png b/toxygen/smileys/default/D83CDF4E.png index 73a3174..e360df0 100644 Binary files a/toxygen/smileys/default/D83CDF4E.png and b/toxygen/smileys/default/D83CDF4E.png differ diff --git a/toxygen/smileys/default/D83CDF4F.png b/toxygen/smileys/default/D83CDF4F.png index 9dec886..4f42927 100644 Binary files a/toxygen/smileys/default/D83CDF4F.png and b/toxygen/smileys/default/D83CDF4F.png differ diff --git a/toxygen/smileys/default/D83CDF50.png b/toxygen/smileys/default/D83CDF50.png index b56380f..436b580 100644 Binary files a/toxygen/smileys/default/D83CDF50.png and b/toxygen/smileys/default/D83CDF50.png differ diff --git a/toxygen/smileys/default/D83CDF51.png b/toxygen/smileys/default/D83CDF51.png index df81f72..677749f 100644 Binary files a/toxygen/smileys/default/D83CDF51.png and b/toxygen/smileys/default/D83CDF51.png differ diff --git a/toxygen/smileys/default/D83CDF52.png b/toxygen/smileys/default/D83CDF52.png index 262cd7a..3069b83 100644 Binary files a/toxygen/smileys/default/D83CDF52.png and b/toxygen/smileys/default/D83CDF52.png differ diff --git a/toxygen/smileys/default/D83CDF53.png b/toxygen/smileys/default/D83CDF53.png index 5438131..eeb27c8 100644 Binary files a/toxygen/smileys/default/D83CDF53.png and b/toxygen/smileys/default/D83CDF53.png differ diff --git a/toxygen/smileys/default/D83CDF54.png b/toxygen/smileys/default/D83CDF54.png index f5dc18f..8065a3e 100644 Binary files a/toxygen/smileys/default/D83CDF54.png and b/toxygen/smileys/default/D83CDF54.png differ diff --git a/toxygen/smileys/default/D83CDF55.png b/toxygen/smileys/default/D83CDF55.png index d3a43de..5cb9566 100644 Binary files a/toxygen/smileys/default/D83CDF55.png and b/toxygen/smileys/default/D83CDF55.png differ diff --git a/toxygen/smileys/default/D83CDF56.png b/toxygen/smileys/default/D83CDF56.png index 3cae88e..2c9d393 100644 Binary files a/toxygen/smileys/default/D83CDF56.png and b/toxygen/smileys/default/D83CDF56.png differ diff --git a/toxygen/smileys/default/D83CDF57.png b/toxygen/smileys/default/D83CDF57.png index fafc625..d21ea0d 100644 Binary files a/toxygen/smileys/default/D83CDF57.png and b/toxygen/smileys/default/D83CDF57.png differ diff --git a/toxygen/smileys/default/D83CDF58.png b/toxygen/smileys/default/D83CDF58.png index 5e40d1b..948a08d 100644 Binary files a/toxygen/smileys/default/D83CDF58.png and b/toxygen/smileys/default/D83CDF58.png differ diff --git a/toxygen/smileys/default/D83CDF59.png b/toxygen/smileys/default/D83CDF59.png index 9c72e3c..61ab47a 100644 Binary files a/toxygen/smileys/default/D83CDF59.png and b/toxygen/smileys/default/D83CDF59.png differ diff --git a/toxygen/smileys/default/D83CDF5A.png b/toxygen/smileys/default/D83CDF5A.png index e9e4f2e..6cb3253 100644 Binary files a/toxygen/smileys/default/D83CDF5A.png and b/toxygen/smileys/default/D83CDF5A.png differ diff --git a/toxygen/smileys/default/D83CDF5B.png b/toxygen/smileys/default/D83CDF5B.png index a6808e1..0a79679 100644 Binary files a/toxygen/smileys/default/D83CDF5B.png and b/toxygen/smileys/default/D83CDF5B.png differ diff --git a/toxygen/smileys/default/D83CDF5C.png b/toxygen/smileys/default/D83CDF5C.png index 3d94196..12fa5e9 100644 Binary files a/toxygen/smileys/default/D83CDF5C.png and b/toxygen/smileys/default/D83CDF5C.png differ diff --git a/toxygen/smileys/default/D83CDF5D.png b/toxygen/smileys/default/D83CDF5D.png index 7b67c2f..f76f82a 100644 Binary files a/toxygen/smileys/default/D83CDF5D.png and b/toxygen/smileys/default/D83CDF5D.png differ diff --git a/toxygen/smileys/default/D83CDF5E.png b/toxygen/smileys/default/D83CDF5E.png index 9a99501..281ddda 100644 Binary files a/toxygen/smileys/default/D83CDF5E.png and b/toxygen/smileys/default/D83CDF5E.png differ diff --git a/toxygen/smileys/default/D83CDF5F.png b/toxygen/smileys/default/D83CDF5F.png index 6b0d1cf..0b4ca04 100644 Binary files a/toxygen/smileys/default/D83CDF5F.png and b/toxygen/smileys/default/D83CDF5F.png differ diff --git a/toxygen/smileys/default/D83CDF60.png b/toxygen/smileys/default/D83CDF60.png index 27ab69e..d25bedc 100644 Binary files a/toxygen/smileys/default/D83CDF60.png and b/toxygen/smileys/default/D83CDF60.png differ diff --git a/toxygen/smileys/default/D83CDF61.png b/toxygen/smileys/default/D83CDF61.png index edb01f7..f8a2280 100644 Binary files a/toxygen/smileys/default/D83CDF61.png and b/toxygen/smileys/default/D83CDF61.png differ diff --git a/toxygen/smileys/default/D83CDF62.png b/toxygen/smileys/default/D83CDF62.png index ce1b492..62a24bc 100644 Binary files a/toxygen/smileys/default/D83CDF62.png and b/toxygen/smileys/default/D83CDF62.png differ diff --git a/toxygen/smileys/default/D83CDF63.png b/toxygen/smileys/default/D83CDF63.png index 9cb9907..361eb81 100644 Binary files a/toxygen/smileys/default/D83CDF63.png and b/toxygen/smileys/default/D83CDF63.png differ diff --git a/toxygen/smileys/default/D83CDF64.png b/toxygen/smileys/default/D83CDF64.png index 3aa20b7..5bd6768 100644 Binary files a/toxygen/smileys/default/D83CDF64.png and b/toxygen/smileys/default/D83CDF64.png differ diff --git a/toxygen/smileys/default/D83CDF65.png b/toxygen/smileys/default/D83CDF65.png index a8d746f..a480926 100644 Binary files a/toxygen/smileys/default/D83CDF65.png and b/toxygen/smileys/default/D83CDF65.png differ diff --git a/toxygen/smileys/default/D83CDF66.png b/toxygen/smileys/default/D83CDF66.png index 7e6e8ab..7d67e8e 100644 Binary files a/toxygen/smileys/default/D83CDF66.png and b/toxygen/smileys/default/D83CDF66.png differ diff --git a/toxygen/smileys/default/D83CDF67.png b/toxygen/smileys/default/D83CDF67.png index e64baba..b88025d 100644 Binary files a/toxygen/smileys/default/D83CDF67.png and b/toxygen/smileys/default/D83CDF67.png differ diff --git a/toxygen/smileys/default/D83CDF68.png b/toxygen/smileys/default/D83CDF68.png index 0e23805..429f44e 100644 Binary files a/toxygen/smileys/default/D83CDF68.png and b/toxygen/smileys/default/D83CDF68.png differ diff --git a/toxygen/smileys/default/D83CDF69.png b/toxygen/smileys/default/D83CDF69.png index de6d759..54efe4f 100644 Binary files a/toxygen/smileys/default/D83CDF69.png and b/toxygen/smileys/default/D83CDF69.png differ diff --git a/toxygen/smileys/default/D83CDF6A.png b/toxygen/smileys/default/D83CDF6A.png index cd6c10a..a739508 100644 Binary files a/toxygen/smileys/default/D83CDF6A.png and b/toxygen/smileys/default/D83CDF6A.png differ diff --git a/toxygen/smileys/default/D83CDF6B.png b/toxygen/smileys/default/D83CDF6B.png index 73ad91c..b16a6e0 100644 Binary files a/toxygen/smileys/default/D83CDF6B.png and b/toxygen/smileys/default/D83CDF6B.png differ diff --git a/toxygen/smileys/default/D83CDF6C.png b/toxygen/smileys/default/D83CDF6C.png index fbc30fd..b796b6a 100644 Binary files a/toxygen/smileys/default/D83CDF6C.png and b/toxygen/smileys/default/D83CDF6C.png differ diff --git a/toxygen/smileys/default/D83CDF6D.png b/toxygen/smileys/default/D83CDF6D.png index 90a201a..622f296 100644 Binary files a/toxygen/smileys/default/D83CDF6D.png and b/toxygen/smileys/default/D83CDF6D.png differ diff --git a/toxygen/smileys/default/D83CDF6E.png b/toxygen/smileys/default/D83CDF6E.png index f3a454c..c534a4b 100644 Binary files a/toxygen/smileys/default/D83CDF6E.png and b/toxygen/smileys/default/D83CDF6E.png differ diff --git a/toxygen/smileys/default/D83CDF6F.png b/toxygen/smileys/default/D83CDF6F.png index f64f24e..3f03181 100644 Binary files a/toxygen/smileys/default/D83CDF6F.png and b/toxygen/smileys/default/D83CDF6F.png differ diff --git a/toxygen/smileys/default/D83CDF70.png b/toxygen/smileys/default/D83CDF70.png index 4101f40..f930ce7 100644 Binary files a/toxygen/smileys/default/D83CDF70.png and b/toxygen/smileys/default/D83CDF70.png differ diff --git a/toxygen/smileys/default/D83CDF71.png b/toxygen/smileys/default/D83CDF71.png index dce9338..0db1d71 100644 Binary files a/toxygen/smileys/default/D83CDF71.png and b/toxygen/smileys/default/D83CDF71.png differ diff --git a/toxygen/smileys/default/D83CDF72.png b/toxygen/smileys/default/D83CDF72.png index 3f26b49..0ae27b1 100644 Binary files a/toxygen/smileys/default/D83CDF72.png and b/toxygen/smileys/default/D83CDF72.png differ diff --git a/toxygen/smileys/default/D83CDF73.png b/toxygen/smileys/default/D83CDF73.png index 4b8c2ef..5b7dcfa 100644 Binary files a/toxygen/smileys/default/D83CDF73.png and b/toxygen/smileys/default/D83CDF73.png differ diff --git a/toxygen/smileys/default/D83CDF74.png b/toxygen/smileys/default/D83CDF74.png index 368e073..a15bf59 100644 Binary files a/toxygen/smileys/default/D83CDF74.png and b/toxygen/smileys/default/D83CDF74.png differ diff --git a/toxygen/smileys/default/D83CDF75.png b/toxygen/smileys/default/D83CDF75.png index e1fd614..cc30ad5 100644 Binary files a/toxygen/smileys/default/D83CDF75.png and b/toxygen/smileys/default/D83CDF75.png differ diff --git a/toxygen/smileys/default/D83CDF76.png b/toxygen/smileys/default/D83CDF76.png index 84afa14..449d352 100644 Binary files a/toxygen/smileys/default/D83CDF76.png and b/toxygen/smileys/default/D83CDF76.png differ diff --git a/toxygen/smileys/default/D83CDF77.png b/toxygen/smileys/default/D83CDF77.png index 0ac8434..12098c5 100644 Binary files a/toxygen/smileys/default/D83CDF77.png and b/toxygen/smileys/default/D83CDF77.png differ diff --git a/toxygen/smileys/default/D83CDF78.png b/toxygen/smileys/default/D83CDF78.png index ea85994..f4ed4ea 100644 Binary files a/toxygen/smileys/default/D83CDF78.png and b/toxygen/smileys/default/D83CDF78.png differ diff --git a/toxygen/smileys/default/D83CDF79.png b/toxygen/smileys/default/D83CDF79.png index 5b94fda..ce34a5f 100644 Binary files a/toxygen/smileys/default/D83CDF79.png and b/toxygen/smileys/default/D83CDF79.png differ diff --git a/toxygen/smileys/default/D83CDF7A.png b/toxygen/smileys/default/D83CDF7A.png index 7f8a1f2..e5efdae 100644 Binary files a/toxygen/smileys/default/D83CDF7A.png and b/toxygen/smileys/default/D83CDF7A.png differ diff --git a/toxygen/smileys/default/D83CDF7B.png b/toxygen/smileys/default/D83CDF7B.png index 5fac44f..f690c80 100644 Binary files a/toxygen/smileys/default/D83CDF7B.png and b/toxygen/smileys/default/D83CDF7B.png differ diff --git a/toxygen/smileys/default/D83CDF7C.png b/toxygen/smileys/default/D83CDF7C.png index 765efa2..81e6102 100644 Binary files a/toxygen/smileys/default/D83CDF7C.png and b/toxygen/smileys/default/D83CDF7C.png differ diff --git a/toxygen/smileys/default/D83CDF80.png b/toxygen/smileys/default/D83CDF80.png index 3e00c99..18bfbb4 100644 Binary files a/toxygen/smileys/default/D83CDF80.png and b/toxygen/smileys/default/D83CDF80.png differ diff --git a/toxygen/smileys/default/D83CDF81.png b/toxygen/smileys/default/D83CDF81.png index 6fa89b5..aa28c36 100644 Binary files a/toxygen/smileys/default/D83CDF81.png and b/toxygen/smileys/default/D83CDF81.png differ diff --git a/toxygen/smileys/default/D83CDF82.png b/toxygen/smileys/default/D83CDF82.png index 5de5f4e..a9c0f5b 100644 Binary files a/toxygen/smileys/default/D83CDF82.png and b/toxygen/smileys/default/D83CDF82.png differ diff --git a/toxygen/smileys/default/D83CDF83.png b/toxygen/smileys/default/D83CDF83.png index 29f2d56..d446cf6 100644 Binary files a/toxygen/smileys/default/D83CDF83.png and b/toxygen/smileys/default/D83CDF83.png differ diff --git a/toxygen/smileys/default/D83CDF84.png b/toxygen/smileys/default/D83CDF84.png index d219ab8..c86aa0f 100644 Binary files a/toxygen/smileys/default/D83CDF84.png and b/toxygen/smileys/default/D83CDF84.png differ diff --git a/toxygen/smileys/default/D83CDF85.png b/toxygen/smileys/default/D83CDF85.png index 2b96030..d9a4273 100644 Binary files a/toxygen/smileys/default/D83CDF85.png and b/toxygen/smileys/default/D83CDF85.png differ diff --git a/toxygen/smileys/default/D83CDF86.png b/toxygen/smileys/default/D83CDF86.png index bbe1a2f..3c07faf 100644 Binary files a/toxygen/smileys/default/D83CDF86.png and b/toxygen/smileys/default/D83CDF86.png differ diff --git a/toxygen/smileys/default/D83CDF87.png b/toxygen/smileys/default/D83CDF87.png index 1af4546..6fb75ec 100644 Binary files a/toxygen/smileys/default/D83CDF87.png and b/toxygen/smileys/default/D83CDF87.png differ diff --git a/toxygen/smileys/default/D83CDF88.png b/toxygen/smileys/default/D83CDF88.png index f208b55..ad51677 100644 Binary files a/toxygen/smileys/default/D83CDF88.png and b/toxygen/smileys/default/D83CDF88.png differ diff --git a/toxygen/smileys/default/D83CDF89.png b/toxygen/smileys/default/D83CDF89.png index aaf4071..5c4d559 100644 Binary files a/toxygen/smileys/default/D83CDF89.png and b/toxygen/smileys/default/D83CDF89.png differ diff --git a/toxygen/smileys/default/D83CDF8A.png b/toxygen/smileys/default/D83CDF8A.png index ace4f9e..7d5afa9 100644 Binary files a/toxygen/smileys/default/D83CDF8A.png and b/toxygen/smileys/default/D83CDF8A.png differ diff --git a/toxygen/smileys/default/D83CDF8B.png b/toxygen/smileys/default/D83CDF8B.png index fedb653..51c96fe 100644 Binary files a/toxygen/smileys/default/D83CDF8B.png and b/toxygen/smileys/default/D83CDF8B.png differ diff --git a/toxygen/smileys/default/D83CDF8C.png b/toxygen/smileys/default/D83CDF8C.png index 3b62bba..f2f460b 100644 Binary files a/toxygen/smileys/default/D83CDF8C.png and b/toxygen/smileys/default/D83CDF8C.png differ diff --git a/toxygen/smileys/default/D83CDF8D.png b/toxygen/smileys/default/D83CDF8D.png index f73d236..b83bebb 100644 Binary files a/toxygen/smileys/default/D83CDF8D.png and b/toxygen/smileys/default/D83CDF8D.png differ diff --git a/toxygen/smileys/default/D83CDF8E.png b/toxygen/smileys/default/D83CDF8E.png index a3dcad2..734e849 100644 Binary files a/toxygen/smileys/default/D83CDF8E.png and b/toxygen/smileys/default/D83CDF8E.png differ diff --git a/toxygen/smileys/default/D83CDF8F.png b/toxygen/smileys/default/D83CDF8F.png index ef3b5fe..a23ab7e 100644 Binary files a/toxygen/smileys/default/D83CDF8F.png and b/toxygen/smileys/default/D83CDF8F.png differ diff --git a/toxygen/smileys/default/D83CDF90.png b/toxygen/smileys/default/D83CDF90.png index 17e008f..7a282a3 100644 Binary files a/toxygen/smileys/default/D83CDF90.png and b/toxygen/smileys/default/D83CDF90.png differ diff --git a/toxygen/smileys/default/D83CDF91.png b/toxygen/smileys/default/D83CDF91.png index a306b33..2c748d0 100644 Binary files a/toxygen/smileys/default/D83CDF91.png and b/toxygen/smileys/default/D83CDF91.png differ diff --git a/toxygen/smileys/default/D83CDF92.png b/toxygen/smileys/default/D83CDF92.png index 557fcf4..485bd18 100644 Binary files a/toxygen/smileys/default/D83CDF92.png and b/toxygen/smileys/default/D83CDF92.png differ diff --git a/toxygen/smileys/default/D83CDF93.png b/toxygen/smileys/default/D83CDF93.png index 8d6ae23..5a601fe 100644 Binary files a/toxygen/smileys/default/D83CDF93.png and b/toxygen/smileys/default/D83CDF93.png differ diff --git a/toxygen/smileys/default/D83CDFA0.png b/toxygen/smileys/default/D83CDFA0.png index 7e28985..0ba4267 100644 Binary files a/toxygen/smileys/default/D83CDFA0.png and b/toxygen/smileys/default/D83CDFA0.png differ diff --git a/toxygen/smileys/default/D83CDFA1.png b/toxygen/smileys/default/D83CDFA1.png index 0413512..d59c5e5 100644 Binary files a/toxygen/smileys/default/D83CDFA1.png and b/toxygen/smileys/default/D83CDFA1.png differ diff --git a/toxygen/smileys/default/D83CDFA2.png b/toxygen/smileys/default/D83CDFA2.png index eb1699c..3e8437b 100644 Binary files a/toxygen/smileys/default/D83CDFA2.png and b/toxygen/smileys/default/D83CDFA2.png differ diff --git a/toxygen/smileys/default/D83CDFA3.png b/toxygen/smileys/default/D83CDFA3.png index 215a5a4..493f9f4 100644 Binary files a/toxygen/smileys/default/D83CDFA3.png and b/toxygen/smileys/default/D83CDFA3.png differ diff --git a/toxygen/smileys/default/D83CDFA4.png b/toxygen/smileys/default/D83CDFA4.png index 8f12411..8ad0988 100644 Binary files a/toxygen/smileys/default/D83CDFA4.png and b/toxygen/smileys/default/D83CDFA4.png differ diff --git a/toxygen/smileys/default/D83CDFA5.png b/toxygen/smileys/default/D83CDFA5.png index 7d73059..d21fd0e 100644 Binary files a/toxygen/smileys/default/D83CDFA5.png and b/toxygen/smileys/default/D83CDFA5.png differ diff --git a/toxygen/smileys/default/D83CDFA6.png b/toxygen/smileys/default/D83CDFA6.png index 8a0dceb..e3a45c1 100644 Binary files a/toxygen/smileys/default/D83CDFA6.png and b/toxygen/smileys/default/D83CDFA6.png differ diff --git a/toxygen/smileys/default/D83CDFA7.png b/toxygen/smileys/default/D83CDFA7.png index 3b36443..e351eff 100644 Binary files a/toxygen/smileys/default/D83CDFA7.png and b/toxygen/smileys/default/D83CDFA7.png differ diff --git a/toxygen/smileys/default/D83CDFA8.png b/toxygen/smileys/default/D83CDFA8.png index 73bba44..ef35ada 100644 Binary files a/toxygen/smileys/default/D83CDFA8.png and b/toxygen/smileys/default/D83CDFA8.png differ diff --git a/toxygen/smileys/default/D83CDFA9.png b/toxygen/smileys/default/D83CDFA9.png index 1337f64..27f6c29 100644 Binary files a/toxygen/smileys/default/D83CDFA9.png and b/toxygen/smileys/default/D83CDFA9.png differ diff --git a/toxygen/smileys/default/D83CDFAA.png b/toxygen/smileys/default/D83CDFAA.png index 81fd66e..ccb34e0 100644 Binary files a/toxygen/smileys/default/D83CDFAA.png and b/toxygen/smileys/default/D83CDFAA.png differ diff --git a/toxygen/smileys/default/D83CDFAB.png b/toxygen/smileys/default/D83CDFAB.png index a0ca7dc..38e00dd 100644 Binary files a/toxygen/smileys/default/D83CDFAB.png and b/toxygen/smileys/default/D83CDFAB.png differ diff --git a/toxygen/smileys/default/D83CDFAC.png b/toxygen/smileys/default/D83CDFAC.png index 3effe3a..6ddf1db 100644 Binary files a/toxygen/smileys/default/D83CDFAC.png and b/toxygen/smileys/default/D83CDFAC.png differ diff --git a/toxygen/smileys/default/D83CDFAD.png b/toxygen/smileys/default/D83CDFAD.png index 97b9917..ec99842 100644 Binary files a/toxygen/smileys/default/D83CDFAD.png and b/toxygen/smileys/default/D83CDFAD.png differ diff --git a/toxygen/smileys/default/D83CDFAE.png b/toxygen/smileys/default/D83CDFAE.png index c125606..a94e3a6 100644 Binary files a/toxygen/smileys/default/D83CDFAE.png and b/toxygen/smileys/default/D83CDFAE.png differ diff --git a/toxygen/smileys/default/D83CDFAF.png b/toxygen/smileys/default/D83CDFAF.png index 3ba2c9c..b8aa1e1 100644 Binary files a/toxygen/smileys/default/D83CDFAF.png and b/toxygen/smileys/default/D83CDFAF.png differ diff --git a/toxygen/smileys/default/D83CDFB0.png b/toxygen/smileys/default/D83CDFB0.png index f6ac7a2..a3c36cf 100644 Binary files a/toxygen/smileys/default/D83CDFB0.png and b/toxygen/smileys/default/D83CDFB0.png differ diff --git a/toxygen/smileys/default/D83CDFB1.png b/toxygen/smileys/default/D83CDFB1.png index 5d28833..efb17ad 100644 Binary files a/toxygen/smileys/default/D83CDFB1.png and b/toxygen/smileys/default/D83CDFB1.png differ diff --git a/toxygen/smileys/default/D83CDFB2.png b/toxygen/smileys/default/D83CDFB2.png index dc90beb..8fc6c03 100644 Binary files a/toxygen/smileys/default/D83CDFB2.png and b/toxygen/smileys/default/D83CDFB2.png differ diff --git a/toxygen/smileys/default/D83CDFB3.png b/toxygen/smileys/default/D83CDFB3.png index 953c7d9..c7459fa 100644 Binary files a/toxygen/smileys/default/D83CDFB3.png and b/toxygen/smileys/default/D83CDFB3.png differ diff --git a/toxygen/smileys/default/D83CDFB4.png b/toxygen/smileys/default/D83CDFB4.png index 6f383e7..2090a8d 100644 Binary files a/toxygen/smileys/default/D83CDFB4.png and b/toxygen/smileys/default/D83CDFB4.png differ diff --git a/toxygen/smileys/default/D83CDFB5.png b/toxygen/smileys/default/D83CDFB5.png index 4024eeb..e9c0683 100644 Binary files a/toxygen/smileys/default/D83CDFB5.png and b/toxygen/smileys/default/D83CDFB5.png differ diff --git a/toxygen/smileys/default/D83CDFB6.png b/toxygen/smileys/default/D83CDFB6.png index 685a06f..956bc4d 100644 Binary files a/toxygen/smileys/default/D83CDFB6.png and b/toxygen/smileys/default/D83CDFB6.png differ diff --git a/toxygen/smileys/default/D83CDFB7.png b/toxygen/smileys/default/D83CDFB7.png index 421da92..4fde005 100644 Binary files a/toxygen/smileys/default/D83CDFB7.png and b/toxygen/smileys/default/D83CDFB7.png differ diff --git a/toxygen/smileys/default/D83CDFB8.png b/toxygen/smileys/default/D83CDFB8.png index 649899d..584ba69 100644 Binary files a/toxygen/smileys/default/D83CDFB8.png and b/toxygen/smileys/default/D83CDFB8.png differ diff --git a/toxygen/smileys/default/D83CDFB9.png b/toxygen/smileys/default/D83CDFB9.png index ecb2a80..748a587 100644 Binary files a/toxygen/smileys/default/D83CDFB9.png and b/toxygen/smileys/default/D83CDFB9.png differ diff --git a/toxygen/smileys/default/D83CDFBA.png b/toxygen/smileys/default/D83CDFBA.png index e19dd82..77ca90e 100644 Binary files a/toxygen/smileys/default/D83CDFBA.png and b/toxygen/smileys/default/D83CDFBA.png differ diff --git a/toxygen/smileys/default/D83CDFBB.png b/toxygen/smileys/default/D83CDFBB.png index 5b59ae9..0f1b9a7 100644 Binary files a/toxygen/smileys/default/D83CDFBB.png and b/toxygen/smileys/default/D83CDFBB.png differ diff --git a/toxygen/smileys/default/D83CDFBC.png b/toxygen/smileys/default/D83CDFBC.png index 12bbebd..0441b72 100644 Binary files a/toxygen/smileys/default/D83CDFBC.png and b/toxygen/smileys/default/D83CDFBC.png differ diff --git a/toxygen/smileys/default/D83CDFBD.png b/toxygen/smileys/default/D83CDFBD.png index 724799d..5d2beac 100644 Binary files a/toxygen/smileys/default/D83CDFBD.png and b/toxygen/smileys/default/D83CDFBD.png differ diff --git a/toxygen/smileys/default/D83CDFBE.png b/toxygen/smileys/default/D83CDFBE.png index 24ae90a..96e8605 100644 Binary files a/toxygen/smileys/default/D83CDFBE.png and b/toxygen/smileys/default/D83CDFBE.png differ diff --git a/toxygen/smileys/default/D83CDFBF.png b/toxygen/smileys/default/D83CDFBF.png index 8891637..79f2da3 100644 Binary files a/toxygen/smileys/default/D83CDFBF.png and b/toxygen/smileys/default/D83CDFBF.png differ diff --git a/toxygen/smileys/default/D83CDFC0.png b/toxygen/smileys/default/D83CDFC0.png index a877641..8bf69e2 100644 Binary files a/toxygen/smileys/default/D83CDFC0.png and b/toxygen/smileys/default/D83CDFC0.png differ diff --git a/toxygen/smileys/default/D83CDFC1.png b/toxygen/smileys/default/D83CDFC1.png index f0f5e29..be1a59b 100644 Binary files a/toxygen/smileys/default/D83CDFC1.png and b/toxygen/smileys/default/D83CDFC1.png differ diff --git a/toxygen/smileys/default/D83CDFC2.png b/toxygen/smileys/default/D83CDFC2.png index 9e35a6e..9bdd6b4 100644 Binary files a/toxygen/smileys/default/D83CDFC2.png and b/toxygen/smileys/default/D83CDFC2.png differ diff --git a/toxygen/smileys/default/D83CDFC3.png b/toxygen/smileys/default/D83CDFC3.png index f4721f1..d8c9cf3 100644 Binary files a/toxygen/smileys/default/D83CDFC3.png and b/toxygen/smileys/default/D83CDFC3.png differ diff --git a/toxygen/smileys/default/D83CDFC4.png b/toxygen/smileys/default/D83CDFC4.png index 19b88bb..dbe988e 100644 Binary files a/toxygen/smileys/default/D83CDFC4.png and b/toxygen/smileys/default/D83CDFC4.png differ diff --git a/toxygen/smileys/default/D83CDFC6.png b/toxygen/smileys/default/D83CDFC6.png index 7ede172..ea79487 100644 Binary files a/toxygen/smileys/default/D83CDFC6.png and b/toxygen/smileys/default/D83CDFC6.png differ diff --git a/toxygen/smileys/default/D83CDFC7.png b/toxygen/smileys/default/D83CDFC7.png index 4579ae2..1309322 100644 Binary files a/toxygen/smileys/default/D83CDFC7.png and b/toxygen/smileys/default/D83CDFC7.png differ diff --git a/toxygen/smileys/default/D83CDFC8.png b/toxygen/smileys/default/D83CDFC8.png index d804bf5..4540605 100644 Binary files a/toxygen/smileys/default/D83CDFC8.png and b/toxygen/smileys/default/D83CDFC8.png differ diff --git a/toxygen/smileys/default/D83CDFC9.png b/toxygen/smileys/default/D83CDFC9.png index fca8fbf..f4048b8 100644 Binary files a/toxygen/smileys/default/D83CDFC9.png and b/toxygen/smileys/default/D83CDFC9.png differ diff --git a/toxygen/smileys/default/D83CDFCA.png b/toxygen/smileys/default/D83CDFCA.png index 4fdb629..45aa9fb 100644 Binary files a/toxygen/smileys/default/D83CDFCA.png and b/toxygen/smileys/default/D83CDFCA.png differ diff --git a/toxygen/smileys/default/D83CDFE0.png b/toxygen/smileys/default/D83CDFE0.png index 2324908..2fcd7b3 100644 Binary files a/toxygen/smileys/default/D83CDFE0.png and b/toxygen/smileys/default/D83CDFE0.png differ diff --git a/toxygen/smileys/default/D83CDFE1.png b/toxygen/smileys/default/D83CDFE1.png index 197c598..18ae9e6 100644 Binary files a/toxygen/smileys/default/D83CDFE1.png and b/toxygen/smileys/default/D83CDFE1.png differ diff --git a/toxygen/smileys/default/D83CDFE2.png b/toxygen/smileys/default/D83CDFE2.png index 43f5c16..4c73dcb 100644 Binary files a/toxygen/smileys/default/D83CDFE2.png and b/toxygen/smileys/default/D83CDFE2.png differ diff --git a/toxygen/smileys/default/D83CDFE3.png b/toxygen/smileys/default/D83CDFE3.png index 8214077..dafcbb0 100644 Binary files a/toxygen/smileys/default/D83CDFE3.png and b/toxygen/smileys/default/D83CDFE3.png differ diff --git a/toxygen/smileys/default/D83CDFE4.png b/toxygen/smileys/default/D83CDFE4.png index 28cec25..32ec6cc 100644 Binary files a/toxygen/smileys/default/D83CDFE4.png and b/toxygen/smileys/default/D83CDFE4.png differ diff --git a/toxygen/smileys/default/D83CDFE5.png b/toxygen/smileys/default/D83CDFE5.png index a048c9c..05da811 100644 Binary files a/toxygen/smileys/default/D83CDFE5.png and b/toxygen/smileys/default/D83CDFE5.png differ diff --git a/toxygen/smileys/default/D83CDFE7.png b/toxygen/smileys/default/D83CDFE7.png index 612e467..4527782 100644 Binary files a/toxygen/smileys/default/D83CDFE7.png and b/toxygen/smileys/default/D83CDFE7.png differ diff --git a/toxygen/smileys/default/D83CDFE8.png b/toxygen/smileys/default/D83CDFE8.png index d6ef1f5..a8586ee 100644 Binary files a/toxygen/smileys/default/D83CDFE8.png and b/toxygen/smileys/default/D83CDFE8.png differ diff --git a/toxygen/smileys/default/D83CDFE9.png b/toxygen/smileys/default/D83CDFE9.png index 55f1ea2..54bc6d1 100644 Binary files a/toxygen/smileys/default/D83CDFE9.png and b/toxygen/smileys/default/D83CDFE9.png differ diff --git a/toxygen/smileys/default/D83CDFEA.png b/toxygen/smileys/default/D83CDFEA.png index dceb8d4..4060f51 100644 Binary files a/toxygen/smileys/default/D83CDFEA.png and b/toxygen/smileys/default/D83CDFEA.png differ diff --git a/toxygen/smileys/default/D83CDFEB.png b/toxygen/smileys/default/D83CDFEB.png index e20e33f..0b2ec51 100644 Binary files a/toxygen/smileys/default/D83CDFEB.png and b/toxygen/smileys/default/D83CDFEB.png differ diff --git a/toxygen/smileys/default/D83CDFEC.png b/toxygen/smileys/default/D83CDFEC.png index a2e4eb8..7b0d510 100644 Binary files a/toxygen/smileys/default/D83CDFEC.png and b/toxygen/smileys/default/D83CDFEC.png differ diff --git a/toxygen/smileys/default/D83CDFED.png b/toxygen/smileys/default/D83CDFED.png index 8172224..0ae5367 100644 Binary files a/toxygen/smileys/default/D83CDFED.png and b/toxygen/smileys/default/D83CDFED.png differ diff --git a/toxygen/smileys/default/D83CDFEE.png b/toxygen/smileys/default/D83CDFEE.png index 5885b27..55982b6 100644 Binary files a/toxygen/smileys/default/D83CDFEE.png and b/toxygen/smileys/default/D83CDFEE.png differ diff --git a/toxygen/smileys/default/D83CDFEF.png b/toxygen/smileys/default/D83CDFEF.png index 22f4662..1e446c8 100644 Binary files a/toxygen/smileys/default/D83CDFEF.png and b/toxygen/smileys/default/D83CDFEF.png differ diff --git a/toxygen/smileys/default/D83CDFF0.png b/toxygen/smileys/default/D83CDFF0.png index 9eccbe5..0db16f2 100644 Binary files a/toxygen/smileys/default/D83CDFF0.png and b/toxygen/smileys/default/D83CDFF0.png differ diff --git a/toxygen/smileys/default/D83DDC00.png b/toxygen/smileys/default/D83DDC00.png index 7ced002..f7982a4 100644 Binary files a/toxygen/smileys/default/D83DDC00.png and b/toxygen/smileys/default/D83DDC00.png differ diff --git a/toxygen/smileys/default/D83DDC01.png b/toxygen/smileys/default/D83DDC01.png index 0a276c7..6d16b88 100644 Binary files a/toxygen/smileys/default/D83DDC01.png and b/toxygen/smileys/default/D83DDC01.png differ diff --git a/toxygen/smileys/default/D83DDC02.png b/toxygen/smileys/default/D83DDC02.png index c59ce81..4ec1cce 100644 Binary files a/toxygen/smileys/default/D83DDC02.png and b/toxygen/smileys/default/D83DDC02.png differ diff --git a/toxygen/smileys/default/D83DDC03.png b/toxygen/smileys/default/D83DDC03.png index e9eb0d1..df9e284 100644 Binary files a/toxygen/smileys/default/D83DDC03.png and b/toxygen/smileys/default/D83DDC03.png differ diff --git a/toxygen/smileys/default/D83DDC04.png b/toxygen/smileys/default/D83DDC04.png index 8f72f4f..2d50aa0 100644 Binary files a/toxygen/smileys/default/D83DDC04.png and b/toxygen/smileys/default/D83DDC04.png differ diff --git a/toxygen/smileys/default/D83DDC05.png b/toxygen/smileys/default/D83DDC05.png index 5fcfd9e..94cb5f0 100644 Binary files a/toxygen/smileys/default/D83DDC05.png and b/toxygen/smileys/default/D83DDC05.png differ diff --git a/toxygen/smileys/default/D83DDC06.png b/toxygen/smileys/default/D83DDC06.png index df6bdd1..bb771a6 100644 Binary files a/toxygen/smileys/default/D83DDC06.png and b/toxygen/smileys/default/D83DDC06.png differ diff --git a/toxygen/smileys/default/D83DDC07.png b/toxygen/smileys/default/D83DDC07.png index 69a3c74..53b5530 100644 Binary files a/toxygen/smileys/default/D83DDC07.png and b/toxygen/smileys/default/D83DDC07.png differ diff --git a/toxygen/smileys/default/D83DDC08.png b/toxygen/smileys/default/D83DDC08.png index 2c239f2..17991b3 100644 Binary files a/toxygen/smileys/default/D83DDC08.png and b/toxygen/smileys/default/D83DDC08.png differ diff --git a/toxygen/smileys/default/D83DDC09.png b/toxygen/smileys/default/D83DDC09.png index 77e3895..6ce569d 100644 Binary files a/toxygen/smileys/default/D83DDC09.png and b/toxygen/smileys/default/D83DDC09.png differ diff --git a/toxygen/smileys/default/D83DDC0A.png b/toxygen/smileys/default/D83DDC0A.png index bb83653..a8e76cb 100644 Binary files a/toxygen/smileys/default/D83DDC0A.png and b/toxygen/smileys/default/D83DDC0A.png differ diff --git a/toxygen/smileys/default/D83DDC0B.png b/toxygen/smileys/default/D83DDC0B.png index 878e117..9cc5171 100644 Binary files a/toxygen/smileys/default/D83DDC0B.png and b/toxygen/smileys/default/D83DDC0B.png differ diff --git a/toxygen/smileys/default/D83DDC0C.png b/toxygen/smileys/default/D83DDC0C.png index 700a0dc..0d36155 100644 Binary files a/toxygen/smileys/default/D83DDC0C.png and b/toxygen/smileys/default/D83DDC0C.png differ diff --git a/toxygen/smileys/default/D83DDC0D.png b/toxygen/smileys/default/D83DDC0D.png index 411c781..6b38170 100644 Binary files a/toxygen/smileys/default/D83DDC0D.png and b/toxygen/smileys/default/D83DDC0D.png differ diff --git a/toxygen/smileys/default/D83DDC0E.png b/toxygen/smileys/default/D83DDC0E.png index b5774b4..9080dd0 100644 Binary files a/toxygen/smileys/default/D83DDC0E.png and b/toxygen/smileys/default/D83DDC0E.png differ diff --git a/toxygen/smileys/default/D83DDC0F.png b/toxygen/smileys/default/D83DDC0F.png index 69c405b..e74447a 100644 Binary files a/toxygen/smileys/default/D83DDC0F.png and b/toxygen/smileys/default/D83DDC0F.png differ diff --git a/toxygen/smileys/default/D83DDC10.png b/toxygen/smileys/default/D83DDC10.png index 871bcad..070c460 100644 Binary files a/toxygen/smileys/default/D83DDC10.png and b/toxygen/smileys/default/D83DDC10.png differ diff --git a/toxygen/smileys/default/D83DDC11.png b/toxygen/smileys/default/D83DDC11.png index 7f92df6..6f143a6 100644 Binary files a/toxygen/smileys/default/D83DDC11.png and b/toxygen/smileys/default/D83DDC11.png differ diff --git a/toxygen/smileys/default/D83DDC12.png b/toxygen/smileys/default/D83DDC12.png index 30c9e4f..a584b4a 100644 Binary files a/toxygen/smileys/default/D83DDC12.png and b/toxygen/smileys/default/D83DDC12.png differ diff --git a/toxygen/smileys/default/D83DDC13.png b/toxygen/smileys/default/D83DDC13.png index db6531a..ed3c077 100644 Binary files a/toxygen/smileys/default/D83DDC13.png and b/toxygen/smileys/default/D83DDC13.png differ diff --git a/toxygen/smileys/default/D83DDC14.png b/toxygen/smileys/default/D83DDC14.png index 7af6dd1..2e92ba2 100644 Binary files a/toxygen/smileys/default/D83DDC14.png and b/toxygen/smileys/default/D83DDC14.png differ diff --git a/toxygen/smileys/default/D83DDC15.png b/toxygen/smileys/default/D83DDC15.png index ee2f83f..d9fc622 100644 Binary files a/toxygen/smileys/default/D83DDC15.png and b/toxygen/smileys/default/D83DDC15.png differ diff --git a/toxygen/smileys/default/D83DDC16.png b/toxygen/smileys/default/D83DDC16.png index a1c3d5d..c321277 100644 Binary files a/toxygen/smileys/default/D83DDC16.png and b/toxygen/smileys/default/D83DDC16.png differ diff --git a/toxygen/smileys/default/D83DDC17.png b/toxygen/smileys/default/D83DDC17.png index 42f14de..0043f3c 100644 Binary files a/toxygen/smileys/default/D83DDC17.png and b/toxygen/smileys/default/D83DDC17.png differ diff --git a/toxygen/smileys/default/D83DDC18.png b/toxygen/smileys/default/D83DDC18.png index 27f35b6..8a93ce9 100644 Binary files a/toxygen/smileys/default/D83DDC18.png and b/toxygen/smileys/default/D83DDC18.png differ diff --git a/toxygen/smileys/default/D83DDC19.png b/toxygen/smileys/default/D83DDC19.png index da588a4..ac19c2d 100644 Binary files a/toxygen/smileys/default/D83DDC19.png and b/toxygen/smileys/default/D83DDC19.png differ diff --git a/toxygen/smileys/default/D83DDC1A.png b/toxygen/smileys/default/D83DDC1A.png index ae8a07b..635ccfa 100644 Binary files a/toxygen/smileys/default/D83DDC1A.png and b/toxygen/smileys/default/D83DDC1A.png differ diff --git a/toxygen/smileys/default/D83DDC1B.png b/toxygen/smileys/default/D83DDC1B.png index 412d5fe..dccb76e 100644 Binary files a/toxygen/smileys/default/D83DDC1B.png and b/toxygen/smileys/default/D83DDC1B.png differ diff --git a/toxygen/smileys/default/D83DDC1C.png b/toxygen/smileys/default/D83DDC1C.png index fd285ed..73d740e 100644 Binary files a/toxygen/smileys/default/D83DDC1C.png and b/toxygen/smileys/default/D83DDC1C.png differ diff --git a/toxygen/smileys/default/D83DDC1D.png b/toxygen/smileys/default/D83DDC1D.png index c74c7a7..1b49267 100644 Binary files a/toxygen/smileys/default/D83DDC1D.png and b/toxygen/smileys/default/D83DDC1D.png differ diff --git a/toxygen/smileys/default/D83DDC1E.png b/toxygen/smileys/default/D83DDC1E.png index 4a47598..d66de86 100644 Binary files a/toxygen/smileys/default/D83DDC1E.png and b/toxygen/smileys/default/D83DDC1E.png differ diff --git a/toxygen/smileys/default/D83DDC1F.png b/toxygen/smileys/default/D83DDC1F.png index ca88daf..52f30a8 100644 Binary files a/toxygen/smileys/default/D83DDC1F.png and b/toxygen/smileys/default/D83DDC1F.png differ diff --git a/toxygen/smileys/default/D83DDC20.png b/toxygen/smileys/default/D83DDC20.png index 465e11a..2b1e644 100644 Binary files a/toxygen/smileys/default/D83DDC20.png and b/toxygen/smileys/default/D83DDC20.png differ diff --git a/toxygen/smileys/default/D83DDC21.png b/toxygen/smileys/default/D83DDC21.png index 475564a..279dc2e 100644 Binary files a/toxygen/smileys/default/D83DDC21.png and b/toxygen/smileys/default/D83DDC21.png differ diff --git a/toxygen/smileys/default/D83DDC22.png b/toxygen/smileys/default/D83DDC22.png index 9db8e98..2314d9f 100644 Binary files a/toxygen/smileys/default/D83DDC22.png and b/toxygen/smileys/default/D83DDC22.png differ diff --git a/toxygen/smileys/default/D83DDC23.png b/toxygen/smileys/default/D83DDC23.png index 12b00cd..7a6f8d5 100644 Binary files a/toxygen/smileys/default/D83DDC23.png and b/toxygen/smileys/default/D83DDC23.png differ diff --git a/toxygen/smileys/default/D83DDC24.png b/toxygen/smileys/default/D83DDC24.png index 37ff954..480fcf1 100644 Binary files a/toxygen/smileys/default/D83DDC24.png and b/toxygen/smileys/default/D83DDC24.png differ diff --git a/toxygen/smileys/default/D83DDC25.png b/toxygen/smileys/default/D83DDC25.png index f72eb69..6e05fed 100644 Binary files a/toxygen/smileys/default/D83DDC25.png and b/toxygen/smileys/default/D83DDC25.png differ diff --git a/toxygen/smileys/default/D83DDC26.png b/toxygen/smileys/default/D83DDC26.png index 033be84..e53f643 100644 Binary files a/toxygen/smileys/default/D83DDC26.png and b/toxygen/smileys/default/D83DDC26.png differ diff --git a/toxygen/smileys/default/D83DDC27.png b/toxygen/smileys/default/D83DDC27.png index f48cc2f..779766c 100644 Binary files a/toxygen/smileys/default/D83DDC27.png and b/toxygen/smileys/default/D83DDC27.png differ diff --git a/toxygen/smileys/default/D83DDC28.png b/toxygen/smileys/default/D83DDC28.png index 4a113e2..cb6821c 100644 Binary files a/toxygen/smileys/default/D83DDC28.png and b/toxygen/smileys/default/D83DDC28.png differ diff --git a/toxygen/smileys/default/D83DDC2A.png b/toxygen/smileys/default/D83DDC2A.png index c3c5aef..a44d2e1 100644 Binary files a/toxygen/smileys/default/D83DDC2A.png and b/toxygen/smileys/default/D83DDC2A.png differ diff --git a/toxygen/smileys/default/D83DDC2B.png b/toxygen/smileys/default/D83DDC2B.png index 0f40d31..f09e1a3 100644 Binary files a/toxygen/smileys/default/D83DDC2B.png and b/toxygen/smileys/default/D83DDC2B.png differ diff --git a/toxygen/smileys/default/D83DDC2C.png b/toxygen/smileys/default/D83DDC2C.png index d107ff6..2c855eb 100644 Binary files a/toxygen/smileys/default/D83DDC2C.png and b/toxygen/smileys/default/D83DDC2C.png differ diff --git a/toxygen/smileys/default/D83DDC2D.png b/toxygen/smileys/default/D83DDC2D.png index d8b4f90..ff2e49a 100644 Binary files a/toxygen/smileys/default/D83DDC2D.png and b/toxygen/smileys/default/D83DDC2D.png differ diff --git a/toxygen/smileys/default/D83DDC2E.png b/toxygen/smileys/default/D83DDC2E.png index cd05541..f95c3b9 100644 Binary files a/toxygen/smileys/default/D83DDC2E.png and b/toxygen/smileys/default/D83DDC2E.png differ diff --git a/toxygen/smileys/default/D83DDC2F.png b/toxygen/smileys/default/D83DDC2F.png index 086a5b6..3598329 100644 Binary files a/toxygen/smileys/default/D83DDC2F.png and b/toxygen/smileys/default/D83DDC2F.png differ diff --git a/toxygen/smileys/default/D83DDC30.png b/toxygen/smileys/default/D83DDC30.png index e926a23..3249366 100644 Binary files a/toxygen/smileys/default/D83DDC30.png and b/toxygen/smileys/default/D83DDC30.png differ diff --git a/toxygen/smileys/default/D83DDC31.png b/toxygen/smileys/default/D83DDC31.png index c250baf..5a410e3 100644 Binary files a/toxygen/smileys/default/D83DDC31.png and b/toxygen/smileys/default/D83DDC31.png differ diff --git a/toxygen/smileys/default/D83DDC32.png b/toxygen/smileys/default/D83DDC32.png index a2f991b..0857137 100644 Binary files a/toxygen/smileys/default/D83DDC32.png and b/toxygen/smileys/default/D83DDC32.png differ diff --git a/toxygen/smileys/default/D83DDC33.png b/toxygen/smileys/default/D83DDC33.png index aacba60..6f025da 100644 Binary files a/toxygen/smileys/default/D83DDC33.png and b/toxygen/smileys/default/D83DDC33.png differ diff --git a/toxygen/smileys/default/D83DDC34.png b/toxygen/smileys/default/D83DDC34.png index a0b1b67..0be777d 100644 Binary files a/toxygen/smileys/default/D83DDC34.png and b/toxygen/smileys/default/D83DDC34.png differ diff --git a/toxygen/smileys/default/D83DDC35.png b/toxygen/smileys/default/D83DDC35.png index 4873b38..5ccdc02 100644 Binary files a/toxygen/smileys/default/D83DDC35.png and b/toxygen/smileys/default/D83DDC35.png differ diff --git a/toxygen/smileys/default/D83DDC36.png b/toxygen/smileys/default/D83DDC36.png index e4bb3d7..50ff6c0 100644 Binary files a/toxygen/smileys/default/D83DDC36.png and b/toxygen/smileys/default/D83DDC36.png differ diff --git a/toxygen/smileys/default/D83DDC37.png b/toxygen/smileys/default/D83DDC37.png index 4d6fad6..78afd2c 100644 Binary files a/toxygen/smileys/default/D83DDC37.png and b/toxygen/smileys/default/D83DDC37.png differ diff --git a/toxygen/smileys/default/D83DDC38.png b/toxygen/smileys/default/D83DDC38.png index d646948..2141d1b 100644 Binary files a/toxygen/smileys/default/D83DDC38.png and b/toxygen/smileys/default/D83DDC38.png differ diff --git a/toxygen/smileys/default/D83DDC39.png b/toxygen/smileys/default/D83DDC39.png index cd2027c..775d857 100644 Binary files a/toxygen/smileys/default/D83DDC39.png and b/toxygen/smileys/default/D83DDC39.png differ diff --git a/toxygen/smileys/default/D83DDC3A.png b/toxygen/smileys/default/D83DDC3A.png index 7dcd9f7..a2bbc5b 100644 Binary files a/toxygen/smileys/default/D83DDC3A.png and b/toxygen/smileys/default/D83DDC3A.png differ diff --git a/toxygen/smileys/default/D83DDC3B.png b/toxygen/smileys/default/D83DDC3B.png index f9d2a6b..6a91df8 100644 Binary files a/toxygen/smileys/default/D83DDC3B.png and b/toxygen/smileys/default/D83DDC3B.png differ diff --git a/toxygen/smileys/default/D83DDC3C.png b/toxygen/smileys/default/D83DDC3C.png index c18d728..58ecebc 100644 Binary files a/toxygen/smileys/default/D83DDC3C.png and b/toxygen/smileys/default/D83DDC3C.png differ diff --git a/toxygen/smileys/default/D83DDC3D.png b/toxygen/smileys/default/D83DDC3D.png index 0044223..3863e97 100644 Binary files a/toxygen/smileys/default/D83DDC3D.png and b/toxygen/smileys/default/D83DDC3D.png differ diff --git a/toxygen/smileys/default/D83DDC3E.png b/toxygen/smileys/default/D83DDC3E.png index 5f8023e..288939d 100644 Binary files a/toxygen/smileys/default/D83DDC3E.png and b/toxygen/smileys/default/D83DDC3E.png differ diff --git a/toxygen/smileys/default/D83DDC40.png b/toxygen/smileys/default/D83DDC40.png index 4c4ede3..3a43419 100644 Binary files a/toxygen/smileys/default/D83DDC40.png and b/toxygen/smileys/default/D83DDC40.png differ diff --git a/toxygen/smileys/default/D83DDC42.png b/toxygen/smileys/default/D83DDC42.png index 990bff9..baeb7b1 100644 Binary files a/toxygen/smileys/default/D83DDC42.png and b/toxygen/smileys/default/D83DDC42.png differ diff --git a/toxygen/smileys/default/D83DDC43.png b/toxygen/smileys/default/D83DDC43.png index 72b0103..71af1e2 100644 Binary files a/toxygen/smileys/default/D83DDC43.png and b/toxygen/smileys/default/D83DDC43.png differ diff --git a/toxygen/smileys/default/D83DDC44.png b/toxygen/smileys/default/D83DDC44.png index 627f204..fd7a6e2 100644 Binary files a/toxygen/smileys/default/D83DDC44.png and b/toxygen/smileys/default/D83DDC44.png differ diff --git a/toxygen/smileys/default/D83DDC45.png b/toxygen/smileys/default/D83DDC45.png index 63ec09e..75aec77 100644 Binary files a/toxygen/smileys/default/D83DDC45.png and b/toxygen/smileys/default/D83DDC45.png differ diff --git a/toxygen/smileys/default/D83DDC46.png b/toxygen/smileys/default/D83DDC46.png index ff52801..d881fdb 100644 Binary files a/toxygen/smileys/default/D83DDC46.png and b/toxygen/smileys/default/D83DDC46.png differ diff --git a/toxygen/smileys/default/D83DDC47.png b/toxygen/smileys/default/D83DDC47.png index 3fc0730..029274e 100644 Binary files a/toxygen/smileys/default/D83DDC47.png and b/toxygen/smileys/default/D83DDC47.png differ diff --git a/toxygen/smileys/default/D83DDC48.png b/toxygen/smileys/default/D83DDC48.png index c961655..7a68d8c 100644 Binary files a/toxygen/smileys/default/D83DDC48.png and b/toxygen/smileys/default/D83DDC48.png differ diff --git a/toxygen/smileys/default/D83DDC49.png b/toxygen/smileys/default/D83DDC49.png index 06ce893..db52a0d 100644 Binary files a/toxygen/smileys/default/D83DDC49.png and b/toxygen/smileys/default/D83DDC49.png differ diff --git a/toxygen/smileys/default/D83DDC4A.png b/toxygen/smileys/default/D83DDC4A.png index a4f5a83..0026ab1 100644 Binary files a/toxygen/smileys/default/D83DDC4A.png and b/toxygen/smileys/default/D83DDC4A.png differ diff --git a/toxygen/smileys/default/D83DDC4B.png b/toxygen/smileys/default/D83DDC4B.png index 66590dd..87d5bfe 100644 Binary files a/toxygen/smileys/default/D83DDC4B.png and b/toxygen/smileys/default/D83DDC4B.png differ diff --git a/toxygen/smileys/default/D83DDC4C.png b/toxygen/smileys/default/D83DDC4C.png index 5445e2f..60b7abc 100644 Binary files a/toxygen/smileys/default/D83DDC4C.png and b/toxygen/smileys/default/D83DDC4C.png differ diff --git a/toxygen/smileys/default/D83DDC4D.png b/toxygen/smileys/default/D83DDC4D.png index 3a8e512..2f816aa 100644 Binary files a/toxygen/smileys/default/D83DDC4D.png and b/toxygen/smileys/default/D83DDC4D.png differ diff --git a/toxygen/smileys/default/D83DDC4E.png b/toxygen/smileys/default/D83DDC4E.png index 79dec71..7773282 100644 Binary files a/toxygen/smileys/default/D83DDC4E.png and b/toxygen/smileys/default/D83DDC4E.png differ diff --git a/toxygen/smileys/default/D83DDC4F.png b/toxygen/smileys/default/D83DDC4F.png index f511857..45a633a 100644 Binary files a/toxygen/smileys/default/D83DDC4F.png and b/toxygen/smileys/default/D83DDC4F.png differ diff --git a/toxygen/smileys/default/D83DDC50.png b/toxygen/smileys/default/D83DDC50.png index 8809893..da64391 100644 Binary files a/toxygen/smileys/default/D83DDC50.png and b/toxygen/smileys/default/D83DDC50.png differ diff --git a/toxygen/smileys/default/D83DDC51.png b/toxygen/smileys/default/D83DDC51.png index d05d576..0eeaeec 100644 Binary files a/toxygen/smileys/default/D83DDC51.png and b/toxygen/smileys/default/D83DDC51.png differ diff --git a/toxygen/smileys/default/D83DDC52.png b/toxygen/smileys/default/D83DDC52.png index 4f4cc0f..897a330 100644 Binary files a/toxygen/smileys/default/D83DDC52.png and b/toxygen/smileys/default/D83DDC52.png differ diff --git a/toxygen/smileys/default/D83DDC53.png b/toxygen/smileys/default/D83DDC53.png index 3a691f0..5b9b401 100644 Binary files a/toxygen/smileys/default/D83DDC53.png and b/toxygen/smileys/default/D83DDC53.png differ diff --git a/toxygen/smileys/default/D83DDC54.png b/toxygen/smileys/default/D83DDC54.png index b095f9c..fa76d90 100644 Binary files a/toxygen/smileys/default/D83DDC54.png and b/toxygen/smileys/default/D83DDC54.png differ diff --git a/toxygen/smileys/default/D83DDC55.png b/toxygen/smileys/default/D83DDC55.png index 84a5d62..23d1ebc 100644 Binary files a/toxygen/smileys/default/D83DDC55.png and b/toxygen/smileys/default/D83DDC55.png differ diff --git a/toxygen/smileys/default/D83DDC56.png b/toxygen/smileys/default/D83DDC56.png index 6e6cdf4..3d3656b 100644 Binary files a/toxygen/smileys/default/D83DDC56.png and b/toxygen/smileys/default/D83DDC56.png differ diff --git a/toxygen/smileys/default/D83DDC57.png b/toxygen/smileys/default/D83DDC57.png index a795c98..14a9774 100644 Binary files a/toxygen/smileys/default/D83DDC57.png and b/toxygen/smileys/default/D83DDC57.png differ diff --git a/toxygen/smileys/default/D83DDC58.png b/toxygen/smileys/default/D83DDC58.png index 9f02a38..553cc6e 100644 Binary files a/toxygen/smileys/default/D83DDC58.png and b/toxygen/smileys/default/D83DDC58.png differ diff --git a/toxygen/smileys/default/D83DDC59.png b/toxygen/smileys/default/D83DDC59.png index 8ebca9a..4d2cfde 100644 Binary files a/toxygen/smileys/default/D83DDC59.png and b/toxygen/smileys/default/D83DDC59.png differ diff --git a/toxygen/smileys/default/D83DDC5A.png b/toxygen/smileys/default/D83DDC5A.png index b065c3b..f72b865 100644 Binary files a/toxygen/smileys/default/D83DDC5A.png and b/toxygen/smileys/default/D83DDC5A.png differ diff --git a/toxygen/smileys/default/D83DDC5B.png b/toxygen/smileys/default/D83DDC5B.png index 4fb1977..c5ea2dd 100644 Binary files a/toxygen/smileys/default/D83DDC5B.png and b/toxygen/smileys/default/D83DDC5B.png differ diff --git a/toxygen/smileys/default/D83DDC5C.png b/toxygen/smileys/default/D83DDC5C.png index 2dc62a9..4fad011 100644 Binary files a/toxygen/smileys/default/D83DDC5C.png and b/toxygen/smileys/default/D83DDC5C.png differ diff --git a/toxygen/smileys/default/D83DDC5D.png b/toxygen/smileys/default/D83DDC5D.png index ad2c7e2..ab72e00 100644 Binary files a/toxygen/smileys/default/D83DDC5D.png and b/toxygen/smileys/default/D83DDC5D.png differ diff --git a/toxygen/smileys/default/D83DDC5E.png b/toxygen/smileys/default/D83DDC5E.png index 260ffaf..a3cf22a 100644 Binary files a/toxygen/smileys/default/D83DDC5E.png and b/toxygen/smileys/default/D83DDC5E.png differ diff --git a/toxygen/smileys/default/D83DDC5F.png b/toxygen/smileys/default/D83DDC5F.png index 246033a..36f592b 100644 Binary files a/toxygen/smileys/default/D83DDC5F.png and b/toxygen/smileys/default/D83DDC5F.png differ diff --git a/toxygen/smileys/default/D83DDC60.png b/toxygen/smileys/default/D83DDC60.png index 069b0ba..03325c8 100644 Binary files a/toxygen/smileys/default/D83DDC60.png and b/toxygen/smileys/default/D83DDC60.png differ diff --git a/toxygen/smileys/default/D83DDC61.png b/toxygen/smileys/default/D83DDC61.png index c55056b..e565a42 100644 Binary files a/toxygen/smileys/default/D83DDC61.png and b/toxygen/smileys/default/D83DDC61.png differ diff --git a/toxygen/smileys/default/D83DDC62.png b/toxygen/smileys/default/D83DDC62.png index c024df0..445320f 100644 Binary files a/toxygen/smileys/default/D83DDC62.png and b/toxygen/smileys/default/D83DDC62.png differ diff --git a/toxygen/smileys/default/D83DDC63.png b/toxygen/smileys/default/D83DDC63.png index b9a69c7..171c4c6 100644 Binary files a/toxygen/smileys/default/D83DDC63.png and b/toxygen/smileys/default/D83DDC63.png differ diff --git a/toxygen/smileys/default/D83DDC64.png b/toxygen/smileys/default/D83DDC64.png index 8661c68..ebd2d98 100644 Binary files a/toxygen/smileys/default/D83DDC64.png and b/toxygen/smileys/default/D83DDC64.png differ diff --git a/toxygen/smileys/default/D83DDC65.png b/toxygen/smileys/default/D83DDC65.png index 90bc937..67d500e 100644 Binary files a/toxygen/smileys/default/D83DDC65.png and b/toxygen/smileys/default/D83DDC65.png differ diff --git a/toxygen/smileys/default/D83DDC66.png b/toxygen/smileys/default/D83DDC66.png index ae329b3..00b77bc 100644 Binary files a/toxygen/smileys/default/D83DDC66.png and b/toxygen/smileys/default/D83DDC66.png differ diff --git a/toxygen/smileys/default/D83DDC67.png b/toxygen/smileys/default/D83DDC67.png index 8d73e2a..162941f 100644 Binary files a/toxygen/smileys/default/D83DDC67.png and b/toxygen/smileys/default/D83DDC67.png differ diff --git a/toxygen/smileys/default/D83DDC68.png b/toxygen/smileys/default/D83DDC68.png index 1ae9332..37dfd2a 100644 Binary files a/toxygen/smileys/default/D83DDC68.png and b/toxygen/smileys/default/D83DDC68.png differ diff --git a/toxygen/smileys/default/D83DDC69.png b/toxygen/smileys/default/D83DDC69.png index 983e540..176ad8f 100644 Binary files a/toxygen/smileys/default/D83DDC69.png and b/toxygen/smileys/default/D83DDC69.png differ diff --git a/toxygen/smileys/default/D83DDC6A.png b/toxygen/smileys/default/D83DDC6A.png index 711c23f..1009ac8 100644 Binary files a/toxygen/smileys/default/D83DDC6A.png and b/toxygen/smileys/default/D83DDC6A.png differ diff --git a/toxygen/smileys/default/D83DDC6B.png b/toxygen/smileys/default/D83DDC6B.png index 11e9b49..be243b4 100644 Binary files a/toxygen/smileys/default/D83DDC6B.png and b/toxygen/smileys/default/D83DDC6B.png differ diff --git a/toxygen/smileys/default/D83DDC6C.png b/toxygen/smileys/default/D83DDC6C.png index 4fa7870..9a262f1 100644 Binary files a/toxygen/smileys/default/D83DDC6C.png and b/toxygen/smileys/default/D83DDC6C.png differ diff --git a/toxygen/smileys/default/D83DDC6D.png b/toxygen/smileys/default/D83DDC6D.png index caf627a..217da23 100644 Binary files a/toxygen/smileys/default/D83DDC6D.png and b/toxygen/smileys/default/D83DDC6D.png differ diff --git a/toxygen/smileys/default/D83DDC6E.png b/toxygen/smileys/default/D83DDC6E.png index b321f21..f389f63 100644 Binary files a/toxygen/smileys/default/D83DDC6E.png and b/toxygen/smileys/default/D83DDC6E.png differ diff --git a/toxygen/smileys/default/D83DDC6F.png b/toxygen/smileys/default/D83DDC6F.png index 9575084..6d1645b 100644 Binary files a/toxygen/smileys/default/D83DDC6F.png and b/toxygen/smileys/default/D83DDC6F.png differ diff --git a/toxygen/smileys/default/D83DDC70.png b/toxygen/smileys/default/D83DDC70.png index 2125032..1311170 100644 Binary files a/toxygen/smileys/default/D83DDC70.png and b/toxygen/smileys/default/D83DDC70.png differ diff --git a/toxygen/smileys/default/D83DDC71.png b/toxygen/smileys/default/D83DDC71.png index 79394f1..ca207b0 100644 Binary files a/toxygen/smileys/default/D83DDC71.png and b/toxygen/smileys/default/D83DDC71.png differ diff --git a/toxygen/smileys/default/D83DDC72.png b/toxygen/smileys/default/D83DDC72.png index 23686f7..86dc325 100644 Binary files a/toxygen/smileys/default/D83DDC72.png and b/toxygen/smileys/default/D83DDC72.png differ diff --git a/toxygen/smileys/default/D83DDC73.png b/toxygen/smileys/default/D83DDC73.png index 8d81068..c5aada5 100644 Binary files a/toxygen/smileys/default/D83DDC73.png and b/toxygen/smileys/default/D83DDC73.png differ diff --git a/toxygen/smileys/default/D83DDC74.png b/toxygen/smileys/default/D83DDC74.png index b3de20e..e007082 100644 Binary files a/toxygen/smileys/default/D83DDC74.png and b/toxygen/smileys/default/D83DDC74.png differ diff --git a/toxygen/smileys/default/D83DDC75.png b/toxygen/smileys/default/D83DDC75.png index 78898d7..1c70b19 100644 Binary files a/toxygen/smileys/default/D83DDC75.png and b/toxygen/smileys/default/D83DDC75.png differ diff --git a/toxygen/smileys/default/D83DDC76.png b/toxygen/smileys/default/D83DDC76.png index 72f543c..3b23af4 100644 Binary files a/toxygen/smileys/default/D83DDC76.png and b/toxygen/smileys/default/D83DDC76.png differ diff --git a/toxygen/smileys/default/D83DDC77.png b/toxygen/smileys/default/D83DDC77.png index e1b062e..65e3966 100644 Binary files a/toxygen/smileys/default/D83DDC77.png and b/toxygen/smileys/default/D83DDC77.png differ diff --git a/toxygen/smileys/default/D83DDC78.png b/toxygen/smileys/default/D83DDC78.png index 622e525..ccd0a43 100644 Binary files a/toxygen/smileys/default/D83DDC78.png and b/toxygen/smileys/default/D83DDC78.png differ diff --git a/toxygen/smileys/default/D83DDC79.png b/toxygen/smileys/default/D83DDC79.png index b5b0ea2..5b3e009 100644 Binary files a/toxygen/smileys/default/D83DDC79.png and b/toxygen/smileys/default/D83DDC79.png differ diff --git a/toxygen/smileys/default/D83DDC7A.png b/toxygen/smileys/default/D83DDC7A.png index ac66041..e649501 100644 Binary files a/toxygen/smileys/default/D83DDC7A.png and b/toxygen/smileys/default/D83DDC7A.png differ diff --git a/toxygen/smileys/default/D83DDC7B.png b/toxygen/smileys/default/D83DDC7B.png index f1f17be..abc5fe2 100644 Binary files a/toxygen/smileys/default/D83DDC7B.png and b/toxygen/smileys/default/D83DDC7B.png differ diff --git a/toxygen/smileys/default/D83DDC7C.png b/toxygen/smileys/default/D83DDC7C.png index 71683c9..4dec37d 100644 Binary files a/toxygen/smileys/default/D83DDC7C.png and b/toxygen/smileys/default/D83DDC7C.png differ diff --git a/toxygen/smileys/default/D83DDC7D.png b/toxygen/smileys/default/D83DDC7D.png index 6c4630f..57db9bb 100644 Binary files a/toxygen/smileys/default/D83DDC7D.png and b/toxygen/smileys/default/D83DDC7D.png differ diff --git a/toxygen/smileys/default/D83DDC7E.png b/toxygen/smileys/default/D83DDC7E.png index 8151fb6..854cae3 100644 Binary files a/toxygen/smileys/default/D83DDC7E.png and b/toxygen/smileys/default/D83DDC7E.png differ diff --git a/toxygen/smileys/default/D83DDC7F.png b/toxygen/smileys/default/D83DDC7F.png index 47ae002..6283942 100644 Binary files a/toxygen/smileys/default/D83DDC7F.png and b/toxygen/smileys/default/D83DDC7F.png differ diff --git a/toxygen/smileys/default/D83DDC80.png b/toxygen/smileys/default/D83DDC80.png index 5dbecd7..73f61d9 100644 Binary files a/toxygen/smileys/default/D83DDC80.png and b/toxygen/smileys/default/D83DDC80.png differ diff --git a/toxygen/smileys/default/D83DDC81.png b/toxygen/smileys/default/D83DDC81.png index f8a8ea5..ec18497 100644 Binary files a/toxygen/smileys/default/D83DDC81.png and b/toxygen/smileys/default/D83DDC81.png differ diff --git a/toxygen/smileys/default/D83DDC82.png b/toxygen/smileys/default/D83DDC82.png index 94dcdec..4591862 100644 Binary files a/toxygen/smileys/default/D83DDC82.png and b/toxygen/smileys/default/D83DDC82.png differ diff --git a/toxygen/smileys/default/D83DDC83.png b/toxygen/smileys/default/D83DDC83.png index 4294502..cae7c04 100644 Binary files a/toxygen/smileys/default/D83DDC83.png and b/toxygen/smileys/default/D83DDC83.png differ diff --git a/toxygen/smileys/default/D83DDC84.png b/toxygen/smileys/default/D83DDC84.png index a4c6036..514f9b0 100644 Binary files a/toxygen/smileys/default/D83DDC84.png and b/toxygen/smileys/default/D83DDC84.png differ diff --git a/toxygen/smileys/default/D83DDC85.png b/toxygen/smileys/default/D83DDC85.png index 504a06e..9d85f43 100644 Binary files a/toxygen/smileys/default/D83DDC85.png and b/toxygen/smileys/default/D83DDC85.png differ diff --git a/toxygen/smileys/default/D83DDC86.png b/toxygen/smileys/default/D83DDC86.png index ebdd6ab..45b22d1 100644 Binary files a/toxygen/smileys/default/D83DDC86.png and b/toxygen/smileys/default/D83DDC86.png differ diff --git a/toxygen/smileys/default/D83DDC87.png b/toxygen/smileys/default/D83DDC87.png index 2b05cff..aa8ac45 100644 Binary files a/toxygen/smileys/default/D83DDC87.png and b/toxygen/smileys/default/D83DDC87.png differ diff --git a/toxygen/smileys/default/D83DDC88.png b/toxygen/smileys/default/D83DDC88.png index cf3e845..0491543 100644 Binary files a/toxygen/smileys/default/D83DDC88.png and b/toxygen/smileys/default/D83DDC88.png differ diff --git a/toxygen/smileys/default/D83DDC89.png b/toxygen/smileys/default/D83DDC89.png index ba2b624..c2151a2 100644 Binary files a/toxygen/smileys/default/D83DDC89.png and b/toxygen/smileys/default/D83DDC89.png differ diff --git a/toxygen/smileys/default/D83DDC8A.png b/toxygen/smileys/default/D83DDC8A.png index 950a9fb..1ee7330 100644 Binary files a/toxygen/smileys/default/D83DDC8A.png and b/toxygen/smileys/default/D83DDC8A.png differ diff --git a/toxygen/smileys/default/D83DDC8B.png b/toxygen/smileys/default/D83DDC8B.png index 620a4e5..c2ae15e 100644 Binary files a/toxygen/smileys/default/D83DDC8B.png and b/toxygen/smileys/default/D83DDC8B.png differ diff --git a/toxygen/smileys/default/D83DDC8C.png b/toxygen/smileys/default/D83DDC8C.png index ed94152..9a0a3eb 100644 Binary files a/toxygen/smileys/default/D83DDC8C.png and b/toxygen/smileys/default/D83DDC8C.png differ diff --git a/toxygen/smileys/default/D83DDC8D.png b/toxygen/smileys/default/D83DDC8D.png index 1c90ba0..cde47d9 100644 Binary files a/toxygen/smileys/default/D83DDC8D.png and b/toxygen/smileys/default/D83DDC8D.png differ diff --git a/toxygen/smileys/default/D83DDC8E.png b/toxygen/smileys/default/D83DDC8E.png index 379a76d..d17d19c 100644 Binary files a/toxygen/smileys/default/D83DDC8E.png and b/toxygen/smileys/default/D83DDC8E.png differ diff --git a/toxygen/smileys/default/D83DDC8F.png b/toxygen/smileys/default/D83DDC8F.png index 8837f68..af81a7f 100644 Binary files a/toxygen/smileys/default/D83DDC8F.png and b/toxygen/smileys/default/D83DDC8F.png differ diff --git a/toxygen/smileys/default/D83DDC90.png b/toxygen/smileys/default/D83DDC90.png index 2ee1054..41d16a3 100644 Binary files a/toxygen/smileys/default/D83DDC90.png and b/toxygen/smileys/default/D83DDC90.png differ diff --git a/toxygen/smileys/default/D83DDC91.png b/toxygen/smileys/default/D83DDC91.png index e8638cc..2654b92 100644 Binary files a/toxygen/smileys/default/D83DDC91.png and b/toxygen/smileys/default/D83DDC91.png differ diff --git a/toxygen/smileys/default/D83DDC92.png b/toxygen/smileys/default/D83DDC92.png index 621b28b..9146473 100644 Binary files a/toxygen/smileys/default/D83DDC92.png and b/toxygen/smileys/default/D83DDC92.png differ diff --git a/toxygen/smileys/default/D83DDC93.png b/toxygen/smileys/default/D83DDC93.png index b6443f8..cf1b001 100644 Binary files a/toxygen/smileys/default/D83DDC93.png and b/toxygen/smileys/default/D83DDC93.png differ diff --git a/toxygen/smileys/default/D83DDC94.png b/toxygen/smileys/default/D83DDC94.png index b2c521a..17a5bd9 100644 Binary files a/toxygen/smileys/default/D83DDC94.png and b/toxygen/smileys/default/D83DDC94.png differ diff --git a/toxygen/smileys/default/D83DDC95.png b/toxygen/smileys/default/D83DDC95.png index bef3d7c..8757fb1 100644 Binary files a/toxygen/smileys/default/D83DDC95.png and b/toxygen/smileys/default/D83DDC95.png differ diff --git a/toxygen/smileys/default/D83DDC96.png b/toxygen/smileys/default/D83DDC96.png index 161fd16..1dda2ee 100644 Binary files a/toxygen/smileys/default/D83DDC96.png and b/toxygen/smileys/default/D83DDC96.png differ diff --git a/toxygen/smileys/default/D83DDC97.png b/toxygen/smileys/default/D83DDC97.png index aaa9839..7baa800 100644 Binary files a/toxygen/smileys/default/D83DDC97.png and b/toxygen/smileys/default/D83DDC97.png differ diff --git a/toxygen/smileys/default/D83DDC98.png b/toxygen/smileys/default/D83DDC98.png index 1459cdd..2cee5aa 100644 Binary files a/toxygen/smileys/default/D83DDC98.png and b/toxygen/smileys/default/D83DDC98.png differ diff --git a/toxygen/smileys/default/D83DDC99.png b/toxygen/smileys/default/D83DDC99.png index dc7c449..e9ab2c1 100644 Binary files a/toxygen/smileys/default/D83DDC99.png and b/toxygen/smileys/default/D83DDC99.png differ diff --git a/toxygen/smileys/default/D83DDC9A.png b/toxygen/smileys/default/D83DDC9A.png index 1100ab0..9f94d53 100644 Binary files a/toxygen/smileys/default/D83DDC9A.png and b/toxygen/smileys/default/D83DDC9A.png differ diff --git a/toxygen/smileys/default/D83DDC9B.png b/toxygen/smileys/default/D83DDC9B.png index ce1b877..77174a5 100644 Binary files a/toxygen/smileys/default/D83DDC9B.png and b/toxygen/smileys/default/D83DDC9B.png differ diff --git a/toxygen/smileys/default/D83DDC9C.png b/toxygen/smileys/default/D83DDC9C.png index 0d9d147..207d7d3 100644 Binary files a/toxygen/smileys/default/D83DDC9C.png and b/toxygen/smileys/default/D83DDC9C.png differ diff --git a/toxygen/smileys/default/D83DDC9D.png b/toxygen/smileys/default/D83DDC9D.png index 148421e..908575c 100644 Binary files a/toxygen/smileys/default/D83DDC9D.png and b/toxygen/smileys/default/D83DDC9D.png differ diff --git a/toxygen/smileys/default/D83DDC9E.png b/toxygen/smileys/default/D83DDC9E.png index 030ceb5..d0d1292 100644 Binary files a/toxygen/smileys/default/D83DDC9E.png and b/toxygen/smileys/default/D83DDC9E.png differ diff --git a/toxygen/smileys/default/D83DDC9F.png b/toxygen/smileys/default/D83DDC9F.png index 11d897a..c4d1c4e 100644 Binary files a/toxygen/smileys/default/D83DDC9F.png and b/toxygen/smileys/default/D83DDC9F.png differ diff --git a/toxygen/smileys/default/D83DDCA0.png b/toxygen/smileys/default/D83DDCA0.png index f596e31..fc2c29f 100644 Binary files a/toxygen/smileys/default/D83DDCA0.png and b/toxygen/smileys/default/D83DDCA0.png differ diff --git a/toxygen/smileys/default/D83DDCA1.png b/toxygen/smileys/default/D83DDCA1.png index d2cb0f2..57a5d7f 100644 Binary files a/toxygen/smileys/default/D83DDCA1.png and b/toxygen/smileys/default/D83DDCA1.png differ diff --git a/toxygen/smileys/default/D83DDCA2.png b/toxygen/smileys/default/D83DDCA2.png index e232809..cff291f 100644 Binary files a/toxygen/smileys/default/D83DDCA2.png and b/toxygen/smileys/default/D83DDCA2.png differ diff --git a/toxygen/smileys/default/D83DDCA3.png b/toxygen/smileys/default/D83DDCA3.png index 2480754..2b943e9 100644 Binary files a/toxygen/smileys/default/D83DDCA3.png and b/toxygen/smileys/default/D83DDCA3.png differ diff --git a/toxygen/smileys/default/D83DDCA4.png b/toxygen/smileys/default/D83DDCA4.png index 04fa05f..d25ffff 100644 Binary files a/toxygen/smileys/default/D83DDCA4.png and b/toxygen/smileys/default/D83DDCA4.png differ diff --git a/toxygen/smileys/default/D83DDCA5.png b/toxygen/smileys/default/D83DDCA5.png index 7fbed7d..4db5a0e 100644 Binary files a/toxygen/smileys/default/D83DDCA5.png and b/toxygen/smileys/default/D83DDCA5.png differ diff --git a/toxygen/smileys/default/D83DDCA6.png b/toxygen/smileys/default/D83DDCA6.png index d4b4dde..758ce6d 100644 Binary files a/toxygen/smileys/default/D83DDCA6.png and b/toxygen/smileys/default/D83DDCA6.png differ diff --git a/toxygen/smileys/default/D83DDCA7.png b/toxygen/smileys/default/D83DDCA7.png index 1602702..74c1d2b 100644 Binary files a/toxygen/smileys/default/D83DDCA7.png and b/toxygen/smileys/default/D83DDCA7.png differ diff --git a/toxygen/smileys/default/D83DDCA8.png b/toxygen/smileys/default/D83DDCA8.png index c1d6de3..f8039e1 100644 Binary files a/toxygen/smileys/default/D83DDCA8.png and b/toxygen/smileys/default/D83DDCA8.png differ diff --git a/toxygen/smileys/default/D83DDCA9.png b/toxygen/smileys/default/D83DDCA9.png index e1a4e26..a86877f 100644 Binary files a/toxygen/smileys/default/D83DDCA9.png and b/toxygen/smileys/default/D83DDCA9.png differ diff --git a/toxygen/smileys/default/D83DDCAA.png b/toxygen/smileys/default/D83DDCAA.png index 50a329b..5a1e68d 100644 Binary files a/toxygen/smileys/default/D83DDCAA.png and b/toxygen/smileys/default/D83DDCAA.png differ diff --git a/toxygen/smileys/default/D83DDCAB.png b/toxygen/smileys/default/D83DDCAB.png index ab3e93e..999a667 100644 Binary files a/toxygen/smileys/default/D83DDCAB.png and b/toxygen/smileys/default/D83DDCAB.png differ diff --git a/toxygen/smileys/default/D83DDCAC.png b/toxygen/smileys/default/D83DDCAC.png index d9d38ca..effcbbe 100644 Binary files a/toxygen/smileys/default/D83DDCAC.png and b/toxygen/smileys/default/D83DDCAC.png differ diff --git a/toxygen/smileys/default/D83DDCAD.png b/toxygen/smileys/default/D83DDCAD.png index 9f2dbd6..f23fd2b 100644 Binary files a/toxygen/smileys/default/D83DDCAD.png and b/toxygen/smileys/default/D83DDCAD.png differ diff --git a/toxygen/smileys/default/D83DDCAE.png b/toxygen/smileys/default/D83DDCAE.png index f28bd12..b9af846 100644 Binary files a/toxygen/smileys/default/D83DDCAE.png and b/toxygen/smileys/default/D83DDCAE.png differ diff --git a/toxygen/smileys/default/D83DDCAF.png b/toxygen/smileys/default/D83DDCAF.png index 2a48074..5fb8824 100644 Binary files a/toxygen/smileys/default/D83DDCAF.png and b/toxygen/smileys/default/D83DDCAF.png differ diff --git a/toxygen/smileys/default/D83DDCB0.png b/toxygen/smileys/default/D83DDCB0.png index 6fe6259..4b7d9ad 100644 Binary files a/toxygen/smileys/default/D83DDCB0.png and b/toxygen/smileys/default/D83DDCB0.png differ diff --git a/toxygen/smileys/default/D83DDCB1.png b/toxygen/smileys/default/D83DDCB1.png index ae95bc2..fea9346 100644 Binary files a/toxygen/smileys/default/D83DDCB1.png and b/toxygen/smileys/default/D83DDCB1.png differ diff --git a/toxygen/smileys/default/D83DDCB2.png b/toxygen/smileys/default/D83DDCB2.png index 6b2c85a..4e83e77 100644 Binary files a/toxygen/smileys/default/D83DDCB2.png and b/toxygen/smileys/default/D83DDCB2.png differ diff --git a/toxygen/smileys/default/D83DDCB3.png b/toxygen/smileys/default/D83DDCB3.png index 6976b53..6141cec 100644 Binary files a/toxygen/smileys/default/D83DDCB3.png and b/toxygen/smileys/default/D83DDCB3.png differ diff --git a/toxygen/smileys/default/D83DDCB4.png b/toxygen/smileys/default/D83DDCB4.png index 7d94640..9f6bda2 100644 Binary files a/toxygen/smileys/default/D83DDCB4.png and b/toxygen/smileys/default/D83DDCB4.png differ diff --git a/toxygen/smileys/default/D83DDCB5.png b/toxygen/smileys/default/D83DDCB5.png index 92f8caf..d27fb53 100644 Binary files a/toxygen/smileys/default/D83DDCB5.png and b/toxygen/smileys/default/D83DDCB5.png differ diff --git a/toxygen/smileys/default/D83DDCB6.png b/toxygen/smileys/default/D83DDCB6.png index d47427b..b4d6405 100644 Binary files a/toxygen/smileys/default/D83DDCB6.png and b/toxygen/smileys/default/D83DDCB6.png differ diff --git a/toxygen/smileys/default/D83DDCB7.png b/toxygen/smileys/default/D83DDCB7.png index 1e7679c..e1f5526 100644 Binary files a/toxygen/smileys/default/D83DDCB7.png and b/toxygen/smileys/default/D83DDCB7.png differ diff --git a/toxygen/smileys/default/D83DDCB8.png b/toxygen/smileys/default/D83DDCB8.png index 10b4518..20240f8 100644 Binary files a/toxygen/smileys/default/D83DDCB8.png and b/toxygen/smileys/default/D83DDCB8.png differ diff --git a/toxygen/smileys/default/D83DDCB9.png b/toxygen/smileys/default/D83DDCB9.png index 63ea27d..ba319c9 100644 Binary files a/toxygen/smileys/default/D83DDCB9.png and b/toxygen/smileys/default/D83DDCB9.png differ diff --git a/toxygen/smileys/default/D83DDCBA.png b/toxygen/smileys/default/D83DDCBA.png index a3ba77d..4a9e280 100644 Binary files a/toxygen/smileys/default/D83DDCBA.png and b/toxygen/smileys/default/D83DDCBA.png differ diff --git a/toxygen/smileys/default/D83DDCBB.png b/toxygen/smileys/default/D83DDCBB.png index feb6e40..d4f6546 100644 Binary files a/toxygen/smileys/default/D83DDCBB.png and b/toxygen/smileys/default/D83DDCBB.png differ diff --git a/toxygen/smileys/default/D83DDCBC.png b/toxygen/smileys/default/D83DDCBC.png index ec6ce62..4f7011c 100644 Binary files a/toxygen/smileys/default/D83DDCBC.png and b/toxygen/smileys/default/D83DDCBC.png differ diff --git a/toxygen/smileys/default/D83DDCBD.png b/toxygen/smileys/default/D83DDCBD.png index 11ba64a..d2e416e 100644 Binary files a/toxygen/smileys/default/D83DDCBD.png and b/toxygen/smileys/default/D83DDCBD.png differ diff --git a/toxygen/smileys/default/D83DDCBE.png b/toxygen/smileys/default/D83DDCBE.png index 6137dff..de1a1c0 100644 Binary files a/toxygen/smileys/default/D83DDCBE.png and b/toxygen/smileys/default/D83DDCBE.png differ diff --git a/toxygen/smileys/default/D83DDCBF.png b/toxygen/smileys/default/D83DDCBF.png index d50b58b..38c906b 100644 Binary files a/toxygen/smileys/default/D83DDCBF.png and b/toxygen/smileys/default/D83DDCBF.png differ diff --git a/toxygen/smileys/default/D83DDCC0.png b/toxygen/smileys/default/D83DDCC0.png index 5a76a4c..da3cd5d 100644 Binary files a/toxygen/smileys/default/D83DDCC0.png and b/toxygen/smileys/default/D83DDCC0.png differ diff --git a/toxygen/smileys/default/D83DDCC1.png b/toxygen/smileys/default/D83DDCC1.png index 29f32f3..f37868d 100644 Binary files a/toxygen/smileys/default/D83DDCC1.png and b/toxygen/smileys/default/D83DDCC1.png differ diff --git a/toxygen/smileys/default/D83DDCC2.png b/toxygen/smileys/default/D83DDCC2.png index aae823b..4b727dd 100644 Binary files a/toxygen/smileys/default/D83DDCC2.png and b/toxygen/smileys/default/D83DDCC2.png differ diff --git a/toxygen/smileys/default/D83DDCC3.png b/toxygen/smileys/default/D83DDCC3.png index 902cf6b..08f5dc1 100644 Binary files a/toxygen/smileys/default/D83DDCC3.png and b/toxygen/smileys/default/D83DDCC3.png differ diff --git a/toxygen/smileys/default/D83DDCC4.png b/toxygen/smileys/default/D83DDCC4.png index ef8e394..33665a1 100644 Binary files a/toxygen/smileys/default/D83DDCC4.png and b/toxygen/smileys/default/D83DDCC4.png differ diff --git a/toxygen/smileys/default/D83DDCC5.png b/toxygen/smileys/default/D83DDCC5.png index 63cca6c..b4c0e8c 100644 Binary files a/toxygen/smileys/default/D83DDCC5.png and b/toxygen/smileys/default/D83DDCC5.png differ diff --git a/toxygen/smileys/default/D83DDCC6.png b/toxygen/smileys/default/D83DDCC6.png index 56da2d6..698aabb 100644 Binary files a/toxygen/smileys/default/D83DDCC6.png and b/toxygen/smileys/default/D83DDCC6.png differ diff --git a/toxygen/smileys/default/D83DDCC7.png b/toxygen/smileys/default/D83DDCC7.png index f9519fd..e1b35a1 100644 Binary files a/toxygen/smileys/default/D83DDCC7.png and b/toxygen/smileys/default/D83DDCC7.png differ diff --git a/toxygen/smileys/default/D83DDCC8.png b/toxygen/smileys/default/D83DDCC8.png index 22100cb..ddaa706 100644 Binary files a/toxygen/smileys/default/D83DDCC8.png and b/toxygen/smileys/default/D83DDCC8.png differ diff --git a/toxygen/smileys/default/D83DDCC9.png b/toxygen/smileys/default/D83DDCC9.png index ff5eca4..7b956c6 100644 Binary files a/toxygen/smileys/default/D83DDCC9.png and b/toxygen/smileys/default/D83DDCC9.png differ diff --git a/toxygen/smileys/default/D83DDCCA.png b/toxygen/smileys/default/D83DDCCA.png index d67cb31..4778f38 100644 Binary files a/toxygen/smileys/default/D83DDCCA.png and b/toxygen/smileys/default/D83DDCCA.png differ diff --git a/toxygen/smileys/default/D83DDCCB.png b/toxygen/smileys/default/D83DDCCB.png index ee94954..2d0720d 100644 Binary files a/toxygen/smileys/default/D83DDCCB.png and b/toxygen/smileys/default/D83DDCCB.png differ diff --git a/toxygen/smileys/default/D83DDCCC.png b/toxygen/smileys/default/D83DDCCC.png index c880d4b..9735eca 100644 Binary files a/toxygen/smileys/default/D83DDCCC.png and b/toxygen/smileys/default/D83DDCCC.png differ diff --git a/toxygen/smileys/default/D83DDCCD.png b/toxygen/smileys/default/D83DDCCD.png index 918bf6a..f50854a 100644 Binary files a/toxygen/smileys/default/D83DDCCD.png and b/toxygen/smileys/default/D83DDCCD.png differ diff --git a/toxygen/smileys/default/D83DDCCE.png b/toxygen/smileys/default/D83DDCCE.png index 7fd4ef8..ce86e8b 100644 Binary files a/toxygen/smileys/default/D83DDCCE.png and b/toxygen/smileys/default/D83DDCCE.png differ diff --git a/toxygen/smileys/default/D83DDCCF.png b/toxygen/smileys/default/D83DDCCF.png index 62159a8..8aa5e8f 100644 Binary files a/toxygen/smileys/default/D83DDCCF.png and b/toxygen/smileys/default/D83DDCCF.png differ diff --git a/toxygen/smileys/default/D83DDCD0.png b/toxygen/smileys/default/D83DDCD0.png index 21d5db7..f637998 100644 Binary files a/toxygen/smileys/default/D83DDCD0.png and b/toxygen/smileys/default/D83DDCD0.png differ diff --git a/toxygen/smileys/default/D83DDCD1.png b/toxygen/smileys/default/D83DDCD1.png index 5b4e246..c0a4b77 100644 Binary files a/toxygen/smileys/default/D83DDCD1.png and b/toxygen/smileys/default/D83DDCD1.png differ diff --git a/toxygen/smileys/default/D83DDCD2.png b/toxygen/smileys/default/D83DDCD2.png index 9f5585b..400cf7b 100644 Binary files a/toxygen/smileys/default/D83DDCD2.png and b/toxygen/smileys/default/D83DDCD2.png differ diff --git a/toxygen/smileys/default/D83DDCD3.png b/toxygen/smileys/default/D83DDCD3.png index d045646..930e01f 100644 Binary files a/toxygen/smileys/default/D83DDCD3.png and b/toxygen/smileys/default/D83DDCD3.png differ diff --git a/toxygen/smileys/default/D83DDCD4.png b/toxygen/smileys/default/D83DDCD4.png index 3f988be..b26265e 100644 Binary files a/toxygen/smileys/default/D83DDCD4.png and b/toxygen/smileys/default/D83DDCD4.png differ diff --git a/toxygen/smileys/default/D83DDCD5.png b/toxygen/smileys/default/D83DDCD5.png index 5da1fd4..06d3364 100644 Binary files a/toxygen/smileys/default/D83DDCD5.png and b/toxygen/smileys/default/D83DDCD5.png differ diff --git a/toxygen/smileys/default/D83DDCD6.png b/toxygen/smileys/default/D83DDCD6.png index 9d187a4..be0ef9c 100644 Binary files a/toxygen/smileys/default/D83DDCD6.png and b/toxygen/smileys/default/D83DDCD6.png differ diff --git a/toxygen/smileys/default/D83DDCD7.png b/toxygen/smileys/default/D83DDCD7.png index 7546b7c..1b3f7b7 100644 Binary files a/toxygen/smileys/default/D83DDCD7.png and b/toxygen/smileys/default/D83DDCD7.png differ diff --git a/toxygen/smileys/default/D83DDCD8.png b/toxygen/smileys/default/D83DDCD8.png index bec3da5..7cb1ac9 100644 Binary files a/toxygen/smileys/default/D83DDCD8.png and b/toxygen/smileys/default/D83DDCD8.png differ diff --git a/toxygen/smileys/default/D83DDCD9.png b/toxygen/smileys/default/D83DDCD9.png index 7004cc8..ecf7d46 100644 Binary files a/toxygen/smileys/default/D83DDCD9.png and b/toxygen/smileys/default/D83DDCD9.png differ diff --git a/toxygen/smileys/default/D83DDCDA.png b/toxygen/smileys/default/D83DDCDA.png index 66f8c39..2ebfaf0 100644 Binary files a/toxygen/smileys/default/D83DDCDA.png and b/toxygen/smileys/default/D83DDCDA.png differ diff --git a/toxygen/smileys/default/D83DDCDB.png b/toxygen/smileys/default/D83DDCDB.png index 4445168..36a9b0f 100644 Binary files a/toxygen/smileys/default/D83DDCDB.png and b/toxygen/smileys/default/D83DDCDB.png differ diff --git a/toxygen/smileys/default/D83DDCDC.png b/toxygen/smileys/default/D83DDCDC.png index 64d1bfb..056647b 100644 Binary files a/toxygen/smileys/default/D83DDCDC.png and b/toxygen/smileys/default/D83DDCDC.png differ diff --git a/toxygen/smileys/default/D83DDCDD.png b/toxygen/smileys/default/D83DDCDD.png index 418fd8c..35e9942 100644 Binary files a/toxygen/smileys/default/D83DDCDD.png and b/toxygen/smileys/default/D83DDCDD.png differ diff --git a/toxygen/smileys/default/D83DDCDE.png b/toxygen/smileys/default/D83DDCDE.png index a38c396..20ba9ba 100644 Binary files a/toxygen/smileys/default/D83DDCDE.png and b/toxygen/smileys/default/D83DDCDE.png differ diff --git a/toxygen/smileys/default/D83DDCDF.png b/toxygen/smileys/default/D83DDCDF.png index 4d557ff..8d932d2 100644 Binary files a/toxygen/smileys/default/D83DDCDF.png and b/toxygen/smileys/default/D83DDCDF.png differ diff --git a/toxygen/smileys/default/D83DDCE0.png b/toxygen/smileys/default/D83DDCE0.png index f3cfa40..781669e 100644 Binary files a/toxygen/smileys/default/D83DDCE0.png and b/toxygen/smileys/default/D83DDCE0.png differ diff --git a/toxygen/smileys/default/D83DDCE1.png b/toxygen/smileys/default/D83DDCE1.png index b690973..c2a3bc9 100644 Binary files a/toxygen/smileys/default/D83DDCE1.png and b/toxygen/smileys/default/D83DDCE1.png differ diff --git a/toxygen/smileys/default/D83DDCE2.png b/toxygen/smileys/default/D83DDCE2.png index 0ff4ad0..4c3be3e 100644 Binary files a/toxygen/smileys/default/D83DDCE2.png and b/toxygen/smileys/default/D83DDCE2.png differ diff --git a/toxygen/smileys/default/D83DDCE3.png b/toxygen/smileys/default/D83DDCE3.png index aa3537c..5847867 100644 Binary files a/toxygen/smileys/default/D83DDCE3.png and b/toxygen/smileys/default/D83DDCE3.png differ diff --git a/toxygen/smileys/default/D83DDCE4.png b/toxygen/smileys/default/D83DDCE4.png index b54ab57..0e6254d 100644 Binary files a/toxygen/smileys/default/D83DDCE4.png and b/toxygen/smileys/default/D83DDCE4.png differ diff --git a/toxygen/smileys/default/D83DDCE5.png b/toxygen/smileys/default/D83DDCE5.png index 3e3c172..6a731d1 100644 Binary files a/toxygen/smileys/default/D83DDCE5.png and b/toxygen/smileys/default/D83DDCE5.png differ diff --git a/toxygen/smileys/default/D83DDCE6.png b/toxygen/smileys/default/D83DDCE6.png index f087231..4d3f701 100644 Binary files a/toxygen/smileys/default/D83DDCE6.png and b/toxygen/smileys/default/D83DDCE6.png differ diff --git a/toxygen/smileys/default/D83DDCE7.png b/toxygen/smileys/default/D83DDCE7.png index 6855487..5bd2454 100644 Binary files a/toxygen/smileys/default/D83DDCE7.png and b/toxygen/smileys/default/D83DDCE7.png differ diff --git a/toxygen/smileys/default/D83DDCE8.png b/toxygen/smileys/default/D83DDCE8.png index 8b185e7..446ff97 100644 Binary files a/toxygen/smileys/default/D83DDCE8.png and b/toxygen/smileys/default/D83DDCE8.png differ diff --git a/toxygen/smileys/default/D83DDCE9.png b/toxygen/smileys/default/D83DDCE9.png index 329d08e..b7b83f5 100644 Binary files a/toxygen/smileys/default/D83DDCE9.png and b/toxygen/smileys/default/D83DDCE9.png differ diff --git a/toxygen/smileys/default/D83DDCEA.png b/toxygen/smileys/default/D83DDCEA.png index 1855bf2..ec474be 100644 Binary files a/toxygen/smileys/default/D83DDCEA.png and b/toxygen/smileys/default/D83DDCEA.png differ diff --git a/toxygen/smileys/default/D83DDCEB.png b/toxygen/smileys/default/D83DDCEB.png index 831daf7..4239a5a 100644 Binary files a/toxygen/smileys/default/D83DDCEB.png and b/toxygen/smileys/default/D83DDCEB.png differ diff --git a/toxygen/smileys/default/D83DDCEC.png b/toxygen/smileys/default/D83DDCEC.png index 8b04b2d..4289c26 100644 Binary files a/toxygen/smileys/default/D83DDCEC.png and b/toxygen/smileys/default/D83DDCEC.png differ diff --git a/toxygen/smileys/default/D83DDCED.png b/toxygen/smileys/default/D83DDCED.png index 0e50de2..2084740 100644 Binary files a/toxygen/smileys/default/D83DDCED.png and b/toxygen/smileys/default/D83DDCED.png differ diff --git a/toxygen/smileys/default/D83DDCEE.png b/toxygen/smileys/default/D83DDCEE.png index 7213a4e..e50f686 100644 Binary files a/toxygen/smileys/default/D83DDCEE.png and b/toxygen/smileys/default/D83DDCEE.png differ diff --git a/toxygen/smileys/default/D83DDCEF.png b/toxygen/smileys/default/D83DDCEF.png index 370aef4..2e33772 100644 Binary files a/toxygen/smileys/default/D83DDCEF.png and b/toxygen/smileys/default/D83DDCEF.png differ diff --git a/toxygen/smileys/default/D83DDCF0.png b/toxygen/smileys/default/D83DDCF0.png index 8d12ebe..016fa96 100644 Binary files a/toxygen/smileys/default/D83DDCF0.png and b/toxygen/smileys/default/D83DDCF0.png differ diff --git a/toxygen/smileys/default/D83DDCF1.png b/toxygen/smileys/default/D83DDCF1.png index 3571e61..cc722ad 100644 Binary files a/toxygen/smileys/default/D83DDCF1.png and b/toxygen/smileys/default/D83DDCF1.png differ diff --git a/toxygen/smileys/default/D83DDCF2.png b/toxygen/smileys/default/D83DDCF2.png index ab7fc38..c954661 100644 Binary files a/toxygen/smileys/default/D83DDCF2.png and b/toxygen/smileys/default/D83DDCF2.png differ diff --git a/toxygen/smileys/default/D83DDCF3.png b/toxygen/smileys/default/D83DDCF3.png index 2fd96ff..687897b 100644 Binary files a/toxygen/smileys/default/D83DDCF3.png and b/toxygen/smileys/default/D83DDCF3.png differ diff --git a/toxygen/smileys/default/D83DDCF4.png b/toxygen/smileys/default/D83DDCF4.png index 41aa227..0547aba 100644 Binary files a/toxygen/smileys/default/D83DDCF4.png and b/toxygen/smileys/default/D83DDCF4.png differ diff --git a/toxygen/smileys/default/D83DDCF5.png b/toxygen/smileys/default/D83DDCF5.png index 30fd19c..136b78a 100644 Binary files a/toxygen/smileys/default/D83DDCF5.png and b/toxygen/smileys/default/D83DDCF5.png differ diff --git a/toxygen/smileys/default/D83DDCF6.png b/toxygen/smileys/default/D83DDCF6.png index c0be3ad..68a63e0 100644 Binary files a/toxygen/smileys/default/D83DDCF6.png and b/toxygen/smileys/default/D83DDCF6.png differ diff --git a/toxygen/smileys/default/D83DDCF7.png b/toxygen/smileys/default/D83DDCF7.png index b02f891..d38227e 100644 Binary files a/toxygen/smileys/default/D83DDCF7.png and b/toxygen/smileys/default/D83DDCF7.png differ diff --git a/toxygen/smileys/default/D83DDCF9.png b/toxygen/smileys/default/D83DDCF9.png index 3ce2305..6cb3b36 100644 Binary files a/toxygen/smileys/default/D83DDCF9.png and b/toxygen/smileys/default/D83DDCF9.png differ diff --git a/toxygen/smileys/default/D83DDCFA.png b/toxygen/smileys/default/D83DDCFA.png index c81b1f6..c282230 100644 Binary files a/toxygen/smileys/default/D83DDCFA.png and b/toxygen/smileys/default/D83DDCFA.png differ diff --git a/toxygen/smileys/default/D83DDCFB.png b/toxygen/smileys/default/D83DDCFB.png index 0c1e441..173b13c 100644 Binary files a/toxygen/smileys/default/D83DDCFB.png and b/toxygen/smileys/default/D83DDCFB.png differ diff --git a/toxygen/smileys/default/D83DDCFC.png b/toxygen/smileys/default/D83DDCFC.png index d7fcb26..7c71a81 100644 Binary files a/toxygen/smileys/default/D83DDCFC.png and b/toxygen/smileys/default/D83DDCFC.png differ diff --git a/toxygen/smileys/default/D83DDD00.png b/toxygen/smileys/default/D83DDD00.png index 08fd865..03465b5 100644 Binary files a/toxygen/smileys/default/D83DDD00.png and b/toxygen/smileys/default/D83DDD00.png differ diff --git a/toxygen/smileys/default/D83DDD01.png b/toxygen/smileys/default/D83DDD01.png index 3f8c7bf..bc521ef 100644 Binary files a/toxygen/smileys/default/D83DDD01.png and b/toxygen/smileys/default/D83DDD01.png differ diff --git a/toxygen/smileys/default/D83DDD02.png b/toxygen/smileys/default/D83DDD02.png index 373200a..41ac492 100644 Binary files a/toxygen/smileys/default/D83DDD02.png and b/toxygen/smileys/default/D83DDD02.png differ diff --git a/toxygen/smileys/default/D83DDD03.png b/toxygen/smileys/default/D83DDD03.png index fc4963b..6f24b20 100644 Binary files a/toxygen/smileys/default/D83DDD03.png and b/toxygen/smileys/default/D83DDD03.png differ diff --git a/toxygen/smileys/default/D83DDD04.png b/toxygen/smileys/default/D83DDD04.png index ba2b21f..6255482 100644 Binary files a/toxygen/smileys/default/D83DDD04.png and b/toxygen/smileys/default/D83DDD04.png differ diff --git a/toxygen/smileys/default/D83DDD05.png b/toxygen/smileys/default/D83DDD05.png index e6d2462..0fd3e11 100644 Binary files a/toxygen/smileys/default/D83DDD05.png and b/toxygen/smileys/default/D83DDD05.png differ diff --git a/toxygen/smileys/default/D83DDD06.png b/toxygen/smileys/default/D83DDD06.png index 771f42a..7df3172 100644 Binary files a/toxygen/smileys/default/D83DDD06.png and b/toxygen/smileys/default/D83DDD06.png differ diff --git a/toxygen/smileys/default/D83DDD07.png b/toxygen/smileys/default/D83DDD07.png index cc4fc65..ff3769c 100644 Binary files a/toxygen/smileys/default/D83DDD07.png and b/toxygen/smileys/default/D83DDD07.png differ diff --git a/toxygen/smileys/default/D83DDD09.png b/toxygen/smileys/default/D83DDD09.png index 435a7b9..a51efc8 100644 Binary files a/toxygen/smileys/default/D83DDD09.png and b/toxygen/smileys/default/D83DDD09.png differ diff --git a/toxygen/smileys/default/D83DDD0B.png b/toxygen/smileys/default/D83DDD0B.png index c637732..fd3e3d2 100644 Binary files a/toxygen/smileys/default/D83DDD0B.png and b/toxygen/smileys/default/D83DDD0B.png differ diff --git a/toxygen/smileys/default/D83DDD0C.png b/toxygen/smileys/default/D83DDD0C.png index b0fa88c..0317018 100644 Binary files a/toxygen/smileys/default/D83DDD0C.png and b/toxygen/smileys/default/D83DDD0C.png differ diff --git a/toxygen/smileys/default/D83DDD0D.png b/toxygen/smileys/default/D83DDD0D.png index 4f7cb05..2bdf40b 100644 Binary files a/toxygen/smileys/default/D83DDD0D.png and b/toxygen/smileys/default/D83DDD0D.png differ diff --git a/toxygen/smileys/default/D83DDD0E.png b/toxygen/smileys/default/D83DDD0E.png index fe0ace1..d9a8b8a 100644 Binary files a/toxygen/smileys/default/D83DDD0E.png and b/toxygen/smileys/default/D83DDD0E.png differ diff --git a/toxygen/smileys/default/D83DDD0F.png b/toxygen/smileys/default/D83DDD0F.png index 0b76fe1..3dc1ea0 100644 Binary files a/toxygen/smileys/default/D83DDD0F.png and b/toxygen/smileys/default/D83DDD0F.png differ diff --git a/toxygen/smileys/default/D83DDD10.png b/toxygen/smileys/default/D83DDD10.png index e94d395..4210428 100644 Binary files a/toxygen/smileys/default/D83DDD10.png and b/toxygen/smileys/default/D83DDD10.png differ diff --git a/toxygen/smileys/default/D83DDD11.png b/toxygen/smileys/default/D83DDD11.png index 37ac8b5..c60bcad 100644 Binary files a/toxygen/smileys/default/D83DDD11.png and b/toxygen/smileys/default/D83DDD11.png differ diff --git a/toxygen/smileys/default/D83DDD12.png b/toxygen/smileys/default/D83DDD12.png index 74fb17e..8a680cf 100644 Binary files a/toxygen/smileys/default/D83DDD12.png and b/toxygen/smileys/default/D83DDD12.png differ diff --git a/toxygen/smileys/default/D83DDD13.png b/toxygen/smileys/default/D83DDD13.png index a0f8311..bfc3b9b 100644 Binary files a/toxygen/smileys/default/D83DDD13.png and b/toxygen/smileys/default/D83DDD13.png differ diff --git a/toxygen/smileys/default/D83DDD14.png b/toxygen/smileys/default/D83DDD14.png index fefc17b..937d445 100644 Binary files a/toxygen/smileys/default/D83DDD14.png and b/toxygen/smileys/default/D83DDD14.png differ diff --git a/toxygen/smileys/default/D83DDD15.png b/toxygen/smileys/default/D83DDD15.png index 061cebb..135191f 100644 Binary files a/toxygen/smileys/default/D83DDD15.png and b/toxygen/smileys/default/D83DDD15.png differ diff --git a/toxygen/smileys/default/D83DDD16.png b/toxygen/smileys/default/D83DDD16.png index 42f2d33..8081be2 100644 Binary files a/toxygen/smileys/default/D83DDD16.png and b/toxygen/smileys/default/D83DDD16.png differ diff --git a/toxygen/smileys/default/D83DDD17.png b/toxygen/smileys/default/D83DDD17.png index d7df173..fbab54d 100644 Binary files a/toxygen/smileys/default/D83DDD17.png and b/toxygen/smileys/default/D83DDD17.png differ diff --git a/toxygen/smileys/default/D83DDD18.png b/toxygen/smileys/default/D83DDD18.png index da9708d..9787965 100644 Binary files a/toxygen/smileys/default/D83DDD18.png and b/toxygen/smileys/default/D83DDD18.png differ diff --git a/toxygen/smileys/default/D83DDD19.png b/toxygen/smileys/default/D83DDD19.png index 54da6d2..ed66b43 100644 Binary files a/toxygen/smileys/default/D83DDD19.png and b/toxygen/smileys/default/D83DDD19.png differ diff --git a/toxygen/smileys/default/D83DDD1A.png b/toxygen/smileys/default/D83DDD1A.png index b77ca43..adcdb79 100644 Binary files a/toxygen/smileys/default/D83DDD1A.png and b/toxygen/smileys/default/D83DDD1A.png differ diff --git a/toxygen/smileys/default/D83DDD1B.png b/toxygen/smileys/default/D83DDD1B.png index 9ebeaac..956d7d7 100644 Binary files a/toxygen/smileys/default/D83DDD1B.png and b/toxygen/smileys/default/D83DDD1B.png differ diff --git a/toxygen/smileys/default/D83DDD1C.png b/toxygen/smileys/default/D83DDD1C.png index bb52bb6..72d88f5 100644 Binary files a/toxygen/smileys/default/D83DDD1C.png and b/toxygen/smileys/default/D83DDD1C.png differ diff --git a/toxygen/smileys/default/D83DDD1D.png b/toxygen/smileys/default/D83DDD1D.png index 75acdb0..940a84d 100644 Binary files a/toxygen/smileys/default/D83DDD1D.png and b/toxygen/smileys/default/D83DDD1D.png differ diff --git a/toxygen/smileys/default/D83DDD1E.png b/toxygen/smileys/default/D83DDD1E.png index 81a8f84..4577ba8 100644 Binary files a/toxygen/smileys/default/D83DDD1E.png and b/toxygen/smileys/default/D83DDD1E.png differ diff --git a/toxygen/smileys/default/D83DDD1F.png b/toxygen/smileys/default/D83DDD1F.png index c709d36..9533fa0 100644 Binary files a/toxygen/smileys/default/D83DDD1F.png and b/toxygen/smileys/default/D83DDD1F.png differ diff --git a/toxygen/smileys/default/D83DDD20.png b/toxygen/smileys/default/D83DDD20.png index 0fdbe29..74e29fa 100644 Binary files a/toxygen/smileys/default/D83DDD20.png and b/toxygen/smileys/default/D83DDD20.png differ diff --git a/toxygen/smileys/default/D83DDD21.png b/toxygen/smileys/default/D83DDD21.png index 4cc424d..c77b49a 100644 Binary files a/toxygen/smileys/default/D83DDD21.png and b/toxygen/smileys/default/D83DDD21.png differ diff --git a/toxygen/smileys/default/D83DDD22.png b/toxygen/smileys/default/D83DDD22.png index d86193a..841012e 100644 Binary files a/toxygen/smileys/default/D83DDD22.png and b/toxygen/smileys/default/D83DDD22.png differ diff --git a/toxygen/smileys/default/D83DDD23.png b/toxygen/smileys/default/D83DDD23.png index 5684e65..8320fa3 100644 Binary files a/toxygen/smileys/default/D83DDD23.png and b/toxygen/smileys/default/D83DDD23.png differ diff --git a/toxygen/smileys/default/D83DDD24.png b/toxygen/smileys/default/D83DDD24.png index 37b4083..eeb0666 100644 Binary files a/toxygen/smileys/default/D83DDD24.png and b/toxygen/smileys/default/D83DDD24.png differ diff --git a/toxygen/smileys/default/D83DDD25.png b/toxygen/smileys/default/D83DDD25.png index 3625517..f4db0d9 100644 Binary files a/toxygen/smileys/default/D83DDD25.png and b/toxygen/smileys/default/D83DDD25.png differ diff --git a/toxygen/smileys/default/D83DDD26.png b/toxygen/smileys/default/D83DDD26.png index ec4ca85..78acca9 100644 Binary files a/toxygen/smileys/default/D83DDD26.png and b/toxygen/smileys/default/D83DDD26.png differ diff --git a/toxygen/smileys/default/D83DDD27.png b/toxygen/smileys/default/D83DDD27.png index c29a3d3..9a424b4 100644 Binary files a/toxygen/smileys/default/D83DDD27.png and b/toxygen/smileys/default/D83DDD27.png differ diff --git a/toxygen/smileys/default/D83DDD28.png b/toxygen/smileys/default/D83DDD28.png index d63385a..0193fc1 100644 Binary files a/toxygen/smileys/default/D83DDD28.png and b/toxygen/smileys/default/D83DDD28.png differ diff --git a/toxygen/smileys/default/D83DDD29.png b/toxygen/smileys/default/D83DDD29.png index 59c3282..7eaa1d6 100644 Binary files a/toxygen/smileys/default/D83DDD29.png and b/toxygen/smileys/default/D83DDD29.png differ diff --git a/toxygen/smileys/default/D83DDD2A.png b/toxygen/smileys/default/D83DDD2A.png index 3070ae7..02c024a 100644 Binary files a/toxygen/smileys/default/D83DDD2A.png and b/toxygen/smileys/default/D83DDD2A.png differ diff --git a/toxygen/smileys/default/D83DDD2B.png b/toxygen/smileys/default/D83DDD2B.png index d54b05b..40cd7f9 100644 Binary files a/toxygen/smileys/default/D83DDD2B.png and b/toxygen/smileys/default/D83DDD2B.png differ diff --git a/toxygen/smileys/default/D83DDD2C.png b/toxygen/smileys/default/D83DDD2C.png index abf56d7..0147271 100644 Binary files a/toxygen/smileys/default/D83DDD2C.png and b/toxygen/smileys/default/D83DDD2C.png differ diff --git a/toxygen/smileys/default/D83DDD2D.png b/toxygen/smileys/default/D83DDD2D.png index be1709a..450c039 100644 Binary files a/toxygen/smileys/default/D83DDD2D.png and b/toxygen/smileys/default/D83DDD2D.png differ diff --git a/toxygen/smileys/default/D83DDD2E.png b/toxygen/smileys/default/D83DDD2E.png index b6d25a8..2c056bf 100644 Binary files a/toxygen/smileys/default/D83DDD2E.png and b/toxygen/smileys/default/D83DDD2E.png differ diff --git a/toxygen/smileys/default/D83DDD31.png b/toxygen/smileys/default/D83DDD31.png index 35dc231..d115de5 100644 Binary files a/toxygen/smileys/default/D83DDD31.png and b/toxygen/smileys/default/D83DDD31.png differ diff --git a/toxygen/smileys/default/D83DDD32.png b/toxygen/smileys/default/D83DDD32.png index 04e60df..f0c6e28 100644 Binary files a/toxygen/smileys/default/D83DDD32.png and b/toxygen/smileys/default/D83DDD32.png differ diff --git a/toxygen/smileys/default/D83DDD33.png b/toxygen/smileys/default/D83DDD33.png index 2d7cd76..9b0a44d 100644 Binary files a/toxygen/smileys/default/D83DDD33.png and b/toxygen/smileys/default/D83DDD33.png differ diff --git a/toxygen/smileys/default/D83DDD34.png b/toxygen/smileys/default/D83DDD34.png index 7b308fb..a1b4491 100644 Binary files a/toxygen/smileys/default/D83DDD34.png and b/toxygen/smileys/default/D83DDD34.png differ diff --git a/toxygen/smileys/default/D83DDD35.png b/toxygen/smileys/default/D83DDD35.png index 67cf643..e746012 100644 Binary files a/toxygen/smileys/default/D83DDD35.png and b/toxygen/smileys/default/D83DDD35.png differ diff --git a/toxygen/smileys/default/D83DDD36.png b/toxygen/smileys/default/D83DDD36.png index 54bdf32..2d3c57d 100644 Binary files a/toxygen/smileys/default/D83DDD36.png and b/toxygen/smileys/default/D83DDD36.png differ diff --git a/toxygen/smileys/default/D83DDD37.png b/toxygen/smileys/default/D83DDD37.png index 32336fe..7fea98d 100644 Binary files a/toxygen/smileys/default/D83DDD37.png and b/toxygen/smileys/default/D83DDD37.png differ diff --git a/toxygen/smileys/default/D83DDD38.png b/toxygen/smileys/default/D83DDD38.png index dc39083..136df51 100644 Binary files a/toxygen/smileys/default/D83DDD38.png and b/toxygen/smileys/default/D83DDD38.png differ diff --git a/toxygen/smileys/default/D83DDD39.png b/toxygen/smileys/default/D83DDD39.png index e6bce51..00a0c43 100644 Binary files a/toxygen/smileys/default/D83DDD39.png and b/toxygen/smileys/default/D83DDD39.png differ diff --git a/toxygen/smileys/default/D83DDD3A.png b/toxygen/smileys/default/D83DDD3A.png index d0902a9..8f7b1d3 100644 Binary files a/toxygen/smileys/default/D83DDD3A.png and b/toxygen/smileys/default/D83DDD3A.png differ diff --git a/toxygen/smileys/default/D83DDD3B.png b/toxygen/smileys/default/D83DDD3B.png index de5a4d5..e980342 100644 Binary files a/toxygen/smileys/default/D83DDD3B.png and b/toxygen/smileys/default/D83DDD3B.png differ diff --git a/toxygen/smileys/default/D83DDD3C.png b/toxygen/smileys/default/D83DDD3C.png index 5e58b6f..c5f37cb 100644 Binary files a/toxygen/smileys/default/D83DDD3C.png and b/toxygen/smileys/default/D83DDD3C.png differ diff --git a/toxygen/smileys/default/D83DDD3D.png b/toxygen/smileys/default/D83DDD3D.png index 8c6b23f..d887596 100644 Binary files a/toxygen/smileys/default/D83DDD3D.png and b/toxygen/smileys/default/D83DDD3D.png differ diff --git a/toxygen/smileys/default/D83DDDFB.png b/toxygen/smileys/default/D83DDDFB.png index 588ac40..cdecd76 100644 Binary files a/toxygen/smileys/default/D83DDDFB.png and b/toxygen/smileys/default/D83DDDFB.png differ diff --git a/toxygen/smileys/default/D83DDDFC.png b/toxygen/smileys/default/D83DDDFC.png index 77970e7..ea72808 100644 Binary files a/toxygen/smileys/default/D83DDDFC.png and b/toxygen/smileys/default/D83DDDFC.png differ diff --git a/toxygen/smileys/default/D83DDDFD.png b/toxygen/smileys/default/D83DDDFD.png index e189cd1..899fe6e 100644 Binary files a/toxygen/smileys/default/D83DDDFD.png and b/toxygen/smileys/default/D83DDDFD.png differ diff --git a/toxygen/smileys/default/D83DDDFE.png b/toxygen/smileys/default/D83DDDFE.png index d8fbe06..bd3ca85 100644 Binary files a/toxygen/smileys/default/D83DDDFE.png and b/toxygen/smileys/default/D83DDDFE.png differ diff --git a/toxygen/smileys/default/D83DDDFF.png b/toxygen/smileys/default/D83DDDFF.png index 89148f1..bb6cad6 100644 Binary files a/toxygen/smileys/default/D83DDDFF.png and b/toxygen/smileys/default/D83DDDFF.png differ diff --git a/toxygen/smileys/default/D83DDE00.png b/toxygen/smileys/default/D83DDE00.png index 0cd39d2..e7cbe1d 100644 Binary files a/toxygen/smileys/default/D83DDE00.png and b/toxygen/smileys/default/D83DDE00.png differ diff --git a/toxygen/smileys/default/D83DDE01.png b/toxygen/smileys/default/D83DDE01.png index acf0f88..deee5ea 100644 Binary files a/toxygen/smileys/default/D83DDE01.png and b/toxygen/smileys/default/D83DDE01.png differ diff --git a/toxygen/smileys/default/D83DDE02.png b/toxygen/smileys/default/D83DDE02.png index ba6136b..89190fa 100644 Binary files a/toxygen/smileys/default/D83DDE02.png and b/toxygen/smileys/default/D83DDE02.png differ diff --git a/toxygen/smileys/default/D83DDE03.png b/toxygen/smileys/default/D83DDE03.png index 1f17728..be04f6c 100644 Binary files a/toxygen/smileys/default/D83DDE03.png and b/toxygen/smileys/default/D83DDE03.png differ diff --git a/toxygen/smileys/default/D83DDE04.png b/toxygen/smileys/default/D83DDE04.png index eaddfd3..435b0ca 100644 Binary files a/toxygen/smileys/default/D83DDE04.png and b/toxygen/smileys/default/D83DDE04.png differ diff --git a/toxygen/smileys/default/D83DDE05.png b/toxygen/smileys/default/D83DDE05.png index 0ffdcd3..2aaf1b7 100644 Binary files a/toxygen/smileys/default/D83DDE05.png and b/toxygen/smileys/default/D83DDE05.png differ diff --git a/toxygen/smileys/default/D83DDE06.png b/toxygen/smileys/default/D83DDE06.png index 99739e2..f3f1c7e 100644 Binary files a/toxygen/smileys/default/D83DDE06.png and b/toxygen/smileys/default/D83DDE06.png differ diff --git a/toxygen/smileys/default/D83DDE07.png b/toxygen/smileys/default/D83DDE07.png index 12dee1b..00ddb6e 100644 Binary files a/toxygen/smileys/default/D83DDE07.png and b/toxygen/smileys/default/D83DDE07.png differ diff --git a/toxygen/smileys/default/D83DDE08.png b/toxygen/smileys/default/D83DDE08.png index aa09cf9..b775c51 100644 Binary files a/toxygen/smileys/default/D83DDE08.png and b/toxygen/smileys/default/D83DDE08.png differ diff --git a/toxygen/smileys/default/D83DDE09.png b/toxygen/smileys/default/D83DDE09.png index 510fd1e..5eccad2 100644 Binary files a/toxygen/smileys/default/D83DDE09.png and b/toxygen/smileys/default/D83DDE09.png differ diff --git a/toxygen/smileys/default/D83DDE0A.png b/toxygen/smileys/default/D83DDE0A.png index c24689c..2885494 100644 Binary files a/toxygen/smileys/default/D83DDE0A.png and b/toxygen/smileys/default/D83DDE0A.png differ diff --git a/toxygen/smileys/default/D83DDE0B.png b/toxygen/smileys/default/D83DDE0B.png index cdac089..83c0e83 100644 Binary files a/toxygen/smileys/default/D83DDE0B.png and b/toxygen/smileys/default/D83DDE0B.png differ diff --git a/toxygen/smileys/default/D83DDE0C.png b/toxygen/smileys/default/D83DDE0C.png index a1a9721..b8a367a 100644 Binary files a/toxygen/smileys/default/D83DDE0C.png and b/toxygen/smileys/default/D83DDE0C.png differ diff --git a/toxygen/smileys/default/D83DDE0D.png b/toxygen/smileys/default/D83DDE0D.png index 0ec4145..6fdf5c6 100644 Binary files a/toxygen/smileys/default/D83DDE0D.png and b/toxygen/smileys/default/D83DDE0D.png differ diff --git a/toxygen/smileys/default/D83DDE0E.png b/toxygen/smileys/default/D83DDE0E.png index 4393ef6..8c1b63f 100644 Binary files a/toxygen/smileys/default/D83DDE0E.png and b/toxygen/smileys/default/D83DDE0E.png differ diff --git a/toxygen/smileys/default/D83DDE0F.png b/toxygen/smileys/default/D83DDE0F.png index a14dc21..d8e00d0 100644 Binary files a/toxygen/smileys/default/D83DDE0F.png and b/toxygen/smileys/default/D83DDE0F.png differ diff --git a/toxygen/smileys/default/D83DDE10.png b/toxygen/smileys/default/D83DDE10.png index 21b43ea..f5b9b12 100644 Binary files a/toxygen/smileys/default/D83DDE10.png and b/toxygen/smileys/default/D83DDE10.png differ diff --git a/toxygen/smileys/default/D83DDE11.png b/toxygen/smileys/default/D83DDE11.png index e6946b0..c050655 100644 Binary files a/toxygen/smileys/default/D83DDE11.png and b/toxygen/smileys/default/D83DDE11.png differ diff --git a/toxygen/smileys/default/D83DDE12.png b/toxygen/smileys/default/D83DDE12.png index bd3e0a2..bfd07f9 100644 Binary files a/toxygen/smileys/default/D83DDE12.png and b/toxygen/smileys/default/D83DDE12.png differ diff --git a/toxygen/smileys/default/D83DDE13.png b/toxygen/smileys/default/D83DDE13.png index d9be4e9..9812eea 100644 Binary files a/toxygen/smileys/default/D83DDE13.png and b/toxygen/smileys/default/D83DDE13.png differ diff --git a/toxygen/smileys/default/D83DDE14.png b/toxygen/smileys/default/D83DDE14.png index c73602b..e2ff195 100644 Binary files a/toxygen/smileys/default/D83DDE14.png and b/toxygen/smileys/default/D83DDE14.png differ diff --git a/toxygen/smileys/default/D83DDE15.png b/toxygen/smileys/default/D83DDE15.png index 6d16ea3..c1dcf86 100644 Binary files a/toxygen/smileys/default/D83DDE15.png and b/toxygen/smileys/default/D83DDE15.png differ diff --git a/toxygen/smileys/default/D83DDE16.png b/toxygen/smileys/default/D83DDE16.png index a0ae46a..e61bc89 100644 Binary files a/toxygen/smileys/default/D83DDE16.png and b/toxygen/smileys/default/D83DDE16.png differ diff --git a/toxygen/smileys/default/D83DDE17.png b/toxygen/smileys/default/D83DDE17.png index d110e6a..b583473 100644 Binary files a/toxygen/smileys/default/D83DDE17.png and b/toxygen/smileys/default/D83DDE17.png differ diff --git a/toxygen/smileys/default/D83DDE18.png b/toxygen/smileys/default/D83DDE18.png index 04f349e..b4b985e 100644 Binary files a/toxygen/smileys/default/D83DDE18.png and b/toxygen/smileys/default/D83DDE18.png differ diff --git a/toxygen/smileys/default/D83DDE19.png b/toxygen/smileys/default/D83DDE19.png index be3d55c..6981b2b 100644 Binary files a/toxygen/smileys/default/D83DDE19.png and b/toxygen/smileys/default/D83DDE19.png differ diff --git a/toxygen/smileys/default/D83DDE1A.png b/toxygen/smileys/default/D83DDE1A.png index 0be83c9..5d72bc9 100644 Binary files a/toxygen/smileys/default/D83DDE1A.png and b/toxygen/smileys/default/D83DDE1A.png differ diff --git a/toxygen/smileys/default/D83DDE1B.png b/toxygen/smileys/default/D83DDE1B.png index 0a52738..5466a03 100644 Binary files a/toxygen/smileys/default/D83DDE1B.png and b/toxygen/smileys/default/D83DDE1B.png differ diff --git a/toxygen/smileys/default/D83DDE1C.png b/toxygen/smileys/default/D83DDE1C.png index 628cea5..6796924 100644 Binary files a/toxygen/smileys/default/D83DDE1C.png and b/toxygen/smileys/default/D83DDE1C.png differ diff --git a/toxygen/smileys/default/D83DDE1D.png b/toxygen/smileys/default/D83DDE1D.png index a646c97..aa3d784 100644 Binary files a/toxygen/smileys/default/D83DDE1D.png and b/toxygen/smileys/default/D83DDE1D.png differ diff --git a/toxygen/smileys/default/D83DDE1E.png b/toxygen/smileys/default/D83DDE1E.png index 908d406..f2845ef 100644 Binary files a/toxygen/smileys/default/D83DDE1E.png and b/toxygen/smileys/default/D83DDE1E.png differ diff --git a/toxygen/smileys/default/D83DDE1F.png b/toxygen/smileys/default/D83DDE1F.png index 486b21c..b21d08f 100644 Binary files a/toxygen/smileys/default/D83DDE1F.png and b/toxygen/smileys/default/D83DDE1F.png differ diff --git a/toxygen/smileys/default/D83DDE20.png b/toxygen/smileys/default/D83DDE20.png index 7bcfc84..5b4a0cd 100644 Binary files a/toxygen/smileys/default/D83DDE20.png and b/toxygen/smileys/default/D83DDE20.png differ diff --git a/toxygen/smileys/default/D83DDE21.png b/toxygen/smileys/default/D83DDE21.png index 4e176b4..4d891fa 100644 Binary files a/toxygen/smileys/default/D83DDE21.png and b/toxygen/smileys/default/D83DDE21.png differ diff --git a/toxygen/smileys/default/D83DDE22.png b/toxygen/smileys/default/D83DDE22.png index 11063d8..2cc2c82 100644 Binary files a/toxygen/smileys/default/D83DDE22.png and b/toxygen/smileys/default/D83DDE22.png differ diff --git a/toxygen/smileys/default/D83DDE23.png b/toxygen/smileys/default/D83DDE23.png index bab140c..3cd5062 100644 Binary files a/toxygen/smileys/default/D83DDE23.png and b/toxygen/smileys/default/D83DDE23.png differ diff --git a/toxygen/smileys/default/D83DDE24.png b/toxygen/smileys/default/D83DDE24.png index 900d0dc..7a98d95 100644 Binary files a/toxygen/smileys/default/D83DDE24.png and b/toxygen/smileys/default/D83DDE24.png differ diff --git a/toxygen/smileys/default/D83DDE25.png b/toxygen/smileys/default/D83DDE25.png index 271da83..e244fe7 100644 Binary files a/toxygen/smileys/default/D83DDE25.png and b/toxygen/smileys/default/D83DDE25.png differ diff --git a/toxygen/smileys/default/D83DDE26.png b/toxygen/smileys/default/D83DDE26.png index bd494b6..48641e6 100644 Binary files a/toxygen/smileys/default/D83DDE26.png and b/toxygen/smileys/default/D83DDE26.png differ diff --git a/toxygen/smileys/default/D83DDE27.png b/toxygen/smileys/default/D83DDE27.png index b18443d..a2e655a 100644 Binary files a/toxygen/smileys/default/D83DDE27.png and b/toxygen/smileys/default/D83DDE27.png differ diff --git a/toxygen/smileys/default/D83DDE28.png b/toxygen/smileys/default/D83DDE28.png index 75eafd2..76ccea9 100644 Binary files a/toxygen/smileys/default/D83DDE28.png and b/toxygen/smileys/default/D83DDE28.png differ diff --git a/toxygen/smileys/default/D83DDE29.png b/toxygen/smileys/default/D83DDE29.png index 28252f8..4430882 100644 Binary files a/toxygen/smileys/default/D83DDE29.png and b/toxygen/smileys/default/D83DDE29.png differ diff --git a/toxygen/smileys/default/D83DDE2A.png b/toxygen/smileys/default/D83DDE2A.png index c98fe7f..0bb276b 100644 Binary files a/toxygen/smileys/default/D83DDE2A.png and b/toxygen/smileys/default/D83DDE2A.png differ diff --git a/toxygen/smileys/default/D83DDE2B.png b/toxygen/smileys/default/D83DDE2B.png index fc972fb..0b459a2 100644 Binary files a/toxygen/smileys/default/D83DDE2B.png and b/toxygen/smileys/default/D83DDE2B.png differ diff --git a/toxygen/smileys/default/D83DDE2C.png b/toxygen/smileys/default/D83DDE2C.png index e4bc449..b945eff 100644 Binary files a/toxygen/smileys/default/D83DDE2C.png and b/toxygen/smileys/default/D83DDE2C.png differ diff --git a/toxygen/smileys/default/D83DDE2D.png b/toxygen/smileys/default/D83DDE2D.png index 2c7f81d..aac29ca 100644 Binary files a/toxygen/smileys/default/D83DDE2D.png and b/toxygen/smileys/default/D83DDE2D.png differ diff --git a/toxygen/smileys/default/D83DDE2E.png b/toxygen/smileys/default/D83DDE2E.png index f9d5570..f6df656 100644 Binary files a/toxygen/smileys/default/D83DDE2E.png and b/toxygen/smileys/default/D83DDE2E.png differ diff --git a/toxygen/smileys/default/D83DDE2F.png b/toxygen/smileys/default/D83DDE2F.png index 79a6935..98c4308 100644 Binary files a/toxygen/smileys/default/D83DDE2F.png and b/toxygen/smileys/default/D83DDE2F.png differ diff --git a/toxygen/smileys/default/D83DDE30.png b/toxygen/smileys/default/D83DDE30.png index b86ff4b..8f50d8d 100644 Binary files a/toxygen/smileys/default/D83DDE30.png and b/toxygen/smileys/default/D83DDE30.png differ diff --git a/toxygen/smileys/default/D83DDE31.png b/toxygen/smileys/default/D83DDE31.png index fbd5527..a54a8a0 100644 Binary files a/toxygen/smileys/default/D83DDE31.png and b/toxygen/smileys/default/D83DDE31.png differ diff --git a/toxygen/smileys/default/D83DDE32.png b/toxygen/smileys/default/D83DDE32.png index f401d0e..48c87ea 100644 Binary files a/toxygen/smileys/default/D83DDE32.png and b/toxygen/smileys/default/D83DDE32.png differ diff --git a/toxygen/smileys/default/D83DDE33.png b/toxygen/smileys/default/D83DDE33.png index cecc347..cf50892 100644 Binary files a/toxygen/smileys/default/D83DDE33.png and b/toxygen/smileys/default/D83DDE33.png differ diff --git a/toxygen/smileys/default/D83DDE34.png b/toxygen/smileys/default/D83DDE34.png index b5fb2d7..3f4f1d6 100644 Binary files a/toxygen/smileys/default/D83DDE34.png and b/toxygen/smileys/default/D83DDE34.png differ diff --git a/toxygen/smileys/default/D83DDE35.png b/toxygen/smileys/default/D83DDE35.png index 41cacc7..24391db 100644 Binary files a/toxygen/smileys/default/D83DDE35.png and b/toxygen/smileys/default/D83DDE35.png differ diff --git a/toxygen/smileys/default/D83DDE36.png b/toxygen/smileys/default/D83DDE36.png index a81be46..46b30af 100644 Binary files a/toxygen/smileys/default/D83DDE36.png and b/toxygen/smileys/default/D83DDE36.png differ diff --git a/toxygen/smileys/default/D83DDE37.png b/toxygen/smileys/default/D83DDE37.png index c8177d6..1dea4b5 100644 Binary files a/toxygen/smileys/default/D83DDE37.png and b/toxygen/smileys/default/D83DDE37.png differ diff --git a/toxygen/smileys/default/D83DDE38.png b/toxygen/smileys/default/D83DDE38.png index ffad9c5..882d0ac 100644 Binary files a/toxygen/smileys/default/D83DDE38.png and b/toxygen/smileys/default/D83DDE38.png differ diff --git a/toxygen/smileys/default/D83DDE39.png b/toxygen/smileys/default/D83DDE39.png index 828b832..c311744 100644 Binary files a/toxygen/smileys/default/D83DDE39.png and b/toxygen/smileys/default/D83DDE39.png differ diff --git a/toxygen/smileys/default/D83DDE3A.png b/toxygen/smileys/default/D83DDE3A.png index 8022c4d..a18fa7d 100644 Binary files a/toxygen/smileys/default/D83DDE3A.png and b/toxygen/smileys/default/D83DDE3A.png differ diff --git a/toxygen/smileys/default/D83DDE3B.png b/toxygen/smileys/default/D83DDE3B.png index c9405b7..ed35e28 100644 Binary files a/toxygen/smileys/default/D83DDE3B.png and b/toxygen/smileys/default/D83DDE3B.png differ diff --git a/toxygen/smileys/default/D83DDE3C.png b/toxygen/smileys/default/D83DDE3C.png index cb088d1..f924c45 100644 Binary files a/toxygen/smileys/default/D83DDE3C.png and b/toxygen/smileys/default/D83DDE3C.png differ diff --git a/toxygen/smileys/default/D83DDE3D.png b/toxygen/smileys/default/D83DDE3D.png index ca2a4cc..bf8c962 100644 Binary files a/toxygen/smileys/default/D83DDE3D.png and b/toxygen/smileys/default/D83DDE3D.png differ diff --git a/toxygen/smileys/default/D83DDE3E.png b/toxygen/smileys/default/D83DDE3E.png index 840ced0..e02931c 100644 Binary files a/toxygen/smileys/default/D83DDE3E.png and b/toxygen/smileys/default/D83DDE3E.png differ diff --git a/toxygen/smileys/default/D83DDE3F.png b/toxygen/smileys/default/D83DDE3F.png index 8d0375b..a7cd4e1 100644 Binary files a/toxygen/smileys/default/D83DDE3F.png and b/toxygen/smileys/default/D83DDE3F.png differ diff --git a/toxygen/smileys/default/D83DDE40.png b/toxygen/smileys/default/D83DDE40.png index 01df29d..9a72002 100644 Binary files a/toxygen/smileys/default/D83DDE40.png and b/toxygen/smileys/default/D83DDE40.png differ diff --git a/toxygen/smileys/default/D83DDE45.png b/toxygen/smileys/default/D83DDE45.png index f9da41a..503fd32 100644 Binary files a/toxygen/smileys/default/D83DDE45.png and b/toxygen/smileys/default/D83DDE45.png differ diff --git a/toxygen/smileys/default/D83DDE46.png b/toxygen/smileys/default/D83DDE46.png index cd8c707..f965125 100644 Binary files a/toxygen/smileys/default/D83DDE46.png and b/toxygen/smileys/default/D83DDE46.png differ diff --git a/toxygen/smileys/default/D83DDE47.png b/toxygen/smileys/default/D83DDE47.png index a491820..355c9d2 100644 Binary files a/toxygen/smileys/default/D83DDE47.png and b/toxygen/smileys/default/D83DDE47.png differ diff --git a/toxygen/smileys/default/D83DDE48.png b/toxygen/smileys/default/D83DDE48.png index 937cb83..098c7f5 100644 Binary files a/toxygen/smileys/default/D83DDE48.png and b/toxygen/smileys/default/D83DDE48.png differ diff --git a/toxygen/smileys/default/D83DDE49.png b/toxygen/smileys/default/D83DDE49.png index e63475f..320c7fa 100644 Binary files a/toxygen/smileys/default/D83DDE49.png and b/toxygen/smileys/default/D83DDE49.png differ diff --git a/toxygen/smileys/default/D83DDE4A.png b/toxygen/smileys/default/D83DDE4A.png index e73108d..2a36454 100644 Binary files a/toxygen/smileys/default/D83DDE4A.png and b/toxygen/smileys/default/D83DDE4A.png differ diff --git a/toxygen/smileys/default/D83DDE4B.png b/toxygen/smileys/default/D83DDE4B.png index 90e1e1c..e9f8655 100644 Binary files a/toxygen/smileys/default/D83DDE4B.png and b/toxygen/smileys/default/D83DDE4B.png differ diff --git a/toxygen/smileys/default/D83DDE4C.png b/toxygen/smileys/default/D83DDE4C.png index a6e7081..ccb621a 100644 Binary files a/toxygen/smileys/default/D83DDE4C.png and b/toxygen/smileys/default/D83DDE4C.png differ diff --git a/toxygen/smileys/default/D83DDE4D.png b/toxygen/smileys/default/D83DDE4D.png index 2954a6e..fc9fb7e 100644 Binary files a/toxygen/smileys/default/D83DDE4D.png and b/toxygen/smileys/default/D83DDE4D.png differ diff --git a/toxygen/smileys/default/D83DDE4E.png b/toxygen/smileys/default/D83DDE4E.png index 8032b6f..2fd7c71 100644 Binary files a/toxygen/smileys/default/D83DDE4E.png and b/toxygen/smileys/default/D83DDE4E.png differ diff --git a/toxygen/smileys/default/D83DDE4F.png b/toxygen/smileys/default/D83DDE4F.png index d597f2c..204545d 100644 Binary files a/toxygen/smileys/default/D83DDE4F.png and b/toxygen/smileys/default/D83DDE4F.png differ diff --git a/toxygen/smileys/default/D83DDE80.png b/toxygen/smileys/default/D83DDE80.png index 6ddfda4..7e9241d 100644 Binary files a/toxygen/smileys/default/D83DDE80.png and b/toxygen/smileys/default/D83DDE80.png differ diff --git a/toxygen/smileys/default/D83DDE81.png b/toxygen/smileys/default/D83DDE81.png index 3f6eb56..ade03ae 100644 Binary files a/toxygen/smileys/default/D83DDE81.png and b/toxygen/smileys/default/D83DDE81.png differ diff --git a/toxygen/smileys/default/D83DDE82.png b/toxygen/smileys/default/D83DDE82.png index 335e87f..ad242a2 100644 Binary files a/toxygen/smileys/default/D83DDE82.png and b/toxygen/smileys/default/D83DDE82.png differ diff --git a/toxygen/smileys/default/D83DDE83.png b/toxygen/smileys/default/D83DDE83.png index 25ae099..0c6dc18 100644 Binary files a/toxygen/smileys/default/D83DDE83.png and b/toxygen/smileys/default/D83DDE83.png differ diff --git a/toxygen/smileys/default/D83DDE84.png b/toxygen/smileys/default/D83DDE84.png index 025cf05..829cb73 100644 Binary files a/toxygen/smileys/default/D83DDE84.png and b/toxygen/smileys/default/D83DDE84.png differ diff --git a/toxygen/smileys/default/D83DDE85.png b/toxygen/smileys/default/D83DDE85.png index 4afbff7..07eb96a 100644 Binary files a/toxygen/smileys/default/D83DDE85.png and b/toxygen/smileys/default/D83DDE85.png differ diff --git a/toxygen/smileys/default/D83DDE86.png b/toxygen/smileys/default/D83DDE86.png index 8fc9521..d757d24 100644 Binary files a/toxygen/smileys/default/D83DDE86.png and b/toxygen/smileys/default/D83DDE86.png differ diff --git a/toxygen/smileys/default/D83DDE87.png b/toxygen/smileys/default/D83DDE87.png index b518e93..c50d0df 100644 Binary files a/toxygen/smileys/default/D83DDE87.png and b/toxygen/smileys/default/D83DDE87.png differ diff --git a/toxygen/smileys/default/D83DDE88.png b/toxygen/smileys/default/D83DDE88.png index 1120b8b..2be47b2 100644 Binary files a/toxygen/smileys/default/D83DDE88.png and b/toxygen/smileys/default/D83DDE88.png differ diff --git a/toxygen/smileys/default/D83DDE89.png b/toxygen/smileys/default/D83DDE89.png index fff5d09..0de6bd4 100644 Binary files a/toxygen/smileys/default/D83DDE89.png and b/toxygen/smileys/default/D83DDE89.png differ diff --git a/toxygen/smileys/default/D83DDE8A.png b/toxygen/smileys/default/D83DDE8A.png index 84a5df9..46637f4 100644 Binary files a/toxygen/smileys/default/D83DDE8A.png and b/toxygen/smileys/default/D83DDE8A.png differ diff --git a/toxygen/smileys/default/D83DDE8B.png b/toxygen/smileys/default/D83DDE8B.png index 53c3359..c25512b 100644 Binary files a/toxygen/smileys/default/D83DDE8B.png and b/toxygen/smileys/default/D83DDE8B.png differ diff --git a/toxygen/smileys/default/D83DDE8C.png b/toxygen/smileys/default/D83DDE8C.png index 2630cd1..95e047a 100644 Binary files a/toxygen/smileys/default/D83DDE8C.png and b/toxygen/smileys/default/D83DDE8C.png differ diff --git a/toxygen/smileys/default/D83DDE8D.png b/toxygen/smileys/default/D83DDE8D.png index a923db8..eeffc28 100644 Binary files a/toxygen/smileys/default/D83DDE8D.png and b/toxygen/smileys/default/D83DDE8D.png differ diff --git a/toxygen/smileys/default/D83DDE8E.png b/toxygen/smileys/default/D83DDE8E.png index d75ab66..cce5a5b 100644 Binary files a/toxygen/smileys/default/D83DDE8E.png and b/toxygen/smileys/default/D83DDE8E.png differ diff --git a/toxygen/smileys/default/D83DDE8F.png b/toxygen/smileys/default/D83DDE8F.png index df2c9ca..be13deb 100644 Binary files a/toxygen/smileys/default/D83DDE8F.png and b/toxygen/smileys/default/D83DDE8F.png differ diff --git a/toxygen/smileys/default/D83DDE90.png b/toxygen/smileys/default/D83DDE90.png index 44dc513..8407d41 100644 Binary files a/toxygen/smileys/default/D83DDE90.png and b/toxygen/smileys/default/D83DDE90.png differ diff --git a/toxygen/smileys/default/D83DDE91.png b/toxygen/smileys/default/D83DDE91.png index fd48b03..d9b1421 100644 Binary files a/toxygen/smileys/default/D83DDE91.png and b/toxygen/smileys/default/D83DDE91.png differ diff --git a/toxygen/smileys/default/D83DDE92.png b/toxygen/smileys/default/D83DDE92.png index 8d5c3e9..57981c7 100644 Binary files a/toxygen/smileys/default/D83DDE92.png and b/toxygen/smileys/default/D83DDE92.png differ diff --git a/toxygen/smileys/default/D83DDE93.png b/toxygen/smileys/default/D83DDE93.png index beb4e21..b2c6847 100644 Binary files a/toxygen/smileys/default/D83DDE93.png and b/toxygen/smileys/default/D83DDE93.png differ diff --git a/toxygen/smileys/default/D83DDE94.png b/toxygen/smileys/default/D83DDE94.png index b6efd4a..3cacb88 100644 Binary files a/toxygen/smileys/default/D83DDE94.png and b/toxygen/smileys/default/D83DDE94.png differ diff --git a/toxygen/smileys/default/D83DDE95.png b/toxygen/smileys/default/D83DDE95.png index d2e3f98..7facea0 100644 Binary files a/toxygen/smileys/default/D83DDE95.png and b/toxygen/smileys/default/D83DDE95.png differ diff --git a/toxygen/smileys/default/D83DDE96.png b/toxygen/smileys/default/D83DDE96.png index cdda0b7..8fe25d5 100644 Binary files a/toxygen/smileys/default/D83DDE96.png and b/toxygen/smileys/default/D83DDE96.png differ diff --git a/toxygen/smileys/default/D83DDE97.png b/toxygen/smileys/default/D83DDE97.png index 15ab93c..26e2b61 100644 Binary files a/toxygen/smileys/default/D83DDE97.png and b/toxygen/smileys/default/D83DDE97.png differ diff --git a/toxygen/smileys/default/D83DDE98.png b/toxygen/smileys/default/D83DDE98.png index 7c80a77..1324bd2 100644 Binary files a/toxygen/smileys/default/D83DDE98.png and b/toxygen/smileys/default/D83DDE98.png differ diff --git a/toxygen/smileys/default/D83DDE99.png b/toxygen/smileys/default/D83DDE99.png index 6a965bb..9b0d95c 100644 Binary files a/toxygen/smileys/default/D83DDE99.png and b/toxygen/smileys/default/D83DDE99.png differ diff --git a/toxygen/smileys/default/D83DDE9A.png b/toxygen/smileys/default/D83DDE9A.png index 93a7987..ef64e61 100644 Binary files a/toxygen/smileys/default/D83DDE9A.png and b/toxygen/smileys/default/D83DDE9A.png differ diff --git a/toxygen/smileys/default/D83DDE9B.png b/toxygen/smileys/default/D83DDE9B.png index 5edebd5..59af9fe 100644 Binary files a/toxygen/smileys/default/D83DDE9B.png and b/toxygen/smileys/default/D83DDE9B.png differ diff --git a/toxygen/smileys/default/D83DDE9C.png b/toxygen/smileys/default/D83DDE9C.png index bbc00b8..3fa3330 100644 Binary files a/toxygen/smileys/default/D83DDE9C.png and b/toxygen/smileys/default/D83DDE9C.png differ diff --git a/toxygen/smileys/default/D83DDE9D.png b/toxygen/smileys/default/D83DDE9D.png index 255da82..7c4a412 100644 Binary files a/toxygen/smileys/default/D83DDE9D.png and b/toxygen/smileys/default/D83DDE9D.png differ diff --git a/toxygen/smileys/default/D83DDE9E.png b/toxygen/smileys/default/D83DDE9E.png index be587b1..19b0411 100644 Binary files a/toxygen/smileys/default/D83DDE9E.png and b/toxygen/smileys/default/D83DDE9E.png differ diff --git a/toxygen/smileys/default/D83DDE9F.png b/toxygen/smileys/default/D83DDE9F.png index af0cff5..f391cdf 100644 Binary files a/toxygen/smileys/default/D83DDE9F.png and b/toxygen/smileys/default/D83DDE9F.png differ diff --git a/toxygen/smileys/default/D83DDEA0.png b/toxygen/smileys/default/D83DDEA0.png index 82936e9..6987fd5 100644 Binary files a/toxygen/smileys/default/D83DDEA0.png and b/toxygen/smileys/default/D83DDEA0.png differ diff --git a/toxygen/smileys/default/D83DDEA1.png b/toxygen/smileys/default/D83DDEA1.png index 9149416..e6abeb1 100644 Binary files a/toxygen/smileys/default/D83DDEA1.png and b/toxygen/smileys/default/D83DDEA1.png differ diff --git a/toxygen/smileys/default/D83DDEA2.png b/toxygen/smileys/default/D83DDEA2.png index 023eb77..ec8cd9e 100644 Binary files a/toxygen/smileys/default/D83DDEA2.png and b/toxygen/smileys/default/D83DDEA2.png differ diff --git a/toxygen/smileys/default/D83DDEA3.png b/toxygen/smileys/default/D83DDEA3.png index b91a9fb..4f29601 100644 Binary files a/toxygen/smileys/default/D83DDEA3.png and b/toxygen/smileys/default/D83DDEA3.png differ diff --git a/toxygen/smileys/default/D83DDEA4.png b/toxygen/smileys/default/D83DDEA4.png index 1f18619..e4ab4aa 100644 Binary files a/toxygen/smileys/default/D83DDEA4.png and b/toxygen/smileys/default/D83DDEA4.png differ diff --git a/toxygen/smileys/default/D83DDEA5.png b/toxygen/smileys/default/D83DDEA5.png index f6dab29..d3d6899 100644 Binary files a/toxygen/smileys/default/D83DDEA5.png and b/toxygen/smileys/default/D83DDEA5.png differ diff --git a/toxygen/smileys/default/D83DDEA6.png b/toxygen/smileys/default/D83DDEA6.png index a5c108f..4f32bd1 100644 Binary files a/toxygen/smileys/default/D83DDEA6.png and b/toxygen/smileys/default/D83DDEA6.png differ diff --git a/toxygen/smileys/default/D83DDEA7.png b/toxygen/smileys/default/D83DDEA7.png index dab4659..041e7ba 100644 Binary files a/toxygen/smileys/default/D83DDEA7.png and b/toxygen/smileys/default/D83DDEA7.png differ diff --git a/toxygen/smileys/default/D83DDEA8.png b/toxygen/smileys/default/D83DDEA8.png index 3e7d2ac..b67f810 100644 Binary files a/toxygen/smileys/default/D83DDEA8.png and b/toxygen/smileys/default/D83DDEA8.png differ diff --git a/toxygen/smileys/default/D83DDEA9.png b/toxygen/smileys/default/D83DDEA9.png index 08f1c5d..a0127db 100644 Binary files a/toxygen/smileys/default/D83DDEA9.png and b/toxygen/smileys/default/D83DDEA9.png differ diff --git a/toxygen/smileys/default/D83DDEAA.png b/toxygen/smileys/default/D83DDEAA.png index d186b37..808f73c 100644 Binary files a/toxygen/smileys/default/D83DDEAA.png and b/toxygen/smileys/default/D83DDEAA.png differ diff --git a/toxygen/smileys/default/D83DDEAB.png b/toxygen/smileys/default/D83DDEAB.png index 8d02055..092f7f7 100644 Binary files a/toxygen/smileys/default/D83DDEAB.png and b/toxygen/smileys/default/D83DDEAB.png differ diff --git a/toxygen/smileys/default/D83DDEAC.png b/toxygen/smileys/default/D83DDEAC.png index 0e9d890..2295dd1 100644 Binary files a/toxygen/smileys/default/D83DDEAC.png and b/toxygen/smileys/default/D83DDEAC.png differ diff --git a/toxygen/smileys/default/D83DDEAD.png b/toxygen/smileys/default/D83DDEAD.png index e8eb437..947ecea 100644 Binary files a/toxygen/smileys/default/D83DDEAD.png and b/toxygen/smileys/default/D83DDEAD.png differ diff --git a/toxygen/smileys/default/D83DDEAE.png b/toxygen/smileys/default/D83DDEAE.png index 89e1dbf..7c37d2e 100644 Binary files a/toxygen/smileys/default/D83DDEAE.png and b/toxygen/smileys/default/D83DDEAE.png differ diff --git a/toxygen/smileys/default/D83DDEAF.png b/toxygen/smileys/default/D83DDEAF.png index c1febe4..cc94e68 100644 Binary files a/toxygen/smileys/default/D83DDEAF.png and b/toxygen/smileys/default/D83DDEAF.png differ diff --git a/toxygen/smileys/default/D83DDEB0.png b/toxygen/smileys/default/D83DDEB0.png index 5a814ce..3790c67 100644 Binary files a/toxygen/smileys/default/D83DDEB0.png and b/toxygen/smileys/default/D83DDEB0.png differ diff --git a/toxygen/smileys/default/D83DDEB1.png b/toxygen/smileys/default/D83DDEB1.png index b94e904..6b01824 100644 Binary files a/toxygen/smileys/default/D83DDEB1.png and b/toxygen/smileys/default/D83DDEB1.png differ diff --git a/toxygen/smileys/default/D83DDEB2.png b/toxygen/smileys/default/D83DDEB2.png index 204b1ff..ff22425 100644 Binary files a/toxygen/smileys/default/D83DDEB2.png and b/toxygen/smileys/default/D83DDEB2.png differ diff --git a/toxygen/smileys/default/D83DDEB3.png b/toxygen/smileys/default/D83DDEB3.png index 94b6121..3aa33ae 100644 Binary files a/toxygen/smileys/default/D83DDEB3.png and b/toxygen/smileys/default/D83DDEB3.png differ diff --git a/toxygen/smileys/default/D83DDEB4.png b/toxygen/smileys/default/D83DDEB4.png index 2b63d91..5b8e424 100644 Binary files a/toxygen/smileys/default/D83DDEB4.png and b/toxygen/smileys/default/D83DDEB4.png differ diff --git a/toxygen/smileys/default/D83DDEB5.png b/toxygen/smileys/default/D83DDEB5.png index 9f9fb96..a53016c 100644 Binary files a/toxygen/smileys/default/D83DDEB5.png and b/toxygen/smileys/default/D83DDEB5.png differ diff --git a/toxygen/smileys/default/D83DDEB6.png b/toxygen/smileys/default/D83DDEB6.png index 2662d36..3cae405 100644 Binary files a/toxygen/smileys/default/D83DDEB6.png and b/toxygen/smileys/default/D83DDEB6.png differ diff --git a/toxygen/smileys/default/D83DDEB7.png b/toxygen/smileys/default/D83DDEB7.png index a3c0a9b..464b925 100644 Binary files a/toxygen/smileys/default/D83DDEB7.png and b/toxygen/smileys/default/D83DDEB7.png differ diff --git a/toxygen/smileys/default/D83DDEB8.png b/toxygen/smileys/default/D83DDEB8.png index 479c4ee..ff26204 100644 Binary files a/toxygen/smileys/default/D83DDEB8.png and b/toxygen/smileys/default/D83DDEB8.png differ diff --git a/toxygen/smileys/default/D83DDEB9.png b/toxygen/smileys/default/D83DDEB9.png index ed294c0..34f6afe 100644 Binary files a/toxygen/smileys/default/D83DDEB9.png and b/toxygen/smileys/default/D83DDEB9.png differ diff --git a/toxygen/smileys/default/D83DDEBA.png b/toxygen/smileys/default/D83DDEBA.png index 2ec1130..42f346a 100644 Binary files a/toxygen/smileys/default/D83DDEBA.png and b/toxygen/smileys/default/D83DDEBA.png differ diff --git a/toxygen/smileys/default/D83DDEBB.png b/toxygen/smileys/default/D83DDEBB.png index d213051..730020e 100644 Binary files a/toxygen/smileys/default/D83DDEBB.png and b/toxygen/smileys/default/D83DDEBB.png differ diff --git a/toxygen/smileys/default/D83DDEBC.png b/toxygen/smileys/default/D83DDEBC.png index b473c5f..2e16d90 100644 Binary files a/toxygen/smileys/default/D83DDEBC.png and b/toxygen/smileys/default/D83DDEBC.png differ diff --git a/toxygen/smileys/default/D83DDEBD.png b/toxygen/smileys/default/D83DDEBD.png index 911fac4..4bae582 100644 Binary files a/toxygen/smileys/default/D83DDEBD.png and b/toxygen/smileys/default/D83DDEBD.png differ diff --git a/toxygen/smileys/default/D83DDEBE.png b/toxygen/smileys/default/D83DDEBE.png index e2bfc5d..0ad1c76 100644 Binary files a/toxygen/smileys/default/D83DDEBE.png and b/toxygen/smileys/default/D83DDEBE.png differ diff --git a/toxygen/smileys/default/D83DDEBF.png b/toxygen/smileys/default/D83DDEBF.png index 093ca87..90e601a 100644 Binary files a/toxygen/smileys/default/D83DDEBF.png and b/toxygen/smileys/default/D83DDEBF.png differ diff --git a/toxygen/smileys/default/D83DDEC0.png b/toxygen/smileys/default/D83DDEC0.png index 907b1da..048ec2c 100644 Binary files a/toxygen/smileys/default/D83DDEC0.png and b/toxygen/smileys/default/D83DDEC0.png differ diff --git a/toxygen/smileys/default/D83DDEC1.png b/toxygen/smileys/default/D83DDEC1.png index 9ac0098..84976f7 100644 Binary files a/toxygen/smileys/default/D83DDEC1.png and b/toxygen/smileys/default/D83DDEC1.png differ diff --git a/toxygen/smileys/default/D83DDEC2.png b/toxygen/smileys/default/D83DDEC2.png index 2e8a9b7..9149a02 100644 Binary files a/toxygen/smileys/default/D83DDEC2.png and b/toxygen/smileys/default/D83DDEC2.png differ diff --git a/toxygen/smileys/default/D83DDEC3.png b/toxygen/smileys/default/D83DDEC3.png index d76d3e2..affea8e 100644 Binary files a/toxygen/smileys/default/D83DDEC3.png and b/toxygen/smileys/default/D83DDEC3.png differ diff --git a/toxygen/smileys/default/D83DDEC4.png b/toxygen/smileys/default/D83DDEC4.png index 2bb6658..1ba4191 100644 Binary files a/toxygen/smileys/default/D83DDEC4.png and b/toxygen/smileys/default/D83DDEC4.png differ diff --git a/toxygen/smileys/default/D83DDEC5.png b/toxygen/smileys/default/D83DDEC5.png index 19f966a..cd438cb 100644 Binary files a/toxygen/smileys/default/D83DDEC5.png and b/toxygen/smileys/default/D83DDEC5.png differ diff --git a/toxygen/smileys/default/ad.png b/toxygen/smileys/default/ad.png old mode 100644 new mode 100755 index 552e976..625ca84 Binary files a/toxygen/smileys/default/ad.png and b/toxygen/smileys/default/ad.png differ diff --git a/toxygen/smileys/default/ae.png b/toxygen/smileys/default/ae.png old mode 100644 new mode 100755 index 670f615..ef3a1ec Binary files a/toxygen/smileys/default/ae.png and b/toxygen/smileys/default/ae.png differ diff --git a/toxygen/smileys/default/af.png b/toxygen/smileys/default/af.png old mode 100644 new mode 100755 index cb6e23b..a4742e2 Binary files a/toxygen/smileys/default/af.png and b/toxygen/smileys/default/af.png differ diff --git a/toxygen/smileys/default/ag.png b/toxygen/smileys/default/ag.png old mode 100644 new mode 100755 index 421ae03..556d550 Binary files a/toxygen/smileys/default/ag.png and b/toxygen/smileys/default/ag.png differ diff --git a/toxygen/smileys/default/ai.png b/toxygen/smileys/default/ai.png old mode 100644 new mode 100755 index d4683cb..74ed29d Binary files a/toxygen/smileys/default/ai.png and b/toxygen/smileys/default/ai.png differ diff --git a/toxygen/smileys/default/al.png b/toxygen/smileys/default/al.png old mode 100644 new mode 100755 index 1fe5c25..92354cb Binary files a/toxygen/smileys/default/al.png and b/toxygen/smileys/default/al.png differ diff --git a/toxygen/smileys/default/am.png b/toxygen/smileys/default/am.png old mode 100644 new mode 100755 index 0bc24a7..344a2a8 Binary files a/toxygen/smileys/default/am.png and b/toxygen/smileys/default/am.png differ diff --git a/toxygen/smileys/default/an.png b/toxygen/smileys/default/an.png old mode 100644 new mode 100755 index 796943f..633e4b8 Binary files a/toxygen/smileys/default/an.png and b/toxygen/smileys/default/an.png differ diff --git a/toxygen/smileys/default/ao.png b/toxygen/smileys/default/ao.png index 5efe456..bcbd1d6 100644 Binary files a/toxygen/smileys/default/ao.png and b/toxygen/smileys/default/ao.png differ diff --git a/toxygen/smileys/default/ar.png b/toxygen/smileys/default/ar.png old mode 100644 new mode 100755 index 8cc53f5..e5ef8f1 Binary files a/toxygen/smileys/default/ar.png and b/toxygen/smileys/default/ar.png differ diff --git a/toxygen/smileys/default/as.png b/toxygen/smileys/default/as.png old mode 100644 new mode 100755 index a13e8c8..32f30e4 Binary files a/toxygen/smileys/default/as.png and b/toxygen/smileys/default/as.png differ diff --git a/toxygen/smileys/default/at.png b/toxygen/smileys/default/at.png old mode 100644 new mode 100755 index a420c86..0f15f34 Binary files a/toxygen/smileys/default/at.png and b/toxygen/smileys/default/at.png differ diff --git a/toxygen/smileys/default/au.png b/toxygen/smileys/default/au.png old mode 100644 new mode 100755 index f847827..a01389a Binary files a/toxygen/smileys/default/au.png and b/toxygen/smileys/default/au.png differ diff --git a/toxygen/smileys/default/aw.png b/toxygen/smileys/default/aw.png old mode 100644 new mode 100755 index 3804086..a3579c2 Binary files a/toxygen/smileys/default/aw.png and b/toxygen/smileys/default/aw.png differ diff --git a/toxygen/smileys/default/ax.png b/toxygen/smileys/default/ax.png old mode 100644 new mode 100755 index d8075a5..1eea80a Binary files a/toxygen/smileys/default/ax.png and b/toxygen/smileys/default/ax.png differ diff --git a/toxygen/smileys/default/az.png b/toxygen/smileys/default/az.png old mode 100644 new mode 100755 index 0eaf6b2..4ee9fe5 Binary files a/toxygen/smileys/default/az.png and b/toxygen/smileys/default/az.png differ diff --git a/toxygen/smileys/default/ba.png b/toxygen/smileys/default/ba.png old mode 100644 new mode 100755 index 96619d2..c774992 Binary files a/toxygen/smileys/default/ba.png and b/toxygen/smileys/default/ba.png differ diff --git a/toxygen/smileys/default/bb.png b/toxygen/smileys/default/bb.png old mode 100644 new mode 100755 index bf17f85..0df19c7 Binary files a/toxygen/smileys/default/bb.png and b/toxygen/smileys/default/bb.png differ diff --git a/toxygen/smileys/default/bd.png b/toxygen/smileys/default/bd.png old mode 100644 new mode 100755 index 4f0390c..076a8bf Binary files a/toxygen/smileys/default/bd.png and b/toxygen/smileys/default/bd.png differ diff --git a/toxygen/smileys/default/be.png b/toxygen/smileys/default/be.png old mode 100644 new mode 100755 index 4c2c9da..d86ebc8 Binary files a/toxygen/smileys/default/be.png and b/toxygen/smileys/default/be.png differ diff --git a/toxygen/smileys/default/bf.png b/toxygen/smileys/default/bf.png old mode 100644 new mode 100755 index b7de459..ab5ce8f Binary files a/toxygen/smileys/default/bf.png and b/toxygen/smileys/default/bf.png differ diff --git a/toxygen/smileys/default/bg.png b/toxygen/smileys/default/bg.png old mode 100644 new mode 100755 index c3c2e2c..0469f06 Binary files a/toxygen/smileys/default/bg.png and b/toxygen/smileys/default/bg.png differ diff --git a/toxygen/smileys/default/bh.png b/toxygen/smileys/default/bh.png old mode 100644 new mode 100755 index f3a88fd..ea8ce68 Binary files a/toxygen/smileys/default/bh.png and b/toxygen/smileys/default/bh.png differ diff --git a/toxygen/smileys/default/bi.png b/toxygen/smileys/default/bi.png old mode 100644 new mode 100755 index 18d51e2..5cc2e30 Binary files a/toxygen/smileys/default/bi.png and b/toxygen/smileys/default/bi.png differ diff --git a/toxygen/smileys/default/bj.png b/toxygen/smileys/default/bj.png old mode 100644 new mode 100755 index 32cf542..1cc8b45 Binary files a/toxygen/smileys/default/bj.png and b/toxygen/smileys/default/bj.png differ diff --git a/toxygen/smileys/default/bm.png b/toxygen/smileys/default/bm.png old mode 100644 new mode 100755 index 007e4d8..c0c7aea Binary files a/toxygen/smileys/default/bm.png and b/toxygen/smileys/default/bm.png differ diff --git a/toxygen/smileys/default/bn.png b/toxygen/smileys/default/bn.png old mode 100644 new mode 100755 index a9e83e9..8fb0984 Binary files a/toxygen/smileys/default/bn.png and b/toxygen/smileys/default/bn.png differ diff --git a/toxygen/smileys/default/bo.png b/toxygen/smileys/default/bo.png old mode 100644 new mode 100755 index 06eb7a7..ce7ba52 Binary files a/toxygen/smileys/default/bo.png and b/toxygen/smileys/default/bo.png differ diff --git a/toxygen/smileys/default/br.png b/toxygen/smileys/default/br.png old mode 100644 new mode 100755 index f9da237..9b1a553 Binary files a/toxygen/smileys/default/br.png and b/toxygen/smileys/default/br.png differ diff --git a/toxygen/smileys/default/bs.png b/toxygen/smileys/default/bs.png old mode 100644 new mode 100755 index 09c3c6d..639fa6c Binary files a/toxygen/smileys/default/bs.png and b/toxygen/smileys/default/bs.png differ diff --git a/toxygen/smileys/default/bt.png b/toxygen/smileys/default/bt.png old mode 100644 new mode 100755 index 5f07fd9..1d512df Binary files a/toxygen/smileys/default/bt.png and b/toxygen/smileys/default/bt.png differ diff --git a/toxygen/smileys/default/bv.png b/toxygen/smileys/default/bv.png old mode 100644 new mode 100755 index 00997d1..160b6b5 Binary files a/toxygen/smileys/default/bv.png and b/toxygen/smileys/default/bv.png differ diff --git a/toxygen/smileys/default/bw.png b/toxygen/smileys/default/bw.png old mode 100644 new mode 100755 index 51d5ec4..fcb1039 Binary files a/toxygen/smileys/default/bw.png and b/toxygen/smileys/default/bw.png differ diff --git a/toxygen/smileys/default/by.png b/toxygen/smileys/default/by.png old mode 100644 new mode 100755 index b26d9a7..504774e Binary files a/toxygen/smileys/default/by.png and b/toxygen/smileys/default/by.png differ diff --git a/toxygen/smileys/default/bz.png b/toxygen/smileys/default/bz.png old mode 100644 new mode 100755 index 3de1ee3..be63ee1 Binary files a/toxygen/smileys/default/bz.png and b/toxygen/smileys/default/bz.png differ diff --git a/toxygen/smileys/default/ca.png b/toxygen/smileys/default/ca.png old mode 100644 new mode 100755 index d11daef..1f20419 Binary files a/toxygen/smileys/default/ca.png and b/toxygen/smileys/default/ca.png differ diff --git a/toxygen/smileys/default/catalonia.png b/toxygen/smileys/default/catalonia.png index 0ae1406..5041e30 100644 Binary files a/toxygen/smileys/default/catalonia.png and b/toxygen/smileys/default/catalonia.png differ diff --git a/toxygen/smileys/default/cc.png b/toxygen/smileys/default/cc.png old mode 100644 new mode 100755 index 6b71349..aed3d3b Binary files a/toxygen/smileys/default/cc.png and b/toxygen/smileys/default/cc.png differ diff --git a/toxygen/smileys/default/cd.png b/toxygen/smileys/default/cd.png index d04d285..5e48942 100644 Binary files a/toxygen/smileys/default/cd.png and b/toxygen/smileys/default/cd.png differ diff --git a/toxygen/smileys/default/cf.png b/toxygen/smileys/default/cf.png old mode 100644 new mode 100755 index e08fe16..da687bd Binary files a/toxygen/smileys/default/cf.png and b/toxygen/smileys/default/cf.png differ diff --git a/toxygen/smileys/default/cg.png b/toxygen/smileys/default/cg.png old mode 100644 new mode 100755 index 5ff3986..a859792 Binary files a/toxygen/smileys/default/cg.png and b/toxygen/smileys/default/cg.png differ diff --git a/toxygen/smileys/default/ch.png b/toxygen/smileys/default/ch.png old mode 100644 new mode 100755 index da989f3..242ec01 Binary files a/toxygen/smileys/default/ch.png and b/toxygen/smileys/default/ch.png differ diff --git a/toxygen/smileys/default/ci.png b/toxygen/smileys/default/ci.png old mode 100644 new mode 100755 index 631d1fb..3f2c62e Binary files a/toxygen/smileys/default/ci.png and b/toxygen/smileys/default/ci.png differ diff --git a/toxygen/smileys/default/ck.png b/toxygen/smileys/default/ck.png old mode 100644 new mode 100755 index 6f8d893..746d3d6 Binary files a/toxygen/smileys/default/ck.png and b/toxygen/smileys/default/ck.png differ diff --git a/toxygen/smileys/default/cl.png b/toxygen/smileys/default/cl.png old mode 100644 new mode 100755 index 3fe300c..29c6d61 Binary files a/toxygen/smileys/default/cl.png and b/toxygen/smileys/default/cl.png differ diff --git a/toxygen/smileys/default/cm.png b/toxygen/smileys/default/cm.png old mode 100644 new mode 100755 index 6a13412..f65c5bd Binary files a/toxygen/smileys/default/cm.png and b/toxygen/smileys/default/cm.png differ diff --git a/toxygen/smileys/default/cn.png b/toxygen/smileys/default/cn.png old mode 100644 new mode 100755 index a73f73b..8914414 Binary files a/toxygen/smileys/default/cn.png and b/toxygen/smileys/default/cn.png differ diff --git a/toxygen/smileys/default/co.png b/toxygen/smileys/default/co.png old mode 100644 new mode 100755 index 075ff39..a118ff4 Binary files a/toxygen/smileys/default/co.png and b/toxygen/smileys/default/co.png differ diff --git a/toxygen/smileys/default/cr.png b/toxygen/smileys/default/cr.png old mode 100644 new mode 100755 index a90450c..c7a3731 Binary files a/toxygen/smileys/default/cr.png and b/toxygen/smileys/default/cr.png differ diff --git a/toxygen/smileys/default/cs.png b/toxygen/smileys/default/cs.png old mode 100644 new mode 100755 index 45b4710..8254790 Binary files a/toxygen/smileys/default/cs.png and b/toxygen/smileys/default/cs.png differ diff --git a/toxygen/smileys/default/cu.png b/toxygen/smileys/default/cu.png old mode 100644 new mode 100755 index eef7f8a..083f1d6 Binary files a/toxygen/smileys/default/cu.png and b/toxygen/smileys/default/cu.png differ diff --git a/toxygen/smileys/default/cv.png b/toxygen/smileys/default/cv.png old mode 100644 new mode 100755 index 4ac3d24..a63f7ea Binary files a/toxygen/smileys/default/cv.png and b/toxygen/smileys/default/cv.png differ diff --git a/toxygen/smileys/default/cx.png b/toxygen/smileys/default/cx.png old mode 100644 new mode 100755 index 1c57fbf..48e31ad Binary files a/toxygen/smileys/default/cx.png and b/toxygen/smileys/default/cx.png differ diff --git a/toxygen/smileys/default/cy.png b/toxygen/smileys/default/cy.png old mode 100644 new mode 100755 index 6e234cc..5b1ad6c Binary files a/toxygen/smileys/default/cy.png and b/toxygen/smileys/default/cy.png differ diff --git a/toxygen/smileys/default/cz.png b/toxygen/smileys/default/cz.png old mode 100644 new mode 100755 index 526d990..c8403dd Binary files a/toxygen/smileys/default/cz.png and b/toxygen/smileys/default/cz.png differ diff --git a/toxygen/smileys/default/de.png b/toxygen/smileys/default/de.png old mode 100644 new mode 100755 index 4e202a6..ac4a977 Binary files a/toxygen/smileys/default/de.png and b/toxygen/smileys/default/de.png differ diff --git a/toxygen/smileys/default/dj.png b/toxygen/smileys/default/dj.png old mode 100644 new mode 100755 index 9b3da9c..582af36 Binary files a/toxygen/smileys/default/dj.png and b/toxygen/smileys/default/dj.png differ diff --git a/toxygen/smileys/default/dk.png b/toxygen/smileys/default/dk.png old mode 100644 new mode 100755 index 72af9e3..e2993d3 Binary files a/toxygen/smileys/default/dk.png and b/toxygen/smileys/default/dk.png differ diff --git a/toxygen/smileys/default/dm.png b/toxygen/smileys/default/dm.png old mode 100644 new mode 100755 index d10d036..5fbffcb Binary files a/toxygen/smileys/default/dm.png and b/toxygen/smileys/default/dm.png differ diff --git a/toxygen/smileys/default/do.png b/toxygen/smileys/default/do.png old mode 100644 new mode 100755 index 2134259..5a04932 Binary files a/toxygen/smileys/default/do.png and b/toxygen/smileys/default/do.png differ diff --git a/toxygen/smileys/default/dz.png b/toxygen/smileys/default/dz.png old mode 100644 new mode 100755 index f49fb58..335c239 Binary files a/toxygen/smileys/default/dz.png and b/toxygen/smileys/default/dz.png differ diff --git a/toxygen/smileys/default/ec.png b/toxygen/smileys/default/ec.png old mode 100644 new mode 100755 index d6b42d6..0caa0b1 Binary files a/toxygen/smileys/default/ec.png and b/toxygen/smileys/default/ec.png differ diff --git a/toxygen/smileys/default/ee.png b/toxygen/smileys/default/ee.png old mode 100644 new mode 100755 index 5ffe80e..0c82efb Binary files a/toxygen/smileys/default/ee.png and b/toxygen/smileys/default/ee.png differ diff --git a/toxygen/smileys/default/eg.png b/toxygen/smileys/default/eg.png old mode 100644 new mode 100755 index 50e4c7e..8a3f7a1 Binary files a/toxygen/smileys/default/eg.png and b/toxygen/smileys/default/eg.png differ diff --git a/toxygen/smileys/default/eh.png b/toxygen/smileys/default/eh.png old mode 100644 new mode 100755 index b4f35cd..90a1195 Binary files a/toxygen/smileys/default/eh.png and b/toxygen/smileys/default/eh.png differ diff --git a/toxygen/smileys/default/england.png b/toxygen/smileys/default/england.png old mode 100644 new mode 100755 index 0cd6e96..3a7311d Binary files a/toxygen/smileys/default/england.png and b/toxygen/smileys/default/england.png differ diff --git a/toxygen/smileys/default/er.png b/toxygen/smileys/default/er.png old mode 100644 new mode 100755 index 4d302a6..13065ae Binary files a/toxygen/smileys/default/er.png and b/toxygen/smileys/default/er.png differ diff --git a/toxygen/smileys/default/es.png b/toxygen/smileys/default/es.png old mode 100644 new mode 100755 index c804049..c2de2d7 Binary files a/toxygen/smileys/default/es.png and b/toxygen/smileys/default/es.png differ diff --git a/toxygen/smileys/default/et.png b/toxygen/smileys/default/et.png old mode 100644 new mode 100755 index ebc5f34..2e893fa Binary files a/toxygen/smileys/default/et.png and b/toxygen/smileys/default/et.png differ diff --git a/toxygen/smileys/default/europeanunion.png b/toxygen/smileys/default/europeanunion.png index 50815ae..d6d8711 100644 Binary files a/toxygen/smileys/default/europeanunion.png and b/toxygen/smileys/default/europeanunion.png differ diff --git a/toxygen/smileys/default/fam.png b/toxygen/smileys/default/fam.png old mode 100644 new mode 100755 index e2cdcb7..cf50c75 Binary files a/toxygen/smileys/default/fam.png and b/toxygen/smileys/default/fam.png differ diff --git a/toxygen/smileys/default/fi.png b/toxygen/smileys/default/fi.png old mode 100644 new mode 100755 index 0c0af94..14ec091 Binary files a/toxygen/smileys/default/fi.png and b/toxygen/smileys/default/fi.png differ diff --git a/toxygen/smileys/default/fj.png b/toxygen/smileys/default/fj.png old mode 100644 new mode 100755 index 14a9d76..cee9988 Binary files a/toxygen/smileys/default/fj.png and b/toxygen/smileys/default/fj.png differ diff --git a/toxygen/smileys/default/fk.png b/toxygen/smileys/default/fk.png old mode 100644 new mode 100755 index 0b2c8e1..ceaeb27 Binary files a/toxygen/smileys/default/fk.png and b/toxygen/smileys/default/fk.png differ diff --git a/toxygen/smileys/default/fm.png b/toxygen/smileys/default/fm.png old mode 100644 new mode 100755 index c3fbeed..066bb24 Binary files a/toxygen/smileys/default/fm.png and b/toxygen/smileys/default/fm.png differ diff --git a/toxygen/smileys/default/fo.png b/toxygen/smileys/default/fo.png old mode 100644 new mode 100755 index b48a3f9..cbceb80 Binary files a/toxygen/smileys/default/fo.png and b/toxygen/smileys/default/fo.png differ diff --git a/toxygen/smileys/default/fr.png b/toxygen/smileys/default/fr.png old mode 100644 new mode 100755 index eaec4f3..8332c4e Binary files a/toxygen/smileys/default/fr.png and b/toxygen/smileys/default/fr.png differ diff --git a/toxygen/smileys/default/ga.png b/toxygen/smileys/default/ga.png old mode 100644 new mode 100755 index 14df032..0e0d434 Binary files a/toxygen/smileys/default/ga.png and b/toxygen/smileys/default/ga.png differ diff --git a/toxygen/smileys/default/gb.png b/toxygen/smileys/default/gb.png index 032b04d..ff701e1 100644 Binary files a/toxygen/smileys/default/gb.png and b/toxygen/smileys/default/gb.png differ diff --git a/toxygen/smileys/default/gd.png b/toxygen/smileys/default/gd.png old mode 100644 new mode 100755 index 96ddfd9..9ab57f5 Binary files a/toxygen/smileys/default/gd.png and b/toxygen/smileys/default/gd.png differ diff --git a/toxygen/smileys/default/ge.png b/toxygen/smileys/default/ge.png old mode 100644 new mode 100755 index 2f5475e..728d970 Binary files a/toxygen/smileys/default/ge.png and b/toxygen/smileys/default/ge.png differ diff --git a/toxygen/smileys/default/gf.png b/toxygen/smileys/default/gf.png old mode 100644 new mode 100755 index fddf8f6..8332c4e Binary files a/toxygen/smileys/default/gf.png and b/toxygen/smileys/default/gf.png differ diff --git a/toxygen/smileys/default/gh.png b/toxygen/smileys/default/gh.png old mode 100644 new mode 100755 index 57561cf..4e2f896 Binary files a/toxygen/smileys/default/gh.png and b/toxygen/smileys/default/gh.png differ diff --git a/toxygen/smileys/default/gi.png b/toxygen/smileys/default/gi.png old mode 100644 new mode 100755 index 29a981a..e76797f Binary files a/toxygen/smileys/default/gi.png and b/toxygen/smileys/default/gi.png differ diff --git a/toxygen/smileys/default/gl.png b/toxygen/smileys/default/gl.png old mode 100644 new mode 100755 index d0f4bca..ef12a73 Binary files a/toxygen/smileys/default/gl.png and b/toxygen/smileys/default/gl.png differ diff --git a/toxygen/smileys/default/gm.png b/toxygen/smileys/default/gm.png old mode 100644 new mode 100755 index abf8f8f..0720b66 Binary files a/toxygen/smileys/default/gm.png and b/toxygen/smileys/default/gm.png differ diff --git a/toxygen/smileys/default/gn.png b/toxygen/smileys/default/gn.png old mode 100644 new mode 100755 index ff76a52..ea660b0 Binary files a/toxygen/smileys/default/gn.png and b/toxygen/smileys/default/gn.png differ diff --git a/toxygen/smileys/default/gp.png b/toxygen/smileys/default/gp.png old mode 100644 new mode 100755 index 88d2995..dbb086d Binary files a/toxygen/smileys/default/gp.png and b/toxygen/smileys/default/gp.png differ diff --git a/toxygen/smileys/default/gq.png b/toxygen/smileys/default/gq.png old mode 100644 new mode 100755 index 1051698..ebe20a2 Binary files a/toxygen/smileys/default/gq.png and b/toxygen/smileys/default/gq.png differ diff --git a/toxygen/smileys/default/gr.png b/toxygen/smileys/default/gr.png old mode 100644 new mode 100755 index 0c856e4..8651ade Binary files a/toxygen/smileys/default/gr.png and b/toxygen/smileys/default/gr.png differ diff --git a/toxygen/smileys/default/gs.png b/toxygen/smileys/default/gs.png old mode 100644 new mode 100755 index a0d6575..7ef0bf5 Binary files a/toxygen/smileys/default/gs.png and b/toxygen/smileys/default/gs.png differ diff --git a/toxygen/smileys/default/gt.png b/toxygen/smileys/default/gt.png old mode 100644 new mode 100755 index cec6821..c43a70d Binary files a/toxygen/smileys/default/gt.png and b/toxygen/smileys/default/gt.png differ diff --git a/toxygen/smileys/default/gu.png b/toxygen/smileys/default/gu.png old mode 100644 new mode 100755 index da5f65b..92f37c0 Binary files a/toxygen/smileys/default/gu.png and b/toxygen/smileys/default/gu.png differ diff --git a/toxygen/smileys/default/gw.png b/toxygen/smileys/default/gw.png old mode 100644 new mode 100755 index 9d3af7c..b37bcf0 Binary files a/toxygen/smileys/default/gw.png and b/toxygen/smileys/default/gw.png differ diff --git a/toxygen/smileys/default/gy.png b/toxygen/smileys/default/gy.png old mode 100644 new mode 100755 index eee94e9..22cbe2f Binary files a/toxygen/smileys/default/gy.png and b/toxygen/smileys/default/gy.png differ diff --git a/toxygen/smileys/default/hk.png b/toxygen/smileys/default/hk.png old mode 100644 new mode 100755 index 4ca283f..d5c380c Binary files a/toxygen/smileys/default/hk.png and b/toxygen/smileys/default/hk.png differ diff --git a/toxygen/smileys/default/hm.png b/toxygen/smileys/default/hm.png old mode 100644 new mode 100755 index 67c1149..a01389a Binary files a/toxygen/smileys/default/hm.png and b/toxygen/smileys/default/hm.png differ diff --git a/toxygen/smileys/default/hn.png b/toxygen/smileys/default/hn.png old mode 100644 new mode 100755 index b1eb441..96f8388 Binary files a/toxygen/smileys/default/hn.png and b/toxygen/smileys/default/hn.png differ diff --git a/toxygen/smileys/default/hr.png b/toxygen/smileys/default/hr.png old mode 100644 new mode 100755 index 8cf6064..696b515 Binary files a/toxygen/smileys/default/hr.png and b/toxygen/smileys/default/hr.png differ diff --git a/toxygen/smileys/default/ht.png b/toxygen/smileys/default/ht.png old mode 100644 new mode 100755 index 9e447d6..416052a Binary files a/toxygen/smileys/default/ht.png and b/toxygen/smileys/default/ht.png differ diff --git a/toxygen/smileys/default/hu.png b/toxygen/smileys/default/hu.png old mode 100644 new mode 100755 index 09361e4..7baafe4 Binary files a/toxygen/smileys/default/hu.png and b/toxygen/smileys/default/hu.png differ diff --git a/toxygen/smileys/default/id.png b/toxygen/smileys/default/id.png old mode 100644 new mode 100755 index 76e9fbd..c6bc0fa Binary files a/toxygen/smileys/default/id.png and b/toxygen/smileys/default/id.png differ diff --git a/toxygen/smileys/default/ie.png b/toxygen/smileys/default/ie.png old mode 100644 new mode 100755 index fd87d4b..26baa31 Binary files a/toxygen/smileys/default/ie.png and b/toxygen/smileys/default/ie.png differ diff --git a/toxygen/smileys/default/il.png b/toxygen/smileys/default/il.png old mode 100644 new mode 100755 index b4d8f2d..2ca772d Binary files a/toxygen/smileys/default/il.png and b/toxygen/smileys/default/il.png differ diff --git a/toxygen/smileys/default/in.png b/toxygen/smileys/default/in.png old mode 100644 new mode 100755 index f72030a..e4d7e81 Binary files a/toxygen/smileys/default/in.png and b/toxygen/smileys/default/in.png differ diff --git a/toxygen/smileys/default/io.png b/toxygen/smileys/default/io.png old mode 100644 new mode 100755 index 0f338e8..3e74b6a Binary files a/toxygen/smileys/default/io.png and b/toxygen/smileys/default/io.png differ diff --git a/toxygen/smileys/default/iq.png b/toxygen/smileys/default/iq.png old mode 100644 new mode 100755 index 97219ae..878a351 Binary files a/toxygen/smileys/default/iq.png and b/toxygen/smileys/default/iq.png differ diff --git a/toxygen/smileys/default/ir.png b/toxygen/smileys/default/ir.png old mode 100644 new mode 100755 index f0b721c..c5fd136 Binary files a/toxygen/smileys/default/ir.png and b/toxygen/smileys/default/ir.png differ diff --git a/toxygen/smileys/default/is.png b/toxygen/smileys/default/is.png old mode 100644 new mode 100755 index 5236627..b8f6d0f Binary files a/toxygen/smileys/default/is.png and b/toxygen/smileys/default/is.png differ diff --git a/toxygen/smileys/default/it.png b/toxygen/smileys/default/it.png old mode 100644 new mode 100755 index a2c0f02..89692f7 Binary files a/toxygen/smileys/default/it.png and b/toxygen/smileys/default/it.png differ diff --git a/toxygen/smileys/default/jm.png b/toxygen/smileys/default/jm.png old mode 100644 new mode 100755 index 37ae2ba..7be119e Binary files a/toxygen/smileys/default/jm.png and b/toxygen/smileys/default/jm.png differ diff --git a/toxygen/smileys/default/jo.png b/toxygen/smileys/default/jo.png old mode 100644 new mode 100755 index 97c0f1a..11bd497 Binary files a/toxygen/smileys/default/jo.png and b/toxygen/smileys/default/jo.png differ diff --git a/toxygen/smileys/default/jp.png b/toxygen/smileys/default/jp.png old mode 100644 new mode 100755 index 7b5c019..325fbad Binary files a/toxygen/smileys/default/jp.png and b/toxygen/smileys/default/jp.png differ diff --git a/toxygen/smileys/default/ke.png b/toxygen/smileys/default/ke.png old mode 100644 new mode 100755 index a6ae21e..51879ad Binary files a/toxygen/smileys/default/ke.png and b/toxygen/smileys/default/ke.png differ diff --git a/toxygen/smileys/default/kg.png b/toxygen/smileys/default/kg.png old mode 100644 new mode 100755 index 0d09612..0a818f6 Binary files a/toxygen/smileys/default/kg.png and b/toxygen/smileys/default/kg.png differ diff --git a/toxygen/smileys/default/kh.png b/toxygen/smileys/default/kh.png old mode 100644 new mode 100755 index 1f272a5..30f6bb1 Binary files a/toxygen/smileys/default/kh.png and b/toxygen/smileys/default/kh.png differ diff --git a/toxygen/smileys/default/ki.png b/toxygen/smileys/default/ki.png old mode 100644 new mode 100755 index 83b15b8..2dcce4b Binary files a/toxygen/smileys/default/ki.png and b/toxygen/smileys/default/ki.png differ diff --git a/toxygen/smileys/default/km.png b/toxygen/smileys/default/km.png old mode 100644 new mode 100755 index 5d8863a..812b2f5 Binary files a/toxygen/smileys/default/km.png and b/toxygen/smileys/default/km.png differ diff --git a/toxygen/smileys/default/kn.png b/toxygen/smileys/default/kn.png old mode 100644 new mode 100755 index 6d48d10..febd5b4 Binary files a/toxygen/smileys/default/kn.png and b/toxygen/smileys/default/kn.png differ diff --git a/toxygen/smileys/default/kp.png b/toxygen/smileys/default/kp.png old mode 100644 new mode 100755 index 50dfa1e..d3d509a Binary files a/toxygen/smileys/default/kp.png and b/toxygen/smileys/default/kp.png differ diff --git a/toxygen/smileys/default/kr.png b/toxygen/smileys/default/kr.png old mode 100644 new mode 100755 index 33b8144..9c0a78e Binary files a/toxygen/smileys/default/kr.png and b/toxygen/smileys/default/kr.png differ diff --git a/toxygen/smileys/default/kw.png b/toxygen/smileys/default/kw.png old mode 100644 new mode 100755 index 66ae3a4..96546da Binary files a/toxygen/smileys/default/kw.png and b/toxygen/smileys/default/kw.png differ diff --git a/toxygen/smileys/default/ky.png b/toxygen/smileys/default/ky.png old mode 100644 new mode 100755 index 823b285..15c5f8e Binary files a/toxygen/smileys/default/ky.png and b/toxygen/smileys/default/ky.png differ diff --git a/toxygen/smileys/default/kz.png b/toxygen/smileys/default/kz.png old mode 100644 new mode 100755 index aa8118a..45a8c88 Binary files a/toxygen/smileys/default/kz.png and b/toxygen/smileys/default/kz.png differ diff --git a/toxygen/smileys/default/la.png b/toxygen/smileys/default/la.png old mode 100644 new mode 100755 index 302427f..e28acd0 Binary files a/toxygen/smileys/default/la.png and b/toxygen/smileys/default/la.png differ diff --git a/toxygen/smileys/default/lb.png b/toxygen/smileys/default/lb.png old mode 100644 new mode 100755 index 55a5e5b..d0d452b Binary files a/toxygen/smileys/default/lb.png and b/toxygen/smileys/default/lb.png differ diff --git a/toxygen/smileys/default/lc.png b/toxygen/smileys/default/lc.png index 291f1c5..a47d065 100644 Binary files a/toxygen/smileys/default/lc.png and b/toxygen/smileys/default/lc.png differ diff --git a/toxygen/smileys/default/li.png b/toxygen/smileys/default/li.png old mode 100644 new mode 100755 index 5c0ec41..6469909 Binary files a/toxygen/smileys/default/li.png and b/toxygen/smileys/default/li.png differ diff --git a/toxygen/smileys/default/lk.png b/toxygen/smileys/default/lk.png old mode 100644 new mode 100755 index d2bc667..088aad6 Binary files a/toxygen/smileys/default/lk.png and b/toxygen/smileys/default/lk.png differ diff --git a/toxygen/smileys/default/lr.png b/toxygen/smileys/default/lr.png old mode 100644 new mode 100755 index 24db5a9..89a5bc7 Binary files a/toxygen/smileys/default/lr.png and b/toxygen/smileys/default/lr.png differ diff --git a/toxygen/smileys/default/ls.png b/toxygen/smileys/default/ls.png old mode 100644 new mode 100755 index e4e7966..33fdef1 Binary files a/toxygen/smileys/default/ls.png and b/toxygen/smileys/default/ls.png differ diff --git a/toxygen/smileys/default/lt.png b/toxygen/smileys/default/lt.png old mode 100644 new mode 100755 index 7c2bdd6..c8ef0da Binary files a/toxygen/smileys/default/lt.png and b/toxygen/smileys/default/lt.png differ diff --git a/toxygen/smileys/default/lu.png b/toxygen/smileys/default/lu.png old mode 100644 new mode 100755 index 37544b4..4cabba9 Binary files a/toxygen/smileys/default/lu.png and b/toxygen/smileys/default/lu.png differ diff --git a/toxygen/smileys/default/lv.png b/toxygen/smileys/default/lv.png old mode 100644 new mode 100755 index 6bb32b0..49b6998 Binary files a/toxygen/smileys/default/lv.png and b/toxygen/smileys/default/lv.png differ diff --git a/toxygen/smileys/default/ly.png b/toxygen/smileys/default/ly.png old mode 100644 new mode 100755 index 86c41fc..b163a9f Binary files a/toxygen/smileys/default/ly.png and b/toxygen/smileys/default/ly.png differ diff --git a/toxygen/smileys/default/ma.png b/toxygen/smileys/default/ma.png old mode 100644 new mode 100755 index e720d87..f386770 Binary files a/toxygen/smileys/default/ma.png and b/toxygen/smileys/default/ma.png differ diff --git a/toxygen/smileys/default/mc.png b/toxygen/smileys/default/mc.png old mode 100644 new mode 100755 index 5666a75..1aa830f Binary files a/toxygen/smileys/default/mc.png and b/toxygen/smileys/default/mc.png differ diff --git a/toxygen/smileys/default/md.png b/toxygen/smileys/default/md.png old mode 100644 new mode 100755 index 1bc8b47..4e92c18 Binary files a/toxygen/smileys/default/md.png and b/toxygen/smileys/default/md.png differ diff --git a/toxygen/smileys/default/me.png b/toxygen/smileys/default/me.png index 7449387..ac72535 100644 Binary files a/toxygen/smileys/default/me.png and b/toxygen/smileys/default/me.png differ diff --git a/toxygen/smileys/default/mg.png b/toxygen/smileys/default/mg.png old mode 100644 new mode 100755 index 65e7f27..d2715b3 Binary files a/toxygen/smileys/default/mg.png and b/toxygen/smileys/default/mg.png differ diff --git a/toxygen/smileys/default/mh.png b/toxygen/smileys/default/mh.png old mode 100644 new mode 100755 index 67cc066..fb523a8 Binary files a/toxygen/smileys/default/mh.png and b/toxygen/smileys/default/mh.png differ diff --git a/toxygen/smileys/default/mk.png b/toxygen/smileys/default/mk.png old mode 100644 new mode 100755 index 2e50b58..db173aa Binary files a/toxygen/smileys/default/mk.png and b/toxygen/smileys/default/mk.png differ diff --git a/toxygen/smileys/default/ml.png b/toxygen/smileys/default/ml.png old mode 100644 new mode 100755 index 47844ad..2cec8ba Binary files a/toxygen/smileys/default/ml.png and b/toxygen/smileys/default/ml.png differ diff --git a/toxygen/smileys/default/mm.png b/toxygen/smileys/default/mm.png old mode 100644 new mode 100755 index db89f01..f464f67 Binary files a/toxygen/smileys/default/mm.png and b/toxygen/smileys/default/mm.png differ diff --git a/toxygen/smileys/default/mn.png b/toxygen/smileys/default/mn.png old mode 100644 new mode 100755 index c976ecd..9396355 Binary files a/toxygen/smileys/default/mn.png and b/toxygen/smileys/default/mn.png differ diff --git a/toxygen/smileys/default/mo.png b/toxygen/smileys/default/mo.png old mode 100644 new mode 100755 index cf8113c..deb801d Binary files a/toxygen/smileys/default/mo.png and b/toxygen/smileys/default/mo.png differ diff --git a/toxygen/smileys/default/mp.png b/toxygen/smileys/default/mp.png old mode 100644 new mode 100755 index 013e183..298d588 Binary files a/toxygen/smileys/default/mp.png and b/toxygen/smileys/default/mp.png differ diff --git a/toxygen/smileys/default/mq.png b/toxygen/smileys/default/mq.png old mode 100644 new mode 100755 index 1920168..010143b Binary files a/toxygen/smileys/default/mq.png and b/toxygen/smileys/default/mq.png differ diff --git a/toxygen/smileys/default/mr.png b/toxygen/smileys/default/mr.png old mode 100644 new mode 100755 index 06984ac..319546b Binary files a/toxygen/smileys/default/mr.png and b/toxygen/smileys/default/mr.png differ diff --git a/toxygen/smileys/default/ms.png b/toxygen/smileys/default/ms.png old mode 100644 new mode 100755 index ab6f7fb..d4cbb43 Binary files a/toxygen/smileys/default/ms.png and b/toxygen/smileys/default/ms.png differ diff --git a/toxygen/smileys/default/mt.png b/toxygen/smileys/default/mt.png old mode 100644 new mode 100755 index 0d1f30c..00af948 Binary files a/toxygen/smileys/default/mt.png and b/toxygen/smileys/default/mt.png differ diff --git a/toxygen/smileys/default/mu.png b/toxygen/smileys/default/mu.png old mode 100644 new mode 100755 index e0191f7..b7fdce1 Binary files a/toxygen/smileys/default/mu.png and b/toxygen/smileys/default/mu.png differ diff --git a/toxygen/smileys/default/mv.png b/toxygen/smileys/default/mv.png old mode 100644 new mode 100755 index 44c2b5f..5073d9e Binary files a/toxygen/smileys/default/mv.png and b/toxygen/smileys/default/mv.png differ diff --git a/toxygen/smileys/default/mw.png b/toxygen/smileys/default/mw.png old mode 100644 new mode 100755 index 675d2c2..13886e9 Binary files a/toxygen/smileys/default/mw.png and b/toxygen/smileys/default/mw.png differ diff --git a/toxygen/smileys/default/mx.png b/toxygen/smileys/default/mx.png old mode 100644 new mode 100755 index 0c11c5a..5bc58ab Binary files a/toxygen/smileys/default/mx.png and b/toxygen/smileys/default/mx.png differ diff --git a/toxygen/smileys/default/my.png b/toxygen/smileys/default/my.png old mode 100644 new mode 100755 index 2757cf3..9034cba Binary files a/toxygen/smileys/default/my.png and b/toxygen/smileys/default/my.png differ diff --git a/toxygen/smileys/default/mz.png b/toxygen/smileys/default/mz.png old mode 100644 new mode 100755 index e4ff602..76405e0 Binary files a/toxygen/smileys/default/mz.png and b/toxygen/smileys/default/mz.png differ diff --git a/toxygen/smileys/default/na.png b/toxygen/smileys/default/na.png old mode 100644 new mode 100755 index 4bf47fd..63358c6 Binary files a/toxygen/smileys/default/na.png and b/toxygen/smileys/default/na.png differ diff --git a/toxygen/smileys/default/nc.png b/toxygen/smileys/default/nc.png old mode 100644 new mode 100755 index a4c6811..2cad283 Binary files a/toxygen/smileys/default/nc.png and b/toxygen/smileys/default/nc.png differ diff --git a/toxygen/smileys/default/ne.png b/toxygen/smileys/default/ne.png old mode 100644 new mode 100755 index bc088df..d85f424 Binary files a/toxygen/smileys/default/ne.png and b/toxygen/smileys/default/ne.png differ diff --git a/toxygen/smileys/default/nf.png b/toxygen/smileys/default/nf.png old mode 100644 new mode 100755 index a02fcb8..f9bcdda Binary files a/toxygen/smileys/default/nf.png and b/toxygen/smileys/default/nf.png differ diff --git a/toxygen/smileys/default/ng.png b/toxygen/smileys/default/ng.png old mode 100644 new mode 100755 index cc46ceb..3eea2e0 Binary files a/toxygen/smileys/default/ng.png and b/toxygen/smileys/default/ng.png differ diff --git a/toxygen/smileys/default/ni.png b/toxygen/smileys/default/ni.png old mode 100644 new mode 100755 index 4171012..3969aaa Binary files a/toxygen/smileys/default/ni.png and b/toxygen/smileys/default/ni.png differ diff --git a/toxygen/smileys/default/nl.png b/toxygen/smileys/default/nl.png old mode 100644 new mode 100755 index 00df165..fe44791 Binary files a/toxygen/smileys/default/nl.png and b/toxygen/smileys/default/nl.png differ diff --git a/toxygen/smileys/default/no.png b/toxygen/smileys/default/no.png old mode 100644 new mode 100755 index d76758b..160b6b5 Binary files a/toxygen/smileys/default/no.png and b/toxygen/smileys/default/no.png differ diff --git a/toxygen/smileys/default/np.png b/toxygen/smileys/default/np.png old mode 100644 new mode 100755 index 48b9d27..aeb058b Binary files a/toxygen/smileys/default/np.png and b/toxygen/smileys/default/np.png differ diff --git a/toxygen/smileys/default/nr.png b/toxygen/smileys/default/nr.png old mode 100644 new mode 100755 index 10f1242..705fc33 Binary files a/toxygen/smileys/default/nr.png and b/toxygen/smileys/default/nr.png differ diff --git a/toxygen/smileys/default/nu.png b/toxygen/smileys/default/nu.png old mode 100644 new mode 100755 index abae7f1..c3ce4ae Binary files a/toxygen/smileys/default/nu.png and b/toxygen/smileys/default/nu.png differ diff --git a/toxygen/smileys/default/nz.png b/toxygen/smileys/default/nz.png old mode 100644 new mode 100755 index c92a8a9..10d6306 Binary files a/toxygen/smileys/default/nz.png and b/toxygen/smileys/default/nz.png differ diff --git a/toxygen/smileys/default/om.png b/toxygen/smileys/default/om.png old mode 100644 new mode 100755 index 53b7a32..2ffba7e Binary files a/toxygen/smileys/default/om.png and b/toxygen/smileys/default/om.png differ diff --git a/toxygen/smileys/default/pa.png b/toxygen/smileys/default/pa.png old mode 100644 new mode 100755 index 6b0c717..9b2ee9a Binary files a/toxygen/smileys/default/pa.png and b/toxygen/smileys/default/pa.png differ diff --git a/toxygen/smileys/default/pe.png b/toxygen/smileys/default/pe.png old mode 100644 new mode 100755 index 2e9d19b..62a0497 Binary files a/toxygen/smileys/default/pe.png and b/toxygen/smileys/default/pe.png differ diff --git a/toxygen/smileys/default/pf.png b/toxygen/smileys/default/pf.png old mode 100644 new mode 100755 index cac6edd..771a0f6 Binary files a/toxygen/smileys/default/pf.png and b/toxygen/smileys/default/pf.png differ diff --git a/toxygen/smileys/default/pg.png b/toxygen/smileys/default/pg.png old mode 100644 new mode 100755 index 0ee77b3..10d6233 Binary files a/toxygen/smileys/default/pg.png and b/toxygen/smileys/default/pg.png differ diff --git a/toxygen/smileys/default/ph.png b/toxygen/smileys/default/ph.png old mode 100644 new mode 100755 index 464cb77..b89e159 Binary files a/toxygen/smileys/default/ph.png and b/toxygen/smileys/default/ph.png differ diff --git a/toxygen/smileys/default/pk.png b/toxygen/smileys/default/pk.png old mode 100644 new mode 100755 index de39a39..e9df70c Binary files a/toxygen/smileys/default/pk.png and b/toxygen/smileys/default/pk.png differ diff --git a/toxygen/smileys/default/pl.png b/toxygen/smileys/default/pl.png old mode 100644 new mode 100755 index ed09b83..d413d01 Binary files a/toxygen/smileys/default/pl.png and b/toxygen/smileys/default/pl.png differ diff --git a/toxygen/smileys/default/pm.png b/toxygen/smileys/default/pm.png old mode 100644 new mode 100755 index 507dd9f..ba91d2c Binary files a/toxygen/smileys/default/pm.png and b/toxygen/smileys/default/pm.png differ diff --git a/toxygen/smileys/default/pn.png b/toxygen/smileys/default/pn.png old mode 100644 new mode 100755 index fb14070..aa9344f Binary files a/toxygen/smileys/default/pn.png and b/toxygen/smileys/default/pn.png differ diff --git a/toxygen/smileys/default/pr.png b/toxygen/smileys/default/pr.png old mode 100644 new mode 100755 index 452991e..82d9130 Binary files a/toxygen/smileys/default/pr.png and b/toxygen/smileys/default/pr.png differ diff --git a/toxygen/smileys/default/ps.png b/toxygen/smileys/default/ps.png old mode 100644 new mode 100755 index ead1ff3..f5f5477 Binary files a/toxygen/smileys/default/ps.png and b/toxygen/smileys/default/ps.png differ diff --git a/toxygen/smileys/default/pt.png b/toxygen/smileys/default/pt.png old mode 100644 new mode 100755 index 98dddc4..ece7980 Binary files a/toxygen/smileys/default/pt.png and b/toxygen/smileys/default/pt.png differ diff --git a/toxygen/smileys/default/pw.png b/toxygen/smileys/default/pw.png old mode 100644 new mode 100755 index e7b5f90..6178b25 Binary files a/toxygen/smileys/default/pw.png and b/toxygen/smileys/default/pw.png differ diff --git a/toxygen/smileys/default/py.png b/toxygen/smileys/default/py.png old mode 100644 new mode 100755 index ae83d82..cb8723c Binary files a/toxygen/smileys/default/py.png and b/toxygen/smileys/default/py.png differ diff --git a/toxygen/smileys/default/qa.png b/toxygen/smileys/default/qa.png old mode 100644 new mode 100755 index edea054..ed4c621 Binary files a/toxygen/smileys/default/qa.png and b/toxygen/smileys/default/qa.png differ diff --git a/toxygen/smileys/default/re.png b/toxygen/smileys/default/re.png old mode 100644 new mode 100755 index 7a9a7fa..8332c4e Binary files a/toxygen/smileys/default/re.png and b/toxygen/smileys/default/re.png differ diff --git a/toxygen/smileys/default/ro.png b/toxygen/smileys/default/ro.png old mode 100644 new mode 100755 index 6d38ac7..57e74a6 Binary files a/toxygen/smileys/default/ro.png and b/toxygen/smileys/default/ro.png differ diff --git a/toxygen/smileys/default/rs.png b/toxygen/smileys/default/rs.png index 178e8b4..9439a5b 100644 Binary files a/toxygen/smileys/default/rs.png and b/toxygen/smileys/default/rs.png differ diff --git a/toxygen/smileys/default/ru.png b/toxygen/smileys/default/ru.png old mode 100644 new mode 100755 index 6f73c01..47da421 Binary files a/toxygen/smileys/default/ru.png and b/toxygen/smileys/default/ru.png differ diff --git a/toxygen/smileys/default/rw.png b/toxygen/smileys/default/rw.png old mode 100644 new mode 100755 index 33f99b9..5356491 Binary files a/toxygen/smileys/default/rw.png and b/toxygen/smileys/default/rw.png differ diff --git a/toxygen/smileys/default/sa.png b/toxygen/smileys/default/sa.png old mode 100644 new mode 100755 index 2057140..b4641c7 Binary files a/toxygen/smileys/default/sa.png and b/toxygen/smileys/default/sa.png differ diff --git a/toxygen/smileys/default/sb.png b/toxygen/smileys/default/sb.png old mode 100644 new mode 100755 index 7b61cab..a9937cc Binary files a/toxygen/smileys/default/sb.png and b/toxygen/smileys/default/sb.png differ diff --git a/toxygen/smileys/default/sc.png b/toxygen/smileys/default/sc.png old mode 100644 new mode 100755 index a222766..39ee371 Binary files a/toxygen/smileys/default/sc.png and b/toxygen/smileys/default/sc.png differ diff --git a/toxygen/smileys/default/scotland.png b/toxygen/smileys/default/scotland.png old mode 100644 new mode 100755 index 44ef46d..a0e57b4 Binary files a/toxygen/smileys/default/scotland.png and b/toxygen/smileys/default/scotland.png differ diff --git a/toxygen/smileys/default/sd.png b/toxygen/smileys/default/sd.png old mode 100644 new mode 100755 index d3a1f2b..eaab69e Binary files a/toxygen/smileys/default/sd.png and b/toxygen/smileys/default/sd.png differ diff --git a/toxygen/smileys/default/se.png b/toxygen/smileys/default/se.png old mode 100644 new mode 100755 index 995f965..1994653 Binary files a/toxygen/smileys/default/se.png and b/toxygen/smileys/default/se.png differ diff --git a/toxygen/smileys/default/sg.png b/toxygen/smileys/default/sg.png old mode 100644 new mode 100755 index 35f8df7..dd34d61 Binary files a/toxygen/smileys/default/sg.png and b/toxygen/smileys/default/sg.png differ diff --git a/toxygen/smileys/default/sh.png b/toxygen/smileys/default/sh.png old mode 100644 new mode 100755 index 34f77a7..4b1d2a2 Binary files a/toxygen/smileys/default/sh.png and b/toxygen/smileys/default/sh.png differ diff --git a/toxygen/smileys/default/si.png b/toxygen/smileys/default/si.png old mode 100644 new mode 100755 index 0e218b6..bb1476f Binary files a/toxygen/smileys/default/si.png and b/toxygen/smileys/default/si.png differ diff --git a/toxygen/smileys/default/sj.png b/toxygen/smileys/default/sj.png old mode 100644 new mode 100755 index eb91f75..160b6b5 Binary files a/toxygen/smileys/default/sj.png and b/toxygen/smileys/default/sj.png differ diff --git a/toxygen/smileys/default/sk.png b/toxygen/smileys/default/sk.png old mode 100644 new mode 100755 index 1d389f7..7ccbc82 Binary files a/toxygen/smileys/default/sk.png and b/toxygen/smileys/default/sk.png differ diff --git a/toxygen/smileys/default/sl.png b/toxygen/smileys/default/sl.png old mode 100644 new mode 100755 index 4e620b3..12d812d Binary files a/toxygen/smileys/default/sl.png and b/toxygen/smileys/default/sl.png differ diff --git a/toxygen/smileys/default/sm.png b/toxygen/smileys/default/sm.png old mode 100644 new mode 100755 index 9b02225..3df2fdc Binary files a/toxygen/smileys/default/sm.png and b/toxygen/smileys/default/sm.png differ diff --git a/toxygen/smileys/default/sn.png b/toxygen/smileys/default/sn.png old mode 100644 new mode 100755 index 188e42a..eabb71d Binary files a/toxygen/smileys/default/sn.png and b/toxygen/smileys/default/sn.png differ diff --git a/toxygen/smileys/default/so.png b/toxygen/smileys/default/so.png old mode 100644 new mode 100755 index f1a1dfc..4a1ea4b Binary files a/toxygen/smileys/default/so.png and b/toxygen/smileys/default/so.png differ diff --git a/toxygen/smileys/default/sr.png b/toxygen/smileys/default/sr.png old mode 100644 new mode 100755 index d6be029..5eff927 Binary files a/toxygen/smileys/default/sr.png and b/toxygen/smileys/default/sr.png differ diff --git a/toxygen/smileys/default/st.png b/toxygen/smileys/default/st.png old mode 100644 new mode 100755 index 0786db0..2978557 Binary files a/toxygen/smileys/default/st.png and b/toxygen/smileys/default/st.png differ diff --git a/toxygen/smileys/default/sv.png b/toxygen/smileys/default/sv.png old mode 100644 new mode 100755 index 7b533d1..2498799 Binary files a/toxygen/smileys/default/sv.png and b/toxygen/smileys/default/sv.png differ diff --git a/toxygen/smileys/default/sy.png b/toxygen/smileys/default/sy.png old mode 100644 new mode 100755 index dfecd39..f5ce30d Binary files a/toxygen/smileys/default/sy.png and b/toxygen/smileys/default/sy.png differ diff --git a/toxygen/smileys/default/sz.png b/toxygen/smileys/default/sz.png old mode 100644 new mode 100755 index 4d4fb90..914ee86 Binary files a/toxygen/smileys/default/sz.png and b/toxygen/smileys/default/sz.png differ diff --git a/toxygen/smileys/default/tc.png b/toxygen/smileys/default/tc.png old mode 100644 new mode 100755 index eaec510..8fc1156 Binary files a/toxygen/smileys/default/tc.png and b/toxygen/smileys/default/tc.png differ diff --git a/toxygen/smileys/default/td.png b/toxygen/smileys/default/td.png old mode 100644 new mode 100755 index 6236dfa..667f21f Binary files a/toxygen/smileys/default/td.png and b/toxygen/smileys/default/td.png differ diff --git a/toxygen/smileys/default/tf.png b/toxygen/smileys/default/tf.png old mode 100644 new mode 100755 index 8534274..80529a4 Binary files a/toxygen/smileys/default/tf.png and b/toxygen/smileys/default/tf.png differ diff --git a/toxygen/smileys/default/tg.png b/toxygen/smileys/default/tg.png old mode 100644 new mode 100755 index ad50b11..3aa00ad Binary files a/toxygen/smileys/default/tg.png and b/toxygen/smileys/default/tg.png differ diff --git a/toxygen/smileys/default/th.png b/toxygen/smileys/default/th.png old mode 100644 new mode 100755 index bb00577..dd8ba91 Binary files a/toxygen/smileys/default/th.png and b/toxygen/smileys/default/th.png differ diff --git a/toxygen/smileys/default/tj.png b/toxygen/smileys/default/tj.png old mode 100644 new mode 100755 index 060d647..617bf64 Binary files a/toxygen/smileys/default/tj.png and b/toxygen/smileys/default/tj.png differ diff --git a/toxygen/smileys/default/tk.png b/toxygen/smileys/default/tk.png old mode 100644 new mode 100755 index 050fd63..67b8c8c Binary files a/toxygen/smileys/default/tk.png and b/toxygen/smileys/default/tk.png differ diff --git a/toxygen/smileys/default/tl.png b/toxygen/smileys/default/tl.png old mode 100644 new mode 100755 index a4fc566..77da181 Binary files a/toxygen/smileys/default/tl.png and b/toxygen/smileys/default/tl.png differ diff --git a/toxygen/smileys/default/tm.png b/toxygen/smileys/default/tm.png old mode 100644 new mode 100755 index 2981188..828020e Binary files a/toxygen/smileys/default/tm.png and b/toxygen/smileys/default/tm.png differ diff --git a/toxygen/smileys/default/tn.png b/toxygen/smileys/default/tn.png old mode 100644 new mode 100755 index 202faea..183cdd3 Binary files a/toxygen/smileys/default/tn.png and b/toxygen/smileys/default/tn.png differ diff --git a/toxygen/smileys/default/to.png b/toxygen/smileys/default/to.png old mode 100644 new mode 100755 index 63949b1..f89b8ba Binary files a/toxygen/smileys/default/to.png and b/toxygen/smileys/default/to.png differ diff --git a/toxygen/smileys/default/tox.png b/toxygen/smileys/default/tox.png old mode 100644 new mode 100755 index ad5e1d5..1c551f7 Binary files a/toxygen/smileys/default/tox.png and b/toxygen/smileys/default/tox.png differ diff --git a/toxygen/smileys/default/tr.png b/toxygen/smileys/default/tr.png old mode 100644 new mode 100755 index 58ee839..be32f77 Binary files a/toxygen/smileys/default/tr.png and b/toxygen/smileys/default/tr.png differ diff --git a/toxygen/smileys/default/tt.png b/toxygen/smileys/default/tt.png old mode 100644 new mode 100755 index e7d7502..2a11c1e Binary files a/toxygen/smileys/default/tt.png and b/toxygen/smileys/default/tt.png differ diff --git a/toxygen/smileys/default/tv.png b/toxygen/smileys/default/tv.png old mode 100644 new mode 100755 index 83720a3..28274c5 Binary files a/toxygen/smileys/default/tv.png and b/toxygen/smileys/default/tv.png differ diff --git a/toxygen/smileys/default/tw.png b/toxygen/smileys/default/tw.png old mode 100644 new mode 100755 index 3e751fd..f31c654 Binary files a/toxygen/smileys/default/tw.png and b/toxygen/smileys/default/tw.png differ diff --git a/toxygen/smileys/default/tz.png b/toxygen/smileys/default/tz.png old mode 100644 new mode 100755 index e1cde1b..c00ff79 Binary files a/toxygen/smileys/default/tz.png and b/toxygen/smileys/default/tz.png differ diff --git a/toxygen/smileys/default/ua.png b/toxygen/smileys/default/ua.png old mode 100644 new mode 100755 index 100319b..09563a2 Binary files a/toxygen/smileys/default/ua.png and b/toxygen/smileys/default/ua.png differ diff --git a/toxygen/smileys/default/ug.png b/toxygen/smileys/default/ug.png old mode 100644 new mode 100755 index 659f629..33f4aff Binary files a/toxygen/smileys/default/ug.png and b/toxygen/smileys/default/ug.png differ diff --git a/toxygen/smileys/default/um.png b/toxygen/smileys/default/um.png old mode 100644 new mode 100755 index 2f425ad..c1dd965 Binary files a/toxygen/smileys/default/um.png and b/toxygen/smileys/default/um.png differ diff --git a/toxygen/smileys/default/us.png b/toxygen/smileys/default/us.png old mode 100644 new mode 100755 index fae49a0..10f451f Binary files a/toxygen/smileys/default/us.png and b/toxygen/smileys/default/us.png differ diff --git a/toxygen/smileys/default/uy.png b/toxygen/smileys/default/uy.png old mode 100644 new mode 100755 index dc42cd1..31d948a Binary files a/toxygen/smileys/default/uy.png and b/toxygen/smileys/default/uy.png differ diff --git a/toxygen/smileys/default/uz.png b/toxygen/smileys/default/uz.png old mode 100644 new mode 100755 index e2a6331..fef5dc1 Binary files a/toxygen/smileys/default/uz.png and b/toxygen/smileys/default/uz.png differ diff --git a/toxygen/smileys/default/va.png b/toxygen/smileys/default/va.png old mode 100644 new mode 100755 index f6ac0a5..b31eaf2 Binary files a/toxygen/smileys/default/va.png and b/toxygen/smileys/default/va.png differ diff --git a/toxygen/smileys/default/vc.png b/toxygen/smileys/default/vc.png old mode 100644 new mode 100755 index d737c4b..8fa17b0 Binary files a/toxygen/smileys/default/vc.png and b/toxygen/smileys/default/vc.png differ diff --git a/toxygen/smileys/default/ve.png b/toxygen/smileys/default/ve.png old mode 100644 new mode 100755 index 629fe46..00c90f9 Binary files a/toxygen/smileys/default/ve.png and b/toxygen/smileys/default/ve.png differ diff --git a/toxygen/smileys/default/vg.png b/toxygen/smileys/default/vg.png old mode 100644 new mode 100755 index b250b1f..4156907 Binary files a/toxygen/smileys/default/vg.png and b/toxygen/smileys/default/vg.png differ diff --git a/toxygen/smileys/default/vi.png b/toxygen/smileys/default/vi.png old mode 100644 new mode 100755 index 22623b0..ed26915 Binary files a/toxygen/smileys/default/vi.png and b/toxygen/smileys/default/vi.png differ diff --git a/toxygen/smileys/default/vn.png b/toxygen/smileys/default/vn.png old mode 100644 new mode 100755 index 76c3aa7..ec7cd48 Binary files a/toxygen/smileys/default/vn.png and b/toxygen/smileys/default/vn.png differ diff --git a/toxygen/smileys/default/vu.png b/toxygen/smileys/default/vu.png old mode 100644 new mode 100755 index c92506e..b3397bc Binary files a/toxygen/smileys/default/vu.png and b/toxygen/smileys/default/vu.png differ diff --git a/toxygen/smileys/default/wales.png b/toxygen/smileys/default/wales.png old mode 100644 new mode 100755 index bc0200b..e0d7cee Binary files a/toxygen/smileys/default/wales.png and b/toxygen/smileys/default/wales.png differ diff --git a/toxygen/smileys/default/wf.png b/toxygen/smileys/default/wf.png old mode 100644 new mode 100755 index 879d578..9f95587 Binary files a/toxygen/smileys/default/wf.png and b/toxygen/smileys/default/wf.png differ diff --git a/toxygen/smileys/default/ws.png b/toxygen/smileys/default/ws.png old mode 100644 new mode 100755 index 3f3e7d7..c169508 Binary files a/toxygen/smileys/default/ws.png and b/toxygen/smileys/default/ws.png differ diff --git a/toxygen/smileys/default/wtox.png b/toxygen/smileys/default/wtox.png old mode 100644 new mode 100755 index a5c49a7..d95f396 Binary files a/toxygen/smileys/default/wtox.png and b/toxygen/smileys/default/wtox.png differ diff --git a/toxygen/smileys/default/ye.png b/toxygen/smileys/default/ye.png old mode 100644 new mode 100755 index 9dcf729..468dfad Binary files a/toxygen/smileys/default/ye.png and b/toxygen/smileys/default/ye.png differ diff --git a/toxygen/smileys/default/yt.png b/toxygen/smileys/default/yt.png old mode 100644 new mode 100755 index 6170745..c298f37 Binary files a/toxygen/smileys/default/yt.png and b/toxygen/smileys/default/yt.png differ diff --git a/toxygen/smileys/default/za.png b/toxygen/smileys/default/za.png old mode 100644 new mode 100755 index ad4d0eb..57c58e2 Binary files a/toxygen/smileys/default/za.png and b/toxygen/smileys/default/za.png differ diff --git a/toxygen/smileys/default/zm.png b/toxygen/smileys/default/zm.png old mode 100644 new mode 100755 index 38d8a3c..c25b07b Binary files a/toxygen/smileys/default/zm.png and b/toxygen/smileys/default/zm.png differ diff --git a/toxygen/smileys/default/zw.png b/toxygen/smileys/default/zw.png old mode 100644 new mode 100755 index e8e51b7..53c9725 Binary files a/toxygen/smileys/default/zw.png and b/toxygen/smileys/default/zw.png differ diff --git a/toxygen/smileys/ksk/angry.png b/toxygen/smileys/ksk/angry.png index 2659bf2..12a51f5 100644 Binary files a/toxygen/smileys/ksk/angry.png and b/toxygen/smileys/ksk/angry.png differ diff --git a/toxygen/smileys/ksk/angry2.png b/toxygen/smileys/ksk/angry2.png index 6ecbb1e..f345319 100644 Binary files a/toxygen/smileys/ksk/angry2.png and b/toxygen/smileys/ksk/angry2.png differ diff --git a/toxygen/smileys/ksk/angry3.png b/toxygen/smileys/ksk/angry3.png index 9b9ebc0..a68d15d 100644 Binary files a/toxygen/smileys/ksk/angry3.png and b/toxygen/smileys/ksk/angry3.png differ diff --git a/toxygen/smileys/ksk/blink.png b/toxygen/smileys/ksk/blink.png index b7fe238..4593e9a 100644 Binary files a/toxygen/smileys/ksk/blink.png and b/toxygen/smileys/ksk/blink.png differ diff --git a/toxygen/smileys/ksk/bluestar.png b/toxygen/smileys/ksk/bluestar.png index 21f37ca..3f84805 100644 Binary files a/toxygen/smileys/ksk/bluestar.png and b/toxygen/smileys/ksk/bluestar.png differ diff --git a/toxygen/smileys/ksk/calm.png b/toxygen/smileys/ksk/calm.png index da19990..cc04aa2 100644 Binary files a/toxygen/smileys/ksk/calm.png and b/toxygen/smileys/ksk/calm.png differ diff --git a/toxygen/smileys/ksk/cool.png b/toxygen/smileys/ksk/cool.png index 891ed33..b223eb3 100644 Binary files a/toxygen/smileys/ksk/cool.png and b/toxygen/smileys/ksk/cool.png differ diff --git a/toxygen/smileys/ksk/cool2.png b/toxygen/smileys/ksk/cool2.png index 3dea030..3e04321 100644 Binary files a/toxygen/smileys/ksk/cool2.png and b/toxygen/smileys/ksk/cool2.png differ diff --git a/toxygen/smileys/ksk/cry.png b/toxygen/smileys/ksk/cry.png index fea2481..bf422fb 100644 Binary files a/toxygen/smileys/ksk/cry.png and b/toxygen/smileys/ksk/cry.png differ diff --git a/toxygen/smileys/ksk/dead.png b/toxygen/smileys/ksk/dead.png index 7b22495..2ea2ca3 100644 Binary files a/toxygen/smileys/ksk/dead.png and b/toxygen/smileys/ksk/dead.png differ diff --git a/toxygen/smileys/ksk/evil.png b/toxygen/smileys/ksk/evil.png index 140a259..a0483e9 100644 Binary files a/toxygen/smileys/ksk/evil.png and b/toxygen/smileys/ksk/evil.png differ diff --git a/toxygen/smileys/ksk/evil2.png b/toxygen/smileys/ksk/evil2.png index c01efdd..0388ab2 100644 Binary files a/toxygen/smileys/ksk/evil2.png and b/toxygen/smileys/ksk/evil2.png differ diff --git a/toxygen/smileys/ksk/flower.png b/toxygen/smileys/ksk/flower.png index 5463fda..ca53961 100644 Binary files a/toxygen/smileys/ksk/flower.png and b/toxygen/smileys/ksk/flower.png differ diff --git a/toxygen/smileys/ksk/getlost.png b/toxygen/smileys/ksk/getlost.png index 2c75727..c54c5d0 100644 Binary files a/toxygen/smileys/ksk/getlost.png and b/toxygen/smileys/ksk/getlost.png differ diff --git a/toxygen/smileys/ksk/greenstar.png b/toxygen/smileys/ksk/greenstar.png index b557c50..aa0e9eb 100644 Binary files a/toxygen/smileys/ksk/greenstar.png and b/toxygen/smileys/ksk/greenstar.png differ diff --git a/toxygen/smileys/ksk/grin.png b/toxygen/smileys/ksk/grin.png index b35bf24..ed79b59 100644 Binary files a/toxygen/smileys/ksk/grin.png and b/toxygen/smileys/ksk/grin.png differ diff --git a/toxygen/smileys/ksk/heart.png b/toxygen/smileys/ksk/heart.png index 25d3d7f..32a3e62 100644 Binary files a/toxygen/smileys/ksk/heart.png and b/toxygen/smileys/ksk/heart.png differ diff --git a/toxygen/smileys/ksk/kiss.png b/toxygen/smileys/ksk/kiss.png index 4764d69..5ba0bea 100644 Binary files a/toxygen/smileys/ksk/kiss.png and b/toxygen/smileys/ksk/kiss.png differ diff --git a/toxygen/smileys/ksk/leaf.png b/toxygen/smileys/ksk/leaf.png index 7598896..c04bedd 100644 Binary files a/toxygen/smileys/ksk/leaf.png and b/toxygen/smileys/ksk/leaf.png differ diff --git a/toxygen/smileys/ksk/lol.png b/toxygen/smileys/ksk/lol.png index 9d42add..801baab 100644 Binary files a/toxygen/smileys/ksk/lol.png and b/toxygen/smileys/ksk/lol.png differ diff --git a/toxygen/smileys/ksk/none.png b/toxygen/smileys/ksk/none.png index 03d421f..99487f5 100644 Binary files a/toxygen/smileys/ksk/none.png and b/toxygen/smileys/ksk/none.png differ diff --git a/toxygen/smileys/ksk/none2.png b/toxygen/smileys/ksk/none2.png index 0fc9cf1..352e102 100644 Binary files a/toxygen/smileys/ksk/none2.png and b/toxygen/smileys/ksk/none2.png differ diff --git a/toxygen/smileys/ksk/notes.png b/toxygen/smileys/ksk/notes.png index 6c07260..7389846 100644 Binary files a/toxygen/smileys/ksk/notes.png and b/toxygen/smileys/ksk/notes.png differ diff --git a/toxygen/smileys/ksk/oops.png b/toxygen/smileys/ksk/oops.png index 744a2a0..3fef724 100644 Binary files a/toxygen/smileys/ksk/oops.png and b/toxygen/smileys/ksk/oops.png differ diff --git a/toxygen/smileys/ksk/pawn.png b/toxygen/smileys/ksk/pawn.png index cce0cad..566d43f 100644 Binary files a/toxygen/smileys/ksk/pawn.png and b/toxygen/smileys/ksk/pawn.png differ diff --git a/toxygen/smileys/ksk/pleased.png b/toxygen/smileys/ksk/pleased.png index 2c7e60d..8a265ea 100644 Binary files a/toxygen/smileys/ksk/pleased.png and b/toxygen/smileys/ksk/pleased.png differ diff --git a/toxygen/smileys/ksk/redstar.png b/toxygen/smileys/ksk/redstar.png index 33bcdf1..eb634bc 100644 Binary files a/toxygen/smileys/ksk/redstar.png and b/toxygen/smileys/ksk/redstar.png differ diff --git a/toxygen/smileys/ksk/sad.png b/toxygen/smileys/ksk/sad.png index 0a33174..3cafa05 100644 Binary files a/toxygen/smileys/ksk/sad.png and b/toxygen/smileys/ksk/sad.png differ diff --git a/toxygen/smileys/ksk/scared.png b/toxygen/smileys/ksk/scared.png index 1b5c55c..b9395c3 100644 Binary files a/toxygen/smileys/ksk/scared.png and b/toxygen/smileys/ksk/scared.png differ diff --git a/toxygen/smileys/ksk/shocked.png b/toxygen/smileys/ksk/shocked.png index 83e0850..8e137f6 100644 Binary files a/toxygen/smileys/ksk/shocked.png and b/toxygen/smileys/ksk/shocked.png differ diff --git a/toxygen/smileys/ksk/smile.png b/toxygen/smileys/ksk/smile.png index a431ca7..3f08d94 100644 Binary files a/toxygen/smileys/ksk/smile.png and b/toxygen/smileys/ksk/smile.png differ diff --git a/toxygen/smileys/ksk/smile2.png b/toxygen/smileys/ksk/smile2.png index 4d003ae..11ea087 100644 Binary files a/toxygen/smileys/ksk/smile2.png and b/toxygen/smileys/ksk/smile2.png differ diff --git a/toxygen/smileys/ksk/tongue.png b/toxygen/smileys/ksk/tongue.png index bf6c37e..840d743 100644 Binary files a/toxygen/smileys/ksk/tongue.png and b/toxygen/smileys/ksk/tongue.png differ diff --git a/toxygen/smileys/ksk/unwell.png b/toxygen/smileys/ksk/unwell.png index 5bca721..c093a9a 100644 Binary files a/toxygen/smileys/ksk/unwell.png and b/toxygen/smileys/ksk/unwell.png differ diff --git a/toxygen/smileys/ksk/yellowstar.png b/toxygen/smileys/ksk/yellowstar.png index 5e00805..f8a2672 100644 Binary files a/toxygen/smileys/ksk/yellowstar.png and b/toxygen/smileys/ksk/yellowstar.png differ diff --git a/toxygen/smileys/ksk/zzz.png b/toxygen/smileys/ksk/zzz.png index 0d17073..dff4463 100644 Binary files a/toxygen/smileys/ksk/zzz.png and b/toxygen/smileys/ksk/zzz.png differ diff --git a/toxygen/smileys/starwars/ackbar.png b/toxygen/smileys/starwars/ackbar.png index 1f8a4d5..0a0a482 100644 Binary files a/toxygen/smileys/starwars/ackbar.png and b/toxygen/smileys/starwars/ackbar.png differ diff --git a/toxygen/smileys/starwars/boba.png b/toxygen/smileys/starwars/boba.png index 1c234c5..88789dc 100644 Binary files a/toxygen/smileys/starwars/boba.png and b/toxygen/smileys/starwars/boba.png differ diff --git a/toxygen/smileys/starwars/c3p0.png b/toxygen/smileys/starwars/c3p0.png index be5adea..a37df94 100644 Binary files a/toxygen/smileys/starwars/c3p0.png and b/toxygen/smileys/starwars/c3p0.png differ diff --git a/toxygen/smileys/starwars/chewie.png b/toxygen/smileys/starwars/chewie.png index 8f5a5f6..669dd36 100644 Binary files a/toxygen/smileys/starwars/chewie.png and b/toxygen/smileys/starwars/chewie.png differ diff --git a/toxygen/smileys/starwars/confused.png b/toxygen/smileys/starwars/confused.png index 5cc2fd1..54afd60 100644 Binary files a/toxygen/smileys/starwars/confused.png and b/toxygen/smileys/starwars/confused.png differ diff --git a/toxygen/smileys/starwars/darthmaul.png b/toxygen/smileys/starwars/darthmaul.png index 57e8ca1..e536a7e 100644 Binary files a/toxygen/smileys/starwars/darthmaul.png and b/toxygen/smileys/starwars/darthmaul.png differ diff --git a/toxygen/smileys/starwars/darthsidious.png b/toxygen/smileys/starwars/darthsidious.png index de36348..6c787c1 100644 Binary files a/toxygen/smileys/starwars/darthsidious.png and b/toxygen/smileys/starwars/darthsidious.png differ diff --git a/toxygen/smileys/starwars/darthvader.png b/toxygen/smileys/starwars/darthvader.png index 66c1409..a0b01e4 100644 Binary files a/toxygen/smileys/starwars/darthvader.png and b/toxygen/smileys/starwars/darthvader.png differ diff --git a/toxygen/smileys/starwars/deathstar.png b/toxygen/smileys/starwars/deathstar.png index 3ee623c..383e730 100644 Binary files a/toxygen/smileys/starwars/deathstar.png and b/toxygen/smileys/starwars/deathstar.png differ diff --git a/toxygen/smileys/starwars/dualsith.png b/toxygen/smileys/starwars/dualsith.png index 732d6a3..39143ec 100644 Binary files a/toxygen/smileys/starwars/dualsith.png and b/toxygen/smileys/starwars/dualsith.png differ diff --git a/toxygen/smileys/starwars/grin.png b/toxygen/smileys/starwars/grin.png index 226f31d..8ee5ff0 100644 Binary files a/toxygen/smileys/starwars/grin.png and b/toxygen/smileys/starwars/grin.png differ diff --git a/toxygen/smileys/starwars/happy.png b/toxygen/smileys/starwars/happy.png index d1f08ed..68f8717 100644 Binary files a/toxygen/smileys/starwars/happy.png and b/toxygen/smileys/starwars/happy.png differ diff --git a/toxygen/smileys/starwars/jango.png b/toxygen/smileys/starwars/jango.png index dc731be..5275e7d 100644 Binary files a/toxygen/smileys/starwars/jango.png and b/toxygen/smileys/starwars/jango.png differ diff --git a/toxygen/smileys/starwars/jarjarbinks.png b/toxygen/smileys/starwars/jarjarbinks.png index b83ebbe..802c83a 100644 Binary files a/toxygen/smileys/starwars/jarjarbinks.png and b/toxygen/smileys/starwars/jarjarbinks.png differ diff --git a/toxygen/smileys/starwars/jedi.png b/toxygen/smileys/starwars/jedi.png index 6bc3173..f8ff638 100644 Binary files a/toxygen/smileys/starwars/jedi.png and b/toxygen/smileys/starwars/jedi.png differ diff --git a/toxygen/smileys/starwars/jedi2.png b/toxygen/smileys/starwars/jedi2.png index 3f3a39d..3550eb8 100644 Binary files a/toxygen/smileys/starwars/jedi2.png and b/toxygen/smileys/starwars/jedi2.png differ diff --git a/toxygen/smileys/starwars/leia.png b/toxygen/smileys/starwars/leia.png index 885087d..2e40729 100644 Binary files a/toxygen/smileys/starwars/leia.png and b/toxygen/smileys/starwars/leia.png differ diff --git a/toxygen/smileys/starwars/mad.png b/toxygen/smileys/starwars/mad.png index 0380c2e..6109783 100644 Binary files a/toxygen/smileys/starwars/mad.png and b/toxygen/smileys/starwars/mad.png differ diff --git a/toxygen/smileys/starwars/masteryoda.png b/toxygen/smileys/starwars/masteryoda.png index 080cc02..9d5f86f 100644 Binary files a/toxygen/smileys/starwars/masteryoda.png and b/toxygen/smileys/starwars/masteryoda.png differ diff --git a/toxygen/smileys/starwars/r2d2.png b/toxygen/smileys/starwars/r2d2.png index f0f0f9d..e114b3f 100644 Binary files a/toxygen/smileys/starwars/r2d2.png and b/toxygen/smileys/starwars/r2d2.png differ diff --git a/toxygen/smileys/starwars/sad.png b/toxygen/smileys/starwars/sad.png index c556767..484d18f 100644 Binary files a/toxygen/smileys/starwars/sad.png and b/toxygen/smileys/starwars/sad.png differ diff --git a/toxygen/smileys/starwars/samjackson.png b/toxygen/smileys/starwars/samjackson.png index 3c109d9..d6f8024 100644 Binary files a/toxygen/smileys/starwars/samjackson.png and b/toxygen/smileys/starwars/samjackson.png differ diff --git a/toxygen/smileys/starwars/shocked.png b/toxygen/smileys/starwars/shocked.png index 2770ab3..fa3747d 100644 Binary files a/toxygen/smileys/starwars/shocked.png and b/toxygen/smileys/starwars/shocked.png differ diff --git a/toxygen/smileys/starwars/sith.png b/toxygen/smileys/starwars/sith.png index 38223e9..e04a07c 100644 Binary files a/toxygen/smileys/starwars/sith.png and b/toxygen/smileys/starwars/sith.png differ diff --git a/toxygen/smileys/starwars/smile.png b/toxygen/smileys/starwars/smile.png index 79beb89..dcc34cb 100644 Binary files a/toxygen/smileys/starwars/smile.png and b/toxygen/smileys/starwars/smile.png differ diff --git a/toxygen/smileys/starwars/stormtrooper.png b/toxygen/smileys/starwars/stormtrooper.png index a4a4cfe..5014c1e 100644 Binary files a/toxygen/smileys/starwars/stormtrooper.png and b/toxygen/smileys/starwars/stormtrooper.png differ diff --git a/toxygen/smileys/starwars/tape.png b/toxygen/smileys/starwars/tape.png index 04cefdb..866ac05 100644 Binary files a/toxygen/smileys/starwars/tape.png and b/toxygen/smileys/starwars/tape.png differ diff --git a/toxygen/smileys/starwars/tongue.png b/toxygen/smileys/starwars/tongue.png index 8c22f1e..b10b914 100644 Binary files a/toxygen/smileys/starwars/tongue.png and b/toxygen/smileys/starwars/tongue.png differ diff --git a/toxygen/smileys/starwars/wink.png b/toxygen/smileys/starwars/wink.png index 50d0edc..dd5a292 100644 Binary files a/toxygen/smileys/starwars/wink.png and b/toxygen/smileys/starwars/wink.png differ diff --git a/toxygen/smileys/starwars/x-wing.png b/toxygen/smileys/starwars/x-wing.png index e48036b..e2a633a 100644 Binary files a/toxygen/smileys/starwars/x-wing.png and b/toxygen/smileys/starwars/x-wing.png differ diff --git a/toxygen/smileys/starwars/y-wing.png b/toxygen/smileys/starwars/y-wing.png index f750184..59d0b52 100644 Binary files a/toxygen/smileys/starwars/y-wing.png and b/toxygen/smileys/starwars/y-wing.png differ diff --git a/toxygen/stickers/__init__.py b/toxygen/stickers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/stickers/stickers.py b/toxygen/stickers/stickers.py deleted file mode 100644 index 56a0a29..0000000 --- a/toxygen/stickers/stickers.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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/stickers/tox/black.png b/toxygen/stickers/tox/black.png old mode 100644 new mode 100755 index 3a38a70..5d1e0eb Binary files a/toxygen/stickers/tox/black.png and b/toxygen/stickers/tox/black.png differ diff --git a/toxygen/stickers/tox/red.png b/toxygen/stickers/tox/red.png old mode 100644 new mode 100755 index cf7fb77..3185319 Binary files a/toxygen/stickers/tox/red.png and b/toxygen/stickers/tox/red.png differ diff --git a/toxygen/stickers/tox/tox_logo.png b/toxygen/stickers/tox/tox_logo.png old mode 100644 new mode 100755 index afb2d2d..977c5fc Binary files a/toxygen/stickers/tox/tox_logo.png and b/toxygen/stickers/tox/tox_logo.png differ diff --git a/toxygen/stickers/tox/tox_logo_1.png b/toxygen/stickers/tox/tox_logo_1.png old mode 100644 new mode 100755 index 038d833..cf1932c Binary files a/toxygen/stickers/tox/tox_logo_1.png and b/toxygen/stickers/tox/tox_logo_1.png differ diff --git a/toxygen/stickers/tox/white.png b/toxygen/stickers/tox/white.png old mode 100644 new mode 100755 index bee4a90..745b597 Binary files a/toxygen/stickers/tox/white.png and b/toxygen/stickers/tox/white.png differ diff --git a/toxygen/styles/dark_style.qss b/toxygen/styles/dark_style.qss deleted file mode 100644 index ece5ec3..0000000 --- a/toxygen/styles/dark_style.qss +++ /dev/null @@ -1,1335 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) <2013-2014> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -QToolTip -{ - border: 1px solid #3A3939; - background-color: rgb(90, 102, 117);; - color: white; - padding: 1px; - opacity: 200; -} - -QWidget -{ - color: silver; - background-color: #302F2F; - selection-background-color: #A9A9A9; - selection-color: black; - background-clip: border; - border-image: none; - outline: 0; -} - -QWidget:item:hover -{ - background-color: #78879b; - color: black; -} - -QWidget:item:selected -{ - background-color: #A9A9A9; -} - -QProgressBar:horizontal { - border: 1px solid #3A3939; - text-align: center; - padding: 1px; - background: #201F1F; -} -QProgressBar::chunk:horizontal { - background-color: qlineargradient(spread:reflect, x1:1, y1:0.545, x2:1, y2:0, stop:0 rgba(28, 66, 111, 255), stop:1 rgba(37, 87, 146, 255)); -} - -QCheckBox:disabled -{ - color: #777777; -} -QCheckBox::indicator, -QGroupBox::indicator -{ - width: 18px; - height: 18px; -} -QGroupBox::indicator -{ - margin-left: 2px; -} - -QCheckBox::indicator:unchecked, -QCheckBox::indicator:unchecked:hover, -QGroupBox::indicator:unchecked, -QGroupBox::indicator:unchecked:hover -{ - image: url(:/qss_icons/rc/checkbox_unchecked.png); -} - -QCheckBox::indicator:unchecked:focus, -QCheckBox::indicator:unchecked:pressed, -QGroupBox::indicator:unchecked:focus, -QGroupBox::indicator:unchecked:pressed -{ - border: none; - image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); -} - -QCheckBox::indicator:checked, -QCheckBox::indicator:checked:hover, -QGroupBox::indicator:checked, -QGroupBox::indicator:checked:hover -{ - image: url(:/qss_icons/rc/checkbox_checked.png); -} - -QCheckBox::indicator:checked:focus, -QCheckBox::indicator:checked:pressed, -QGroupBox::indicator:checked:focus, -QGroupBox::indicator:checked:pressed -{ - border: none; - image: url(:/qss_icons/rc/checkbox_checked_focus.png); -} - -QCheckBox::indicator:indeterminate, -QCheckBox::indicator:indeterminate:hover, -QCheckBox::indicator:indeterminate:pressed -QGroupBox::indicator:indeterminate, -QGroupBox::indicator:indeterminate:hover, -QGroupBox::indicator:indeterminate:pressed -{ - image: url(:/qss_icons/rc/checkbox_indeterminate.png); -} - -QCheckBox::indicator:indeterminate:focus, -QGroupBox::indicator:indeterminate:focus -{ - image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); -} - -QCheckBox::indicator:checked:disabled, -QGroupBox::indicator:checked:disabled -{ - image: url(:/qss_icons/rc/checkbox_checked_disabled.png); -} - -QCheckBox::indicator:unchecked:disabled, -QGroupBox::indicator:unchecked:disabled -{ - image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); -} - -QRadioButton -{ - spacing: 5px; - outline: none; - color: #bbb; - margin-bottom: 2px; -} - -QRadioButton:disabled -{ - color: #777777; -} -QRadioButton::indicator -{ - width: 21px; - height: 21px; -} - -QRadioButton::indicator:unchecked, -QRadioButton::indicator:unchecked:hover -{ - image: url(:/qss_icons/rc/radio_unchecked.png); -} - -QRadioButton::indicator:unchecked:focus, -QRadioButton::indicator:unchecked:pressed -{ - border: none; - outline: none; - image: url(:/qss_icons/rc/radio_unchecked_focus.png); -} - -QRadioButton::indicator:checked, -QRadioButton::indicator:checked:hover -{ - border: none; - outline: none; - image: url(:/qss_icons/rc/radio_checked.png); -} - -QRadioButton::indicator:checked:focus, -QRadioButton::indicato::menu-arrowr:checked:pressed -{ - border: none; - outline: none; - image: url(:/qss_icons/rc/radio_checked_focus.png); -} - -QRadioButton::indicator:indeterminate, -QRadioButton::indicator:indeterminate:hover, -QRadioButton::indicator:indeterminate:pressed -{ - image: url(:/qss_icons/rc/radio_indeterminate.png); -} - -QRadioButton::indicator:checked:disabled -{ - outline: none; - image: url(:/qss_icons/rc/radio_checked_disabled.png); -} - -QRadioButton::indicator:unchecked:disabled -{ - image: url(:/qss_icons/rc/radio_unchecked_disabled.png); -} - - -QMenuBar -{ - background-color: #302F2F; - color: silver; -} - -QMenuBar::item -{ - background: transparent; -} - -QMenuBar::item:selected -{ - background: transparent; - border: 1px solid #A9A9A9; -} - -QMenuBar::item:pressed -{ - border: 1px solid #3A3939; - background-color: #A9A9A9; - color: black; - margin-bottom:-1px; - padding-bottom:1px; -} - -QMenu -{ - border: 1px solid #3A3939; - color: silver; - margin: 2px; -} - -QMenu::icon -{ - margin: 5px; -} - -QMenu::item -{ - padding: 5px 30px 5px 30px; - margin-left: 5px; - border: 1px solid transparent; /* reserve space for selection border */ -} - -QMenu::item:selected -{ - color: black; -} - -QMenu::separator { - height: 2px; - background: lightblue; - margin-left: 10px; - margin-right: 5px; -} - -QMenu::indicator { - width: 18px; - height: 18px; -} - -/* non-exclusive indicator = check box style indicator - (see QActionGroup::setExclusive) */ -QMenu::indicator:non-exclusive:unchecked { - image: url(:/qss_icons/rc/checkbox_unchecked.png); -} - -QMenu::indicator:non-exclusive:unchecked:selected { - image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); -} - -QMenu::indicator:non-exclusive:checked { - image: url(:/qss_icons/rc/checkbox_checked.png); -} - -QMenu::indicator:non-exclusive:checked:selected { - image: url(:/qss_icons/rc/checkbox_checked_disabled.png); -} - -/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ -QMenu::indicator:exclusive:unchecked { - image: url(:/qss_icons/rc/radio_unchecked.png); -} - -QMenu::indicator:exclusive:unchecked:selected { - image: url(:/qss_icons/rc/radio_unchecked_disabled.png); -} - -QMenu::indicator:exclusive:checked { - image: url(:/qss_icons/rc/radio_checked.png); -} - -QMenu::indicator:exclusive:checked:selected { - image: url(:/qss_icons/rc/radio_checked_disabled.png); -} - -QMenu::right-arrow { - margin: 5px; - image: url(:/qss_icons/rc/right_arrow.png) -} - - -QWidget:disabled -{ - color: #404040; - background-color: #302F2F; -} - -QAbstractItemView -{ - alternate-background-color: #3A3939; - color: silver; - border: 1px solid 3A3939; - border-radius: 2px; - padding: 1px; -} - -QWidget:focus, QMenuBar:focus -{ - border: 1px solid #78879b; -} - -QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus -{ - border: none; -} - -QLineEdit -{ - background-color: #201F1F; - padding: 2px; - border-style: solid; - border: 1px solid #3A3939; - border-radius: 2px; - color: silver; -} - -QGroupBox { - border:1px solid #3A3939; - border-radius: 2px; - margin-top: 20px; -} - -QGroupBox::title { - subcontrol-origin: margin; - subcontrol-position: top center; - padding-left: 10px; - padding-right: 10px; - padding-top: 10px; -} - -QAbstractScrollArea -{ - border-radius: 2px; - border: 1px solid #3A3939; - background-color: transparent; -} - -QScrollBar:horizontal -{ - height: 15px; - margin: 3px 15px 3px 15px; - border: 1px transparent #2A2929; - border-radius: 4px; - background-color: #2A2929; -} - -QScrollBar::handle:horizontal -{ - background-color: #605F5F; - min-width: 5px; - border-radius: 4px; -} - -QScrollBar::add-line:horizontal -{ - margin: 0px 3px 0px 3px; - border-image: url(:/qss_icons/rc/right_arrow_disabled.png); - width: 10px; - height: 10px; - subcontrol-position: right; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:horizontal -{ - margin: 0px 3px 0px 3px; - border-image: url(:/qss_icons/rc/left_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on -{ - border-image: url(:/qss_icons/rc/right_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: right; - subcontrol-origin: margin; -} - - -QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on -{ - border-image: url(:/qss_icons/rc/left_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal -{ - background: none; -} - - -QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal -{ - background: none; -} - -QScrollBar:vertical -{ - background-color: #2A2929; - width: 15px; - margin: 15px 3px 15px 3px; - border: 1px transparent #2A2929; - border-radius: 4px; -} - -QScrollBar::handle:vertical -{ - background-color: #605F5F; - min-height: 5px; - border-radius: 4px; -} - -QScrollBar::sub-line:vertical -{ - margin: 3px 0px 3px 0px; - border-image: url(:/qss_icons/rc/up_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: top; - subcontrol-origin: margin; -} - -QScrollBar::add-line:vertical -{ - margin: 3px 0px 3px 0px; - border-image: url(:/qss_icons/rc/down_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on -{ - - border-image: url(:/qss_icons/rc/up_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: top; - subcontrol-origin: margin; -} - - -QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on -{ - border-image: url(:/qss_icons/rc/down_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical -{ - background: none; -} - - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical -{ - background: none; -} - -QTextEdit -{ - background-color: #201F1F; - color: silver; - border: 1px solid #3A3939; -} - -QPlainTextEdit -{ - background-color: #201F1F;; - color: silver; - border-radius: 2px; - border: 1px solid #3A3939; -} - -QHeaderView::section -{ - background-color: #3A3939; - color: silver; - padding-left: 4px; - border: 1px solid #6c6c6c; -} - -QSizeGrip { - image: url(:/qss_icons/rc/sizegrip.png); - width: 12px; - height: 12px; -} - - -QMainWindow::separator -{ - background-color: #302F2F; - color: white; - padding-left: 4px; - spacing: 2px; - border: 1px dashed #3A3939; -} - -QMainWindow::separator:hover -{ - - background-color: #787876; - color: white; - padding-left: 4px; - border: 1px solid #3A3939; - spacing: 2px; -} - - -QMenu::separator -{ - height: 1px; - background-color: #3A3939; - color: white; - padding-left: 4px; - margin-left: 10px; - margin-right: 5px; -} - - -QFrame -{ - border-radius: 2px; - border: 1px solid #444; -} - -QFrame[frameShape="0"] -{ - border-radius: 2px; - border: 1px transparent #444; -} - -QStackedWidget -{ - border: 1px transparent black; -} - -QToolBar { - border: 1px transparent #393838; - background: 1px solid #302F2F; - font-weight: bold; -} - -QToolBar::handle:horizontal { - image: url(:/qss_icons/rc/Hmovetoolbar.png); -} -QToolBar::handle:vertical { - image: url(:/qss_icons/rc/Vmovetoolbar.png); -} -QToolBar::separator:horizontal { - image: url(:/qss_icons/rc/Hsepartoolbar.png); -} -QToolBar::separator:vertical { - image: url(:/qss_icons/rc/Vsepartoolbars.png); -} - -QPushButton -{ - color: silver; - background-color: #302F2F; - border-width: 1px; - border-color: #4A4949; - border-style: solid; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 5px; - padding-right: 5px; - border-radius: 2px; - outline: none; -} - -QPushButton:focus -{ - border-width: 1px; - border-color: #4A4949; - border-style: solid; -} - -QPushButton:disabled -{ - background-color: #302F2F; - border-width: 1px; - border-color: #3A3939; - border-style: solid; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 10px; - padding-right: 10px; - /*border-radius: 2px;*/ - color: #454545; -} - -QComboBox -{ - selection-background-color: #A9A9A9; - background-color: #201F1F; - border-style: solid; - border: 1px solid #3A3939; - border-radius: 2px; - padding: 2px; - min-width: 75px; -} - -QPushButton:hover -{ - background-color: #3d8ec9; - color: white; -} - -QComboBox:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:hover,QTreeView:hover -{ - border: 1px solid #78879b; - color: silver; -} - -QComboBox:on -{ - background-color: #626873; - padding-top: 3px; - padding-left: 4px; - selection-background-color: #4a4a4a; -} - -QComboBox QAbstractItemView -{ - background-color: #201F1F; - border-radius: 2px; - border: 1px solid #444; - selection-background-color: #A9A9A9; -} - -QComboBox::drop-down -{ - subcontrol-origin: padding; - subcontrol-position: top right; - width: 15px; - - border-left-width: 0px; - border-left-color: darkgray; - border-left-style: solid; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} - -QComboBox::down-arrow -{ - image: url(:/qss_icons/rc/down_arrow_disabled.png); -} - -QComboBox::down-arrow:on, QComboBox::down-arrow:hover, -QComboBox::down-arrow:focus -{ - image: url(:/qss_icons/rc/down_arrow.png); -} - -QAbstractSpinBox { - padding-top: 2px; - padding-bottom: 2px; - border: 1px solid #3A3939; - background-color: #201F1F; - color: silver; - border-radius: 2px; - min-width: 75px; -} - -QAbstractSpinBox:up-button -{ - background-color: transparent; - subcontrol-origin: border; - subcontrol-position: center right; -} - -QAbstractSpinBox:down-button -{ - background-color: transparent; - subcontrol-origin: border; - subcontrol-position: center left; -} - -QAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off { - image: url(:/qss_icons/rc/up_arrow_disabled.png); - width: 10px; - height: 10px; -} -QAbstractSpinBox::up-arrow:hover -{ - image: url(:/qss_icons/rc/up_arrow.png); -} - - -QAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off -{ - image: url(:/qss_icons/rc/down_arrow_disabled.png); - width: 10px; - height: 10px; -} -QAbstractSpinBox::down-arrow:hover -{ - image: url(:/qss_icons/rc/down_arrow.png); -} - - -QLabel -{ - border: 0px solid black; - background-color: transparent; -} - -QTabWidget{ - border: 1px transparent black; -} - -QTabWidget::pane { - border: 1px solid #444; - border-radius: 3px; - padding: 3px; -} - -QTabBar -{ - qproperty-drawBase: 0; - left: 5px; /* move to the right by 5px */ -} - -QTabBar:focus -{ - border: 0px transparent black; -} - -QTabBar::close-button { - image: url(:/qss_icons/rc/close.png); - background: transparent; -} - -QTabBar::close-button:hover -{ - image: url(:/qss_icons/rc/close-hover.png); - background: transparent; -} - -QTabBar::close-button:pressed { - image: url(:/qss_icons/rc/close-pressed.png); - background: transparent; -} - -/* TOP TABS */ -QTabBar::tab:top { - color: #b1b1b1; - border: 1px solid #4A4949; - border-bottom: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-top-left-radius: 2px; - border-top-right-radius: 2px; -} - -QTabBar::tab:top:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-bottom: 1px transparent #4A4949; - border-top-left-radius: 0px; - border-top-right-radius: 0px; -} - -QTabBar::tab:top:!selected:hover { - background-color: #48576b; -} - -/* BOTTOM TABS */ -QTabBar::tab:bottom { - color: #b1b1b1; - border: 1px solid #4A4949; - border-top: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; -} - -QTabBar::tab:bottom:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-top: 1px transparent #4A4949; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; -} - -QTabBar::tab:bottom:!selected:hover { - background-color: #78879b; -} - -/* LEFT TABS */ -QTabBar::tab:left { - color: #b1b1b1; - border: 1px solid #4A4949; - border-left: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; -} - -QTabBar::tab:left:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-right: 1px transparent #4A4949; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; -} - -QTabBar::tab:left:!selected:hover { - background-color: #48576b; -} - - -/* RIGHT TABS */ -QTabBar::tab:right { - color: #b1b1b1; - border: 1px solid #4A4949; - border-right: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} - -QTabBar::tab:right:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-right: 1px transparent #4A4949; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; -} - -QTabBar::tab:right:!selected:hover { - background-color: #48576b; -} - -QTabBar QToolButton::right-arrow:enabled { - image: url(:/qss_icons/rc/right_arrow.png); - } - - QTabBar QToolButton::left-arrow:enabled { - image: url(:/qss_icons/rc/left_arrow.png); - } - -QTabBar QToolButton::right-arrow:disabled { - image: url(:/qss_icons/rc/right_arrow_disabled.png); - } - - QTabBar QToolButton::left-arrow:disabled { - image: url(:/qss_icons/rc/left_arrow_disabled.png); - } - - -QDockWidget { - border: 1px solid #403F3F; - titlebar-close-icon: url(:/qss_icons/rc/close.png); - titlebar-normal-icon: url(:/qss_icons/rc/undock.png); -} - -QDockWidget::close-button, QDockWidget::float-button { - border: 1px solid transparent; - border-radius: 2px; - background: transparent; -} - -QDockWidget::close-button:hover, QDockWidget::float-button:hover { - background: rgba(255, 255, 255, 10); -} - -QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { - padding: 1px -1px -1px 1px; - background: rgba(255, 255, 255, 10); -} - -QTreeView, QListView -{ - border: 1px solid #444; - background-color: #201F1F; -} - -QTreeView:branch:selected, QTreeView:branch:hover -{ - background: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:has-siblings:!adjoins-item { - border-image: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:has-siblings:adjoins-item { - border-image: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:!has-children:!has-siblings:adjoins-item { - border-image: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:has-children:!has-siblings:closed, -QTreeView::branch:closed:has-children:has-siblings { - image: url(:/qss_icons/rc/branch_closed.png); -} - -QTreeView::branch:open:has-children:!has-siblings, -QTreeView::branch:open:has-children:has-siblings { - image: url(:/qss_icons/rc/branch_open.png); -} - -QTreeView::branch:has-children:!has-siblings:closed:hover, -QTreeView::branch:closed:has-children:has-siblings:hover { - image: url(:/qss_icons/rc/branch_closed-on.png); - } - -QTreeView::branch:open:has-children:!has-siblings:hover, -QTreeView::branch:open:has-children:has-siblings:hover { - image: url(:/qss_icons/rc/branch_open-on.png); - } - -QListView::item:!selected:hover, QListView::item:!selected:hover, QTreeView::item:!selected:hover { - background: rgba(0, 0, 0, 0); - outline: 0; - color: #FFFFFF -} - -QListView::item:selected:hover, QListView::item:selected:hover, QTreeView::item:selected:hover { - background: #3d8ec9; - color: #FFFFFF; -} - -QSlider::groove:horizontal { - border: 1px solid #3A3939; - height: 8px; - background: #201F1F; - margin: 2px 0; - border-radius: 2px; -} - -QSlider::handle:horizontal { - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0.0 silver, stop: 0.2 #a8a8a8, stop: 1 #727272); - border: 1px solid #3A3939; - width: 14px; - height: 14px; - margin: -4px 0; - border-radius: 2px; -} - -QSlider::groove:vertical { - border: 1px solid #3A3939; - width: 8px; - background: #201F1F; - margin: 0 0px; - border-radius: 2px; -} - -QSlider::handle:vertical { - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 silver, - stop: 0.2 #a8a8a8, stop: 1 #727272); - border: 1px solid #3A3939; - width: 14px; - height: 14px; - margin: 0 -4px; - border-radius: 2px; -} - -QToolButton { - background-color: transparent; - border: 1px transparent #4A4949; - border-radius: 2px; - margin: 3px; - padding: 3px; -} - -QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ - padding-right: 20px; /* make way for the popup button */ - border: 1px transparent #4A4949; - border-radius: 5px; -} - -QToolButton[popupMode="2"] { /* only for InstantPopup */ - padding-right: 10px; /* make way for the popup button */ - border: 1px transparent #4A4949; -} - - -QToolButton:hover, QToolButton::menu-button:hover { - background-color: transparent; - border: 1px solid #78879b; -} - -QToolButton:checked, QToolButton:pressed, - QToolButton::menu-button:pressed { - background-color: #4A4949; - border: 1px solid #78879b; -} - -/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ -QToolButton::menu-indicator { - image: url(:/qss_icons/rc/down_arrow.png); - top: -7px; left: -2px; /* shift it a bit */ -} - -/* the subcontrols below are used only in the MenuButtonPopup mode */ -QToolButton::menu-button { - border: 1px transparent #4A4949; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - /* 16px width + 4px for border = 20px allocated above */ - width: 16px; - outline: none; -} - -QToolButton::menu-arrow { - image: url(:/qss_icons/rc/down_arrow.png); -} - -QToolButton::menu-arrow:open { - top: 1px; left: 1px; /* shift it a bit */ - border: 1px solid #3A3939; -} - -QTableView -{ - border: 1px solid #444; - gridline-color: #6c6c6c; - background-color: #201F1F; -} - - -QTableView, QHeaderView -{ - border-radius: 0px; -} - -QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { - background: #78879b; - color: #FFFFFF; -} - -QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { - background: #3d8ec9; - color: #FFFFFF; -} - - -QHeaderView -{ - border: 1px transparent; - border-radius: 2px; - margin: 0px; - padding: 0px; -} - -QHeaderView::section { - background-color: #3A3939; - color: silver; - padding: 4px; - border: 1px solid #6c6c6c; - border-radius: 0px; - text-align: center; -} - -QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one -{ - border-top: 1px solid #6c6c6c; -} - -QHeaderView::section::vertical -{ - border-top: transparent; -} - -QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one -{ - border-left: 1px solid #6c6c6c; -} - -QHeaderView::section::horizontal -{ - border-left: transparent; -} - - -QHeaderView::section:checked - { - color: white; - background-color: #5A5959; - } - - /* style the sort indicator */ -QHeaderView::down-arrow { - image: url(:/qss_icons/rc/down_arrow.png); -} - -QHeaderView::up-arrow { - image: url(:/qss_icons/rc/up_arrow.png); -} - - -QTableCornerButton::section { - background-color: #3A3939; - border: 1px solid #3A3939; - border-radius: 2px; -} - -QToolBox { - padding: 3px; - border: 1px transparent black; -} - -QToolBox::tab { - color: #b1b1b1; - background-color: #302F2F; - border: 1px solid #4A4949; - border-bottom: 1px transparent #302F2F; - border-top-left-radius: 5px; - border-top-right-radius: 5px; -} - - QToolBox::tab:selected { /* italicize selected tabs */ - font: italic; - background-color: #302F2F; - border-color: #3d8ec9; - } - -QStatusBar::item { - border: 1px solid #3A3939; - border-radius: 2px; - } - - -QFrame[height="3"], QFrame[width="3"] { - background-color: #444; -} - - -QSplitter::handle { - border: 1px dashed #3A3939; -} - -QSplitter::handle:hover { - background-color: #787876; - border: 1px solid #3A3939; -} - -QSplitter::handle:horizontal { - width: 1px; -} - -QSplitter::handle:vertical { - height: 1px; -} - -MessageItem -{ - border: none; -} - -MessageBrowser -{ - border: none; -} - -MessageBrowser::focus -{ - border: none; -} - -MessageItem::focus -{ - border: none; -} - -MessageBrowser:hover -{ - border: none; -} - -QListWidget QPushButton -{ - background-color: transparent; - border: none; -} - -QPushButton:hover -{ - background-color: #4A4949; -} - -#messages:item:selected -{ - background-color: #1E90FF; -} - -MessageBrowser -{ - background-color: transparent; -} - -#messages:item:selected QListWidgetItem -{ - background-color: #1E90FF; -} - -#friendsListWidget:item:selected -{ - background-color: #333333; -} - -#toxygen -{ - color: #A9A9A9; -} - -QCheckBox -{ - spacing: 5px; - outline: none; - color: #bbb; - margin-bottom: 2px; - text-align: center; -} - -QListWidget > QLabel -{ - color: #A9A9A9; -} - -#searchLineEdit -{ - padding-left: 22px; -} - -#mainmenubutton -{ - border: 1px solid #3A3939; - color: silver; - margin: 0px; - text-align: center; -} - -#mainmenubutton:hover -{ - background: transparent; - border: 1px solid #A9A9A9; - background-color: #302F2F; -} - -#mainmenubutton:pressed -{ - background: transparent; - border: 1px solid #A9A9A9; - background-color: #302F2F; -} - -#mainmenubutton::menu-indicator -{ - image: none; - width: 0px; - height: 0px; -} - -ClickableLabel:focus -{ - border-width: 1px; - border-color: #4A4949; - border-style: solid; -} - -ClickableLabel:hover -{ - background-color: #4A4949; -} - -#warningLabel -{ - color: #BC1C1C; -} - -#groupInvitesPushButton -{ - background-color: #009c00; -} - diff --git a/toxygen/styles/rc/Hmovetoolbar.png b/toxygen/styles/rc/Hmovetoolbar.png old mode 100644 new mode 100755 index 4b55192..cead99e Binary files a/toxygen/styles/rc/Hmovetoolbar.png and b/toxygen/styles/rc/Hmovetoolbar.png differ diff --git a/toxygen/styles/rc/Hsepartoolbar.png b/toxygen/styles/rc/Hsepartoolbar.png old mode 100644 new mode 100755 index 58840be..7f183c8 Binary files a/toxygen/styles/rc/Hsepartoolbar.png and b/toxygen/styles/rc/Hsepartoolbar.png differ diff --git a/toxygen/styles/rc/Vmovetoolbar.png b/toxygen/styles/rc/Vmovetoolbar.png old mode 100644 new mode 100755 index c3b4762..512edce Binary files a/toxygen/styles/rc/Vmovetoolbar.png and b/toxygen/styles/rc/Vmovetoolbar.png differ diff --git a/toxygen/styles/rc/Vsepartoolbar.png b/toxygen/styles/rc/Vsepartoolbar.png old mode 100644 new mode 100755 index 5de9a34..d9dc156 Binary files a/toxygen/styles/rc/Vsepartoolbar.png and b/toxygen/styles/rc/Vsepartoolbar.png differ diff --git a/toxygen/styles/rc/branch_closed-on.png b/toxygen/styles/rc/branch_closed-on.png old mode 100644 new mode 100755 index 9020fe7..d081e9b Binary files a/toxygen/styles/rc/branch_closed-on.png and b/toxygen/styles/rc/branch_closed-on.png differ diff --git a/toxygen/styles/rc/branch_closed.png b/toxygen/styles/rc/branch_closed.png old mode 100644 new mode 100755 index 7c20500..d652159 Binary files a/toxygen/styles/rc/branch_closed.png and b/toxygen/styles/rc/branch_closed.png differ diff --git a/toxygen/styles/rc/branch_open-on.png b/toxygen/styles/rc/branch_open-on.png old mode 100644 new mode 100755 index f41f80c..ec372b2 Binary files a/toxygen/styles/rc/branch_open-on.png and b/toxygen/styles/rc/branch_open-on.png differ diff --git a/toxygen/styles/rc/branch_open.png b/toxygen/styles/rc/branch_open.png old mode 100644 new mode 100755 index efb6068..66f8e1a Binary files a/toxygen/styles/rc/branch_open.png and b/toxygen/styles/rc/branch_open.png differ diff --git a/toxygen/styles/rc/checkbox_checked.png b/toxygen/styles/rc/checkbox_checked.png old mode 100644 new mode 100755 index 1539bc9..e09ce02 Binary files a/toxygen/styles/rc/checkbox_checked.png and b/toxygen/styles/rc/checkbox_checked.png differ diff --git a/toxygen/styles/rc/checkbox_checked_disabled.png b/toxygen/styles/rc/checkbox_checked_disabled.png old mode 100644 new mode 100755 index 1539bc9..e09ce02 Binary files a/toxygen/styles/rc/checkbox_checked_disabled.png and b/toxygen/styles/rc/checkbox_checked_disabled.png differ diff --git a/toxygen/styles/rc/checkbox_checked_focus.png b/toxygen/styles/rc/checkbox_checked_focus.png old mode 100644 new mode 100755 index 1539bc9..e09ce02 Binary files a/toxygen/styles/rc/checkbox_checked_focus.png and b/toxygen/styles/rc/checkbox_checked_focus.png differ diff --git a/toxygen/styles/rc/checkbox_indeterminate.png b/toxygen/styles/rc/checkbox_indeterminate.png old mode 100644 new mode 100755 index 15e221b..41024f7 Binary files a/toxygen/styles/rc/checkbox_indeterminate.png and b/toxygen/styles/rc/checkbox_indeterminate.png differ diff --git a/toxygen/styles/rc/checkbox_indeterminate_disabled.png b/toxygen/styles/rc/checkbox_indeterminate_disabled.png old mode 100644 new mode 100755 index bc26933..abdc01d Binary files a/toxygen/styles/rc/checkbox_indeterminate_disabled.png and b/toxygen/styles/rc/checkbox_indeterminate_disabled.png differ diff --git a/toxygen/styles/rc/checkbox_indeterminate_focus.png b/toxygen/styles/rc/checkbox_indeterminate_focus.png old mode 100644 new mode 100755 index 7c00620..a9a16f7 Binary files a/toxygen/styles/rc/checkbox_indeterminate_focus.png and b/toxygen/styles/rc/checkbox_indeterminate_focus.png differ diff --git a/toxygen/styles/rc/checkbox_unchecked.png b/toxygen/styles/rc/checkbox_unchecked.png old mode 100644 new mode 100755 index 30631ba..30deeb5 Binary files a/toxygen/styles/rc/checkbox_unchecked.png and b/toxygen/styles/rc/checkbox_unchecked.png differ diff --git a/toxygen/styles/rc/checkbox_unchecked_disabled.png b/toxygen/styles/rc/checkbox_unchecked_disabled.png old mode 100644 new mode 100755 index 30631ba..30deeb5 Binary files a/toxygen/styles/rc/checkbox_unchecked_disabled.png and b/toxygen/styles/rc/checkbox_unchecked_disabled.png differ diff --git a/toxygen/styles/rc/checkbox_unchecked_focus.png b/toxygen/styles/rc/checkbox_unchecked_focus.png old mode 100644 new mode 100755 index 30631ba..30deeb5 Binary files a/toxygen/styles/rc/checkbox_unchecked_focus.png and b/toxygen/styles/rc/checkbox_unchecked_focus.png differ diff --git a/toxygen/styles/rc/close-hover.png b/toxygen/styles/rc/close-hover.png old mode 100644 new mode 100755 index f8fbb31..657943a Binary files a/toxygen/styles/rc/close-hover.png and b/toxygen/styles/rc/close-hover.png differ diff --git a/toxygen/styles/rc/close-pressed.png b/toxygen/styles/rc/close-pressed.png old mode 100644 new mode 100755 index 7c644b6..937d005 Binary files a/toxygen/styles/rc/close-pressed.png and b/toxygen/styles/rc/close-pressed.png differ diff --git a/toxygen/styles/rc/close.png b/toxygen/styles/rc/close.png old mode 100644 new mode 100755 index b3e51a0..bc0f576 Binary files a/toxygen/styles/rc/close.png and b/toxygen/styles/rc/close.png differ diff --git a/toxygen/styles/rc/down_arrow.png b/toxygen/styles/rc/down_arrow.png old mode 100644 new mode 100755 index ff4a62b..e271f7f Binary files a/toxygen/styles/rc/down_arrow.png and b/toxygen/styles/rc/down_arrow.png differ diff --git a/toxygen/styles/rc/down_arrow_disabled.png b/toxygen/styles/rc/down_arrow_disabled.png old mode 100644 new mode 100755 index 388339c..5805d98 Binary files a/toxygen/styles/rc/down_arrow_disabled.png and b/toxygen/styles/rc/down_arrow_disabled.png differ diff --git a/toxygen/styles/rc/left_arrow.png b/toxygen/styles/rc/left_arrow.png old mode 100644 new mode 100755 index f0c00ea..f808d2d Binary files a/toxygen/styles/rc/left_arrow.png and b/toxygen/styles/rc/left_arrow.png differ diff --git a/toxygen/styles/rc/left_arrow_disabled.png b/toxygen/styles/rc/left_arrow_disabled.png old mode 100644 new mode 100755 index 570a940..f5b9af8 Binary files a/toxygen/styles/rc/left_arrow_disabled.png and b/toxygen/styles/rc/left_arrow_disabled.png differ diff --git a/toxygen/styles/rc/radio_checked.png b/toxygen/styles/rc/radio_checked.png old mode 100644 new mode 100755 index c6aacda..14b1cb1 Binary files a/toxygen/styles/rc/radio_checked.png and b/toxygen/styles/rc/radio_checked.png differ diff --git a/toxygen/styles/rc/radio_checked_disabled.png b/toxygen/styles/rc/radio_checked_disabled.png old mode 100644 new mode 100755 index c6aacda..14b1cb1 Binary files a/toxygen/styles/rc/radio_checked_disabled.png and b/toxygen/styles/rc/radio_checked_disabled.png differ diff --git a/toxygen/styles/rc/radio_checked_focus.png b/toxygen/styles/rc/radio_checked_focus.png old mode 100644 new mode 100755 index c6aacda..14b1cb1 Binary files a/toxygen/styles/rc/radio_checked_focus.png and b/toxygen/styles/rc/radio_checked_focus.png differ diff --git a/toxygen/styles/rc/radio_unchecked.png b/toxygen/styles/rc/radio_unchecked.png old mode 100644 new mode 100755 index c0565b5..27af811 Binary files a/toxygen/styles/rc/radio_unchecked.png and b/toxygen/styles/rc/radio_unchecked.png differ diff --git a/toxygen/styles/rc/radio_unchecked_disabled.png b/toxygen/styles/rc/radio_unchecked_disabled.png old mode 100644 new mode 100755 index c0565b5..27af811 Binary files a/toxygen/styles/rc/radio_unchecked_disabled.png and b/toxygen/styles/rc/radio_unchecked_disabled.png differ diff --git a/toxygen/styles/rc/radio_unchecked_focus.png b/toxygen/styles/rc/radio_unchecked_focus.png old mode 100644 new mode 100755 index c0565b5..27af811 Binary files a/toxygen/styles/rc/radio_unchecked_focus.png and b/toxygen/styles/rc/radio_unchecked_focus.png differ diff --git a/toxygen/styles/rc/right_arrow.png b/toxygen/styles/rc/right_arrow.png old mode 100644 new mode 100755 index 75e5b5a..9b0a4e6 Binary files a/toxygen/styles/rc/right_arrow.png and b/toxygen/styles/rc/right_arrow.png differ diff --git a/toxygen/styles/rc/right_arrow_disabled.png b/toxygen/styles/rc/right_arrow_disabled.png old mode 100644 new mode 100755 index 31f4831..5c0bee4 Binary files a/toxygen/styles/rc/right_arrow_disabled.png and b/toxygen/styles/rc/right_arrow_disabled.png differ diff --git a/toxygen/styles/rc/sizegrip.png b/toxygen/styles/rc/sizegrip.png old mode 100644 new mode 100755 index 09473be..350583a Binary files a/toxygen/styles/rc/sizegrip.png and b/toxygen/styles/rc/sizegrip.png differ diff --git a/toxygen/styles/rc/stylesheet-branch-end.png b/toxygen/styles/rc/stylesheet-branch-end.png old mode 100644 new mode 100755 index 5569ee6..cb5d3b5 Binary files a/toxygen/styles/rc/stylesheet-branch-end.png and b/toxygen/styles/rc/stylesheet-branch-end.png differ diff --git a/toxygen/styles/rc/stylesheet-branch-more.png b/toxygen/styles/rc/stylesheet-branch-more.png old mode 100644 new mode 100755 index 57fe30d..6271140 Binary files a/toxygen/styles/rc/stylesheet-branch-more.png and b/toxygen/styles/rc/stylesheet-branch-more.png differ diff --git a/toxygen/styles/rc/stylesheet-vline.png b/toxygen/styles/rc/stylesheet-vline.png old mode 100644 new mode 100755 index 253cacb..87536cc Binary files a/toxygen/styles/rc/stylesheet-vline.png and b/toxygen/styles/rc/stylesheet-vline.png differ diff --git a/toxygen/styles/rc/transparent.png b/toxygen/styles/rc/transparent.png old mode 100644 new mode 100755 index cf1c4f6..483df25 Binary files a/toxygen/styles/rc/transparent.png and b/toxygen/styles/rc/transparent.png differ diff --git a/toxygen/styles/rc/undock.png b/toxygen/styles/rc/undock.png old mode 100644 new mode 100755 index 4a7b0c8..88691d7 Binary files a/toxygen/styles/rc/undock.png and b/toxygen/styles/rc/undock.png differ diff --git a/toxygen/styles/rc/up_arrow.png b/toxygen/styles/rc/up_arrow.png old mode 100644 new mode 100755 index 0cc7d6d..abcc724 Binary files a/toxygen/styles/rc/up_arrow.png and b/toxygen/styles/rc/up_arrow.png differ diff --git a/toxygen/styles/rc/up_arrow_disabled.png b/toxygen/styles/rc/up_arrow_disabled.png old mode 100644 new mode 100755 index 99c6b67..b9c8e3b Binary files a/toxygen/styles/rc/up_arrow_disabled.png and b/toxygen/styles/rc/up_arrow_disabled.png differ diff --git a/toxygen/styles/style.py b/toxygen/styles/style.py index dca54d8..61352b0 100644 --- a/toxygen/styles/style.py +++ b/toxygen/styles/style.py @@ -1,14 +1,17 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +# -*- coding: utf-8 -*- -from qtpy import QtCore +try: + from PySide import QtCore +except ImportError: + from PyQt4 import QtCore qt_resource_data = b"\x00\x00d\xdc/*\x0a * The MIT License (MIT)\x0a *\x0a * Copyright (c) <2013-2014> \x0a *\x0a * Permission is hereby granted, free of charge, to any person obtaining a copy\x0a * of this software and associated documentation files (the \x22Software\x22), to deal\x0a * in the Software without restriction, including without limitation the rights\x0a * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\x0a * copies of the Software, and to permit persons to whom the Software is\x0a * furnished to do so, subject to the following conditions:\x0a\x0a * The above copyright notice and this permission notice shall be included in\x0a * all copies or substantial portions of the Software.\x0a\x0a * THE SOFTWARE IS PROVIDED \x22AS IS\x22, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\x0a * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\x0a * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\x0a * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\x0a * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\x0a * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\x0a * THE SOFTWARE.\x0a */\x0a\x0aQToolTip\x0a{\x0a border: 1px solid #3A3939;\x0a background-color: rgb(90, 102, 117);;\x0a color: white;\x0a padding: 1px;\x0a opacity: 200;\x0a}\x0a\x0aQWidget\x0a{\x0a color: silver;\x0a background-color: #302F2F;\x0a selection-background-color: #A9A9A9;\x0a selection-color: black;\x0a background-clip: border;\x0a border-image: none;\x0a outline: 0;\x0a}\x0a\x0aQWidget:item:hover\x0a{\x0a background-color: #78879b;\x0a color: black;\x0a}\x0a\x0aQWidget:item:selected\x0a{\x0a background-color: #A9A9A9;\x0a}\x0a\x0aQProgressBar:horizontal {\x0a border: 1px solid #3A3939;\x0a text-align: center;\x0a padding: 1px;\x0a background: #201F1F;\x0a}\x0aQProgressBar::chunk:horizontal {\x0a background-color: qlineargradient(spread:reflect, x1:1, y1:0.545, x2:1, y2:0, stop:0 rgba(28, 66, 111, 255), stop:1 rgba(37, 87, 146, 255));\x0a}\x0a\x0aQCheckBox:disabled\x0a{\x0a color: #777777;\x0a}\x0aQCheckBox::indicator,\x0aQGroupBox::indicator\x0a{\x0a width: 18px;\x0a height: 18px;\x0a}\x0aQGroupBox::indicator\x0a{\x0a margin-left: 2px;\x0a}\x0a\x0aQCheckBox::indicator:unchecked,\x0aQCheckBox::indicator:unchecked:hover,\x0aQGroupBox::indicator:unchecked,\x0aQGroupBox::indicator:unchecked:hover\x0a{\x0a image: url(:/qss_icons/rc/checkbox_unchecked.png);\x0a}\x0a\x0aQCheckBox::indicator:unchecked:focus,\x0aQCheckBox::indicator:unchecked:pressed,\x0aQGroupBox::indicator:unchecked:focus,\x0aQGroupBox::indicator:unchecked:pressed\x0a{\x0a border: none;\x0a image: url(:/qss_icons/rc/checkbox_unchecked_focus.png);\x0a}\x0a\x0aQCheckBox::indicator:checked,\x0aQCheckBox::indicator:checked:hover,\x0aQGroupBox::indicator:checked,\x0aQGroupBox::indicator:checked:hover\x0a{\x0a image: url(:/qss_icons/rc/checkbox_checked.png);\x0a}\x0a\x0aQCheckBox::indicator:checked:focus,\x0aQCheckBox::indicator:checked:pressed,\x0aQGroupBox::indicator:checked:focus,\x0aQGroupBox::indicator:checked:pressed\x0a{\x0a border: none;\x0a image: url(:/qss_icons/rc/checkbox_checked_focus.png);\x0a}\x0a\x0aQCheckBox::indicator:indeterminate,\x0aQCheckBox::indicator:indeterminate:hover,\x0aQCheckBox::indicator:indeterminate:pressed\x0aQGroupBox::indicator:indeterminate,\x0aQGroupBox::indicator:indeterminate:hover,\x0aQGroupBox::indicator:indeterminate:pressed\x0a{\x0a image: url(:/qss_icons/rc/checkbox_indeterminate.png);\x0a}\x0a\x0aQCheckBox::indicator:indeterminate:focus,\x0aQGroupBox::indicator:indeterminate:focus\x0a{\x0a image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png);\x0a}\x0a\x0aQCheckBox::indicator:checked:disabled,\x0aQGroupBox::indicator:checked:disabled\x0a{\x0a image: url(:/qss_icons/rc/checkbox_checked_disabled.png);\x0a}\x0a\x0aQCheckBox::indicator:unchecked:disabled,\x0aQGroupBox::indicator:unchecked:disabled\x0a{\x0a image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png);\x0a}\x0a\x0aQRadioButton\x0a{\x0a spacing: 5px;\x0a outline: none;\x0a color: #bbb;\x0a margin-bottom: 2px;\x0a}\x0a\x0aQRadioButton:disabled\x0a{\x0a color: #777777;\x0a}\x0aQRadioButton::indicator\x0a{\x0a width: 21px;\x0a height: 21px;\x0a}\x0a\x0aQRadioButton::indicator:unchecked,\x0aQRadioButton::indicator:unchecked:hover\x0a{\x0a image: url(:/qss_icons/rc/radio_unchecked.png);\x0a}\x0a\x0aQRadioButton::indicator:unchecked:focus,\x0aQRadioButton::indicator:unchecked:pressed\x0a{\x0a border: none;\x0a outline: none;\x0a image: url(:/qss_icons/rc/radio_unchecked_focus.png);\x0a}\x0a\x0aQRadioButton::indicator:checked,\x0aQRadioButton::indicator:checked:hover\x0a{\x0a border: none;\x0a outline: none;\x0a image: url(:/qss_icons/rc/radio_checked.png);\x0a}\x0a\x0aQRadioButton::indicator:checked:focus,\x0aQRadioButton::indicato::menu-arrowr:checked:pressed\x0a{\x0a border: none;\x0a outline: none;\x0a image: url(:/qss_icons/rc/radio_checked_focus.png);\x0a}\x0a\x0aQRadioButton::indicator:indeterminate,\x0aQRadioButton::indicator:indeterminate:hover,\x0aQRadioButton::indicator:indeterminate:pressed\x0a{\x0a image: url(:/qss_icons/rc/radio_indeterminate.png);\x0a}\x0a\x0aQRadioButton::indicator:checked:disabled\x0a{\x0a outline: none;\x0a image: url(:/qss_icons/rc/radio_checked_disabled.png);\x0a}\x0a\x0aQRadioButton::indicator:unchecked:disabled\x0a{\x0a image: url(:/qss_icons/rc/radio_unchecked_disabled.png);\x0a}\x0a\x0a\x0aQMenuBar\x0a{\x0a background-color: #302F2F;\x0a color: silver;\x0a}\x0a\x0aQMenuBar::item\x0a{\x0a background: transparent;\x0a}\x0a\x0aQMenuBar::item:selected\x0a{\x0a background: transparent;\x0a border: 1px solid #A9A9A9;\x0a}\x0a\x0aQMenuBar::item:pressed\x0a{\x0a border: 1px solid #3A3939;\x0a background-color: #A9A9A9;\x0a color: black;\x0a margin-bottom:-1px;\x0a padding-bottom:1px;\x0a}\x0a\x0aQMenu\x0a{\x0a border: 1px solid #3A3939;\x0a color: silver;\x0a margin: 2px;\x0a}\x0a\x0aQMenu::icon\x0a{\x0a margin: 5px;\x0a}\x0a\x0aQMenu::item\x0a{\x0a padding: 5px 30px 5px 30px;\x0a margin-left: 5px;\x0a border: 1px solid transparent; /* reserve space for selection border */\x0a}\x0a\x0aQMenu::item:selected\x0a{\x0a color: black;\x0a}\x0a\x0aQMenu::separator {\x0a height: 2px;\x0a background: lightblue;\x0a margin-left: 10px;\x0a margin-right: 5px;\x0a}\x0a\x0aQMenu::indicator {\x0a width: 18px;\x0a height: 18px;\x0a}\x0a\x0a/* non-exclusive indicator = check box style indicator\x0a (see QActionGroup::setExclusive) */\x0aQMenu::indicator:non-exclusive:unchecked {\x0a image: url(:/qss_icons/rc/checkbox_unchecked.png);\x0a}\x0a\x0aQMenu::indicator:non-exclusive:unchecked:selected {\x0a image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png);\x0a}\x0a\x0aQMenu::indicator:non-exclusive:checked {\x0a image: url(:/qss_icons/rc/checkbox_checked.png);\x0a}\x0a\x0aQMenu::indicator:non-exclusive:checked:selected {\x0a image: url(:/qss_icons/rc/checkbox_checked_disabled.png);\x0a}\x0a\x0a/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */\x0aQMenu::indicator:exclusive:unchecked {\x0a image: url(:/qss_icons/rc/radio_unchecked.png);\x0a}\x0a\x0aQMenu::indicator:exclusive:unchecked:selected {\x0a image: url(:/qss_icons/rc/radio_unchecked_disabled.png);\x0a}\x0a\x0aQMenu::indicator:exclusive:checked {\x0a image: url(:/qss_icons/rc/radio_checked.png);\x0a}\x0a\x0aQMenu::indicator:exclusive:checked:selected {\x0a image: url(:/qss_icons/rc/radio_checked_disabled.png);\x0a}\x0a\x0aQMenu::right-arrow {\x0a margin: 5px;\x0a image: url(:/qss_icons/rc/right_arrow.png)\x0a}\x0a\x0a\x0aQWidget:disabled\x0a{\x0a color: #404040;\x0a background-color: #302F2F;\x0a}\x0a\x0aQAbstractItemView\x0a{\x0a alternate-background-color: #3A3939;\x0a color: silver;\x0a border: 1px solid 3A3939;\x0a border-radius: 2px;\x0a padding: 1px;\x0a}\x0a\x0aQWidget:focus, QMenuBar:focus\x0a{\x0a border: 1px solid #78879b;\x0a}\x0a\x0aQTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus\x0a{\x0a border: none;\x0a}\x0a\x0aQLineEdit\x0a{\x0a background-color: #201F1F;\x0a padding: 2px;\x0a border-style: solid;\x0a border: 1px solid #3A3939;\x0a border-radius: 2px;\x0a color: silver;\x0a}\x0a\x0aQGroupBox {\x0a border:1px solid #3A3939;\x0a border-radius: 2px;\x0a margin-top: 20px;\x0a}\x0a\x0aQGroupBox::title {\x0a subcontrol-origin: margin;\x0a subcontrol-position: top center;\x0a padding-left: 10px;\x0a padding-right: 10px;\x0a padding-top: 10px;\x0a}\x0a\x0aQAbstractScrollArea\x0a{\x0a border-radius: 2px;\x0a border: 1px solid #3A3939;\x0a background-color: transparent;\x0a}\x0a\x0aQScrollBar:horizontal\x0a{\x0a height: 15px;\x0a margin: 3px 15px 3px 15px;\x0a border: 1px transparent #2A2929;\x0a border-radius: 4px;\x0a background-color: #2A2929;\x0a}\x0a\x0aQScrollBar::handle:horizontal\x0a{\x0a background-color: #605F5F;\x0a min-width: 5px;\x0a border-radius: 4px;\x0a}\x0a\x0aQScrollBar::add-line:horizontal\x0a{\x0a margin: 0px 3px 0px 3px;\x0a border-image: url(:/qss_icons/rc/right_arrow_disabled.png);\x0a width: 10px;\x0a height: 10px;\x0a subcontrol-position: right;\x0a subcontrol-origin: margin;\x0a}\x0a\x0aQScrollBar::sub-line:horizontal\x0a{\x0a margin: 0px 3px 0px 3px;\x0a border-image: url(:/qss_icons/rc/left_arrow_disabled.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: left;\x0a subcontrol-origin: margin;\x0a}\x0a\x0aQScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on\x0a{\x0a border-image: url(:/qss_icons/rc/right_arrow.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: right;\x0a subcontrol-origin: margin;\x0a}\x0a\x0a\x0aQScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on\x0a{\x0a border-image: url(:/qss_icons/rc/left_arrow.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: left;\x0a subcontrol-origin: margin;\x0a}\x0a\x0aQScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal\x0a{\x0a background: none;\x0a}\x0a\x0a\x0aQScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal\x0a{\x0a background: none;\x0a}\x0a\x0aQScrollBar:vertical\x0a{\x0a background-color: #2A2929;\x0a width: 15px;\x0a margin: 15px 3px 15px 3px;\x0a border: 1px transparent #2A2929;\x0a border-radius: 4px;\x0a}\x0a\x0aQScrollBar::handle:vertical\x0a{\x0a background-color: #605F5F;\x0a min-height: 5px;\x0a border-radius: 4px;\x0a}\x0a\x0aQScrollBar::sub-line:vertical\x0a{\x0a margin: 3px 0px 3px 0px;\x0a border-image: url(:/qss_icons/rc/up_arrow_disabled.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: top;\x0a subcontrol-origin: margin;\x0a}\x0a\x0aQScrollBar::add-line:vertical\x0a{\x0a margin: 3px 0px 3px 0px;\x0a border-image: url(:/qss_icons/rc/down_arrow_disabled.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: bottom;\x0a subcontrol-origin: margin;\x0a}\x0a\x0aQScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on\x0a{\x0a\x0a border-image: url(:/qss_icons/rc/up_arrow.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: top;\x0a subcontrol-origin: margin;\x0a}\x0a\x0a\x0aQScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on\x0a{\x0a border-image: url(:/qss_icons/rc/down_arrow.png);\x0a height: 10px;\x0a width: 10px;\x0a subcontrol-position: bottom;\x0a subcontrol-origin: margin;\x0a}\x0a\x0aQScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical\x0a{\x0a background: none;\x0a}\x0a\x0a\x0aQScrollBar::add-page:vertical, QScrollBar::sub-page:vertical\x0a{\x0a background: none;\x0a}\x0a\x0aQTextEdit\x0a{\x0a background-color: #201F1F;\x0a color: silver;\x0a border: 1px solid #3A3939;\x0a}\x0a\x0aQPlainTextEdit\x0a{\x0a background-color: #201F1F;;\x0a color: silver;\x0a border-radius: 2px;\x0a border: 1px solid #3A3939;\x0a}\x0a\x0aQHeaderView::section\x0a{\x0a background-color: #3A3939;\x0a color: silver;\x0a padding-left: 4px;\x0a border: 1px solid #6c6c6c;\x0a}\x0a\x0aQSizeGrip {\x0a image: url(:/qss_icons/rc/sizegrip.png);\x0a width: 12px;\x0a height: 12px;\x0a}\x0a\x0a\x0aQMainWindow::separator\x0a{\x0a background-color: #302F2F;\x0a color: white;\x0a padding-left: 4px;\x0a spacing: 2px;\x0a border: 1px dashed #3A3939;\x0a}\x0a\x0aQMainWindow::separator:hover\x0a{\x0a\x0a background-color: #787876;\x0a color: white;\x0a padding-left: 4px;\x0a border: 1px solid #3A3939;\x0a spacing: 2px;\x0a}\x0a\x0a\x0aQMenu::separator\x0a{\x0a height: 1px;\x0a background-color: #3A3939;\x0a color: white;\x0a padding-left: 4px;\x0a margin-left: 10px;\x0a margin-right: 5px;\x0a}\x0a\x0a\x0aQFrame\x0a{\x0a border-radius: 2px;\x0a border: 1px solid #444;\x0a}\x0a\x0aQFrame[frameShape=\x220\x22]\x0a{\x0a border-radius: 2px;\x0a border: 1px transparent #444;\x0a}\x0a\x0aQStackedWidget\x0a{\x0a border: 1px transparent black;\x0a}\x0a\x0aQToolBar {\x0a border: 1px transparent #393838;\x0a background: 1px solid #302F2F;\x0a font-weight: bold;\x0a}\x0a\x0aQToolBar::handle:horizontal {\x0a image: url(:/qss_icons/rc/Hmovetoolbar.png);\x0a}\x0aQToolBar::handle:vertical {\x0a image: url(:/qss_icons/rc/Vmovetoolbar.png);\x0a}\x0aQToolBar::separator:horizontal {\x0a image: url(:/qss_icons/rc/Hsepartoolbar.png);\x0a}\x0aQToolBar::separator:vertical {\x0a image: url(:/qss_icons/rc/Vsepartoolbars.png);\x0a}\x0a\x0aQPushButton\x0a{\x0a color: silver;\x0a background-color: #302F2F;\x0a border-width: 1px;\x0a border-color: #4A4949;\x0a border-style: solid;\x0a padding-top: 5px;\x0a padding-bottom: 5px;\x0a padding-left: 5px;\x0a padding-right: 5px;\x0a border-radius: 2px;\x0a outline: none;\x0a}\x0a\x0aQPushButton:focus\x0a{\x0a border-width: 1px;\x0a border-color: #4A4949;\x0a border-style: solid;\x0a}\x0a\x0aQPushButton:disabled\x0a{\x0a background-color: #302F2F;\x0a border-width: 1px;\x0a border-color: #3A3939;\x0a border-style: solid;\x0a padding-top: 5px;\x0a padding-bottom: 5px;\x0a padding-left: 10px;\x0a padding-right: 10px;\x0a /*border-radius: 2px;*/\x0a color: #454545;\x0a}\x0a\x0aQComboBox\x0a{\x0a selection-background-color: #A9A9A9;\x0a background-color: #201F1F;\x0a border-style: solid;\x0a border: 1px solid #3A3939;\x0a border-radius: 2px;\x0a padding: 2px;\x0a min-width: 75px;\x0a}\x0a\x0aQPushButton:hover\x0a{\x0a background-color: #3d8ec9;\x0a color: white;\x0a}\x0a\x0aQComboBox:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:hover,QTreeView:hover\x0a{\x0a border: 1px solid #78879b;\x0a color: silver;\x0a}\x0a\x0aQComboBox:on\x0a{\x0a background-color: #626873;\x0a padding-top: 3px;\x0a padding-left: 4px;\x0a selection-background-color: #4a4a4a;\x0a}\x0a\x0aQComboBox QAbstractItemView\x0a{\x0a background-color: #201F1F;\x0a border-radius: 2px;\x0a border: 1px solid #444;\x0a selection-background-color: #A9A9A9;\x0a}\x0a\x0aQComboBox::drop-down\x0a{\x0a subcontrol-origin: padding;\x0a subcontrol-position: top right;\x0a width: 15px;\x0a\x0a border-left-width: 0px;\x0a border-left-color: darkgray;\x0a border-left-style: solid;\x0a border-top-right-radius: 3px;\x0a border-bottom-right-radius: 3px;\x0a}\x0a\x0aQComboBox::down-arrow\x0a{\x0a image: url(:/qss_icons/rc/down_arrow_disabled.png);\x0a}\x0a\x0aQComboBox::down-arrow:on, QComboBox::down-arrow:hover,\x0aQComboBox::down-arrow:focus\x0a{\x0a image: url(:/qss_icons/rc/down_arrow.png);\x0a}\x0a\x0aQAbstractSpinBox {\x0a padding-top: 2px;\x0a padding-bottom: 2px;\x0a border: 1px solid #3A3939;\x0a background-color: #201F1F;\x0a color: silver;\x0a border-radius: 2px;\x0a min-width: 75px;\x0a}\x0a\x0aQAbstractSpinBox:up-button\x0a{\x0a background-color: transparent;\x0a subcontrol-origin: border;\x0a subcontrol-position: center right;\x0a}\x0a\x0aQAbstractSpinBox:down-button\x0a{\x0a background-color: transparent;\x0a subcontrol-origin: border;\x0a subcontrol-position: center left;\x0a}\x0a\x0aQAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off {\x0a image: url(:/qss_icons/rc/up_arrow_disabled.png);\x0a width: 10px;\x0a height: 10px;\x0a}\x0aQAbstractSpinBox::up-arrow:hover\x0a{\x0a image: url(:/qss_icons/rc/up_arrow.png);\x0a}\x0a\x0a\x0aQAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off\x0a{\x0a image: url(:/qss_icons/rc/down_arrow_disabled.png);\x0a width: 10px;\x0a height: 10px;\x0a}\x0aQAbstractSpinBox::down-arrow:hover\x0a{\x0a image: url(:/qss_icons/rc/down_arrow.png);\x0a}\x0a\x0a\x0aQLabel\x0a{\x0a border: 0px solid black;\x0a background-color: transparent;\x0a}\x0a\x0aQTabWidget{\x0a border: 1px transparent black;\x0a}\x0a\x0aQTabWidget::pane {\x0a border: 1px solid #444;\x0a border-radius: 3px;\x0a padding: 3px;\x0a}\x0a\x0aQTabBar\x0a{\x0a qproperty-drawBase: 0;\x0a left: 5px; /* move to the right by 5px */\x0a}\x0a\x0aQTabBar:focus\x0a{\x0a border: 0px transparent black;\x0a}\x0a\x0aQTabBar::close-button {\x0a image: url(:/qss_icons/rc/close.png);\x0a background: transparent;\x0a}\x0a\x0aQTabBar::close-button:hover\x0a{\x0a image: url(:/qss_icons/rc/close-hover.png);\x0a background: transparent;\x0a}\x0a\x0aQTabBar::close-button:pressed {\x0a image: url(:/qss_icons/rc/close-pressed.png);\x0a background: transparent;\x0a}\x0a\x0a/* TOP TABS */\x0aQTabBar::tab:top {\x0a color: #b1b1b1;\x0a border: 1px solid #4A4949;\x0a border-bottom: 1px transparent black;\x0a background-color: #302F2F;\x0a padding: 5px;\x0a border-top-left-radius: 2px;\x0a border-top-right-radius: 2px;\x0a}\x0a\x0aQTabBar::tab:top:!selected\x0a{\x0a color: #b1b1b1;\x0a background-color: #201F1F;\x0a border: 1px transparent #4A4949;\x0a border-bottom: 1px transparent #4A4949;\x0a border-top-left-radius: 0px;\x0a border-top-right-radius: 0px;\x0a}\x0a\x0aQTabBar::tab:top:!selected:hover {\x0a background-color: #48576b;\x0a}\x0a\x0a/* BOTTOM TABS */\x0aQTabBar::tab:bottom {\x0a color: #b1b1b1;\x0a border: 1px solid #4A4949;\x0a border-top: 1px transparent black;\x0a background-color: #302F2F;\x0a padding: 5px;\x0a border-bottom-left-radius: 2px;\x0a border-bottom-right-radius: 2px;\x0a}\x0a\x0aQTabBar::tab:bottom:!selected\x0a{\x0a color: #b1b1b1;\x0a background-color: #201F1F;\x0a border: 1px transparent #4A4949;\x0a border-top: 1px transparent #4A4949;\x0a border-bottom-left-radius: 0px;\x0a border-bottom-right-radius: 0px;\x0a}\x0a\x0aQTabBar::tab:bottom:!selected:hover {\x0a background-color: #78879b;\x0a}\x0a\x0a/* LEFT TABS */\x0aQTabBar::tab:left {\x0a color: #b1b1b1;\x0a border: 1px solid #4A4949;\x0a border-left: 1px transparent black;\x0a background-color: #302F2F;\x0a padding: 5px;\x0a border-top-right-radius: 2px;\x0a border-bottom-right-radius: 2px;\x0a}\x0a\x0aQTabBar::tab:left:!selected\x0a{\x0a color: #b1b1b1;\x0a background-color: #201F1F;\x0a border: 1px transparent #4A4949;\x0a border-right: 1px transparent #4A4949;\x0a border-top-right-radius: 0px;\x0a border-bottom-right-radius: 0px;\x0a}\x0a\x0aQTabBar::tab:left:!selected:hover {\x0a background-color: #48576b;\x0a}\x0a\x0a\x0a/* RIGHT TABS */\x0aQTabBar::tab:right {\x0a color: #b1b1b1;\x0a border: 1px solid #4A4949;\x0a border-right: 1px transparent black;\x0a background-color: #302F2F;\x0a padding: 5px;\x0a border-top-left-radius: 2px;\x0a border-bottom-left-radius: 2px;\x0a}\x0a\x0aQTabBar::tab:right:!selected\x0a{\x0a color: #b1b1b1;\x0a background-color: #201F1F;\x0a border: 1px transparent #4A4949;\x0a border-right: 1px transparent #4A4949;\x0a border-top-left-radius: 0px;\x0a border-bottom-left-radius: 0px;\x0a}\x0a\x0aQTabBar::tab:right:!selected:hover {\x0a background-color: #48576b;\x0a}\x0a\x0aQTabBar QToolButton::right-arrow:enabled {\x0a image: url(:/qss_icons/rc/right_arrow.png);\x0a }\x0a\x0a QTabBar QToolButton::left-arrow:enabled {\x0a image: url(:/qss_icons/rc/left_arrow.png);\x0a }\x0a\x0aQTabBar QToolButton::right-arrow:disabled {\x0a image: url(:/qss_icons/rc/right_arrow_disabled.png);\x0a }\x0a\x0a QTabBar QToolButton::left-arrow:disabled {\x0a image: url(:/qss_icons/rc/left_arrow_disabled.png);\x0a }\x0a\x0a\x0aQDockWidget {\x0a border: 1px solid #403F3F;\x0a titlebar-close-icon: url(:/qss_icons/rc/close.png);\x0a titlebar-normal-icon: url(:/qss_icons/rc/undock.png);\x0a}\x0a\x0aQDockWidget::close-button, QDockWidget::float-button {\x0a border: 1px solid transparent;\x0a border-radius: 2px;\x0a background: transparent;\x0a}\x0a\x0aQDockWidget::close-button:hover, QDockWidget::float-button:hover {\x0a background: rgba(255, 255, 255, 10);\x0a}\x0a\x0aQDockWidget::close-button:pressed, QDockWidget::float-button:pressed {\x0a padding: 1px -1px -1px 1px;\x0a background: rgba(255, 255, 255, 10);\x0a}\x0a\x0aQTreeView, QListView\x0a{\x0a border: 1px solid #444;\x0a background-color: #201F1F;\x0a}\x0a\x0aQTreeView:branch:selected, QTreeView:branch:hover\x0a{\x0a background: url(:/qss_icons/rc/transparent.png);\x0a}\x0a\x0aQTreeView::branch:has-siblings:!adjoins-item {\x0a border-image: url(:/qss_icons/rc/transparent.png);\x0a}\x0a\x0aQTreeView::branch:has-siblings:adjoins-item {\x0a border-image: url(:/qss_icons/rc/transparent.png);\x0a}\x0a\x0aQTreeView::branch:!has-children:!has-siblings:adjoins-item {\x0a border-image: url(:/qss_icons/rc/transparent.png);\x0a}\x0a\x0aQTreeView::branch:has-children:!has-siblings:closed,\x0aQTreeView::branch:closed:has-children:has-siblings {\x0a image: url(:/qss_icons/rc/branch_closed.png);\x0a}\x0a\x0aQTreeView::branch:open:has-children:!has-siblings,\x0aQTreeView::branch:open:has-children:has-siblings {\x0a image: url(:/qss_icons/rc/branch_open.png);\x0a}\x0a\x0aQTreeView::branch:has-children:!has-siblings:closed:hover,\x0aQTreeView::branch:closed:has-children:has-siblings:hover {\x0a image: url(:/qss_icons/rc/branch_closed-on.png);\x0a }\x0a\x0aQTreeView::branch:open:has-children:!has-siblings:hover,\x0aQTreeView::branch:open:has-children:has-siblings:hover {\x0a image: url(:/qss_icons/rc/branch_open-on.png);\x0a }\x0a\x0aQListView::item:!selected:hover, QListView::item:!selected:hover, QTreeView::item:!selected:hover {\x0a background: rgba(0, 0, 0, 0);\x0a outline: 0;\x0a color: #FFFFFF\x0a}\x0a\x0aQListView::item:selected:hover, QListView::item:selected:hover, QTreeView::item:selected:hover {\x0a background: #3d8ec9;\x0a color: #FFFFFF;\x0a}\x0a\x0aQSlider::groove:horizontal {\x0a border: 1px solid #3A3939;\x0a height: 8px;\x0a background: #201F1F;\x0a margin: 2px 0;\x0a border-radius: 2px;\x0a}\x0a\x0aQSlider::handle:horizontal {\x0a background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1,\x0a stop: 0.0 silver, stop: 0.2 #a8a8a8, stop: 1 #727272);\x0a border: 1px solid #3A3939;\x0a width: 14px;\x0a height: 14px;\x0a margin: -4px 0;\x0a border-radius: 2px;\x0a}\x0a\x0aQSlider::groove:vertical {\x0a border: 1px solid #3A3939;\x0a width: 8px;\x0a background: #201F1F;\x0a margin: 0 0px;\x0a border-radius: 2px;\x0a}\x0a\x0aQSlider::handle:vertical {\x0a background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 silver,\x0a stop: 0.2 #a8a8a8, stop: 1 #727272);\x0a border: 1px solid #3A3939;\x0a width: 14px;\x0a height: 14px;\x0a margin: 0 -4px;\x0a border-radius: 2px;\x0a}\x0a\x0aQToolButton {\x0a background-color: transparent;\x0a border: 1px transparent #4A4949;\x0a border-radius: 2px;\x0a margin: 3px;\x0a padding: 3px;\x0a}\x0a\x0aQToolButton[popupMode=\x221\x22] { /* only for MenuButtonPopup */\x0a padding-right: 20px; /* make way for the popup button */\x0a border: 1px transparent #4A4949;\x0a border-radius: 5px;\x0a}\x0a\x0aQToolButton[popupMode=\x222\x22] { /* only for InstantPopup */\x0a padding-right: 10px; /* make way for the popup button */\x0a border: 1px transparent #4A4949;\x0a}\x0a\x0a\x0aQToolButton:hover, QToolButton::menu-button:hover {\x0a background-color: transparent;\x0a border: 1px solid #78879b;\x0a}\x0a\x0aQToolButton:checked, QToolButton:pressed,\x0a QToolButton::menu-button:pressed {\x0a background-color: #4A4949;\x0a border: 1px solid #78879b;\x0a}\x0a\x0a/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */\x0aQToolButton::menu-indicator {\x0a image: url(:/qss_icons/rc/down_arrow.png);\x0a top: -7px; left: -2px; /* shift it a bit */\x0a}\x0a\x0a/* the subcontrols below are used only in the MenuButtonPopup mode */\x0aQToolButton::menu-button {\x0a border: 1px transparent #4A4949;\x0a border-top-right-radius: 6px;\x0a border-bottom-right-radius: 6px;\x0a /* 16px width + 4px for border = 20px allocated above */\x0a width: 16px;\x0a outline: none;\x0a}\x0a\x0aQToolButton::menu-arrow {\x0a image: url(:/qss_icons/rc/down_arrow.png);\x0a}\x0a\x0aQToolButton::menu-arrow:open {\x0a top: 1px; left: 1px; /* shift it a bit */\x0a border: 1px solid #3A3939;\x0a}\x0a\x0aQPushButton::menu-indicator {\x0a subcontrol-origin: padding;\x0a subcontrol-position: bottom right;\x0a left: 8px;\x0a}\x0a\x0aQTableView\x0a{\x0a border: 1px solid #444;\x0a gridline-color: #6c6c6c;\x0a background-color: #201F1F;\x0a}\x0a\x0a\x0aQTableView, QHeaderView\x0a{\x0a border-radius: 0px;\x0a}\x0a\x0aQTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed {\x0a background: #78879b;\x0a color: #FFFFFF;\x0a}\x0a\x0aQTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active {\x0a background: #3d8ec9;\x0a color: #FFFFFF;\x0a}\x0a\x0a\x0aQHeaderView\x0a{\x0a border: 1px transparent;\x0a border-radius: 2px;\x0a margin: 0px;\x0a padding: 0px;\x0a}\x0a\x0aQHeaderView::section {\x0a background-color: #3A3939;\x0a color: silver;\x0a padding: 4px;\x0a border: 1px solid #6c6c6c;\x0a border-radius: 0px;\x0a text-align: center;\x0a}\x0a\x0aQHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one\x0a{\x0a border-top: 1px solid #6c6c6c;\x0a}\x0a\x0aQHeaderView::section::vertical\x0a{\x0a border-top: transparent;\x0a}\x0a\x0aQHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one\x0a{\x0a border-left: 1px solid #6c6c6c;\x0a}\x0a\x0aQHeaderView::section::horizontal\x0a{\x0a border-left: transparent;\x0a}\x0a\x0a\x0aQHeaderView::section:checked\x0a {\x0a color: white;\x0a background-color: #5A5959;\x0a }\x0a\x0a /* style the sort indicator */\x0aQHeaderView::down-arrow {\x0a image: url(:/qss_icons/rc/down_arrow.png);\x0a}\x0a\x0aQHeaderView::up-arrow {\x0a image: url(:/qss_icons/rc/up_arrow.png);\x0a}\x0a\x0a\x0aQTableCornerButton::section {\x0a background-color: #3A3939;\x0a border: 1px solid #3A3939;\x0a border-radius: 2px;\x0a}\x0a\x0aQToolBox {\x0a padding: 3px;\x0a border: 1px transparent black;\x0a}\x0a\x0aQToolBox::tab {\x0a color: #b1b1b1;\x0a background-color: #302F2F;\x0a border: 1px solid #4A4949;\x0a border-bottom: 1px transparent #302F2F;\x0a border-top-left-radius: 5px;\x0a border-top-right-radius: 5px;\x0a}\x0a\x0a QToolBox::tab:selected { /* italicize selected tabs */\x0a font: italic;\x0a background-color: #302F2F;\x0a border-color: #3d8ec9;\x0a }\x0a\x0aQStatusBar::item {\x0a border: 1px solid #3A3939;\x0a border-radius: 2px;\x0a }\x0a\x0a\x0aQFrame[height=\x223\x22], QFrame[width=\x223\x22] {\x0a background-color: #444;\x0a}\x0a\x0a\x0aQSplitter::handle {\x0a border: 1px dashed #3A3939;\x0a}\x0a\x0aQSplitter::handle:hover {\x0a background-color: #787876;\x0a border: 1px solid #3A3939;\x0a}\x0a\x0aQSplitter::handle:horizontal {\x0a width: 1px;\x0a}\x0a\x0aQSplitter::handle:vertical {\x0a height: 1px;\x0a}\x0a\x0aMessageItem\x0a{\x0a border: none;\x0a}\x0a\x0aMessageEdit\x0a{\x0a border: none;\x0a}\x0a\x0aMessageEdit::focus\x0a{\x0a border: none;\x0a}\x0a\x0aMessageItem::focus\x0a{\x0a border: none;\x0a}\x0a\x0aMessageEdit:hover\x0a{\x0a border: none;\x0a}\x0a\x0aQListWidget QPushButton \x0a{\x0a background-color: transparent;\x0a border: none;\x0a}\x0a\x0aQPushButton:hover \x0a{\x0a background-color: #4A4949;\x0a}\x0a\x0a#messages:item:selected\x0a{\x0a background-color: transparent;\x0a}\x0a\x0a#friends_list:item:selected\x0a{\x0a background-color: #333333;\x0a}\x0a\x0a#toxygen\x0a{\x0a color: #A9A9A9;\x0a}\x0a\x0aQCheckBox\x0a{\x0a spacing: 5px;\x0a outline: none;\x0a color: #bbb;\x0a margin-bottom: 2px;\x0a text-align: center;\x0a}\x0a\x0aQListWidget > QLabel \x0a{\x0a color: #A9A9A9;\x0a}\x0a\x0a#contact_name\x0a{\x0a padding-left: 22px;\x0a}\x00\x00\x03\xa5\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x03\x22IDATX\x85\xed\x96MlTU\x14\xc7\x7f\xe7\x0d\xa9\x09\xcc\x90Pv\xb6\xc6``\xe3\xa3\x864\xf4\xc3\xc6g\xa4\x1b\xa2\x98@\x13]\xc9\x1a6\xda\x84~Y\x5c\xcd\xce:\xa43\x09\xcb\xaee\x83\x89\x19L\x04\xc3\xc6:\x98\xb4o\x22bK'\xc64\xac\x9c\x067\x94t\x98\x92P:\xef\xef\xe2M\xa75\x99\xe9\xccCv\xf4\xbf\xba\xe7\xbds\xef\xf9\xdds\xee\x17\xeciO\xaf\xba,\x8a\xb3\x9b,\xb4\x1dN\xac\x0f\xc98\x07\xea\x06:\xaa\xbf\x8a\x88\xdf\xcd,\xfb\xa8t [H\xba\x1b/\x1d\xc0\xcb\xcc\x7f\x82,\x05\x1c\x01\xbb\x8f4\x8bC\x11\xc0\xa4\x0e\xe1\x9c\x02ua<0l\x22w\xa9\xf7\xfb\x97\x02\xf0\xe9\xf5\xeb\xb1\x7fV\xdeL!F\x80\x9f$&\x7f\x1d\xed[\xa8\xe7;\x90\xc9\x9f\x88\x05\x9a\xc28\x0d\x5c\xb9S\xea\x9d$iA\xab\x93\xac+/\xe3O{i\xbf\xf2~f~\xac\xe5>i\x7f\xdcK\xfb\x15/\xed\xa7\x9a\xf9\xee\x9a\x81j\xda\xbf3l,7\xd2;\x0d\xf0\xe1\xd5\xe5\xd7\x9e<\x7f|\xd1\xe03Y\xd0\x15\x0eb\x8b\x18\xd7\xe2\xb1\xf6\x99[\xc3\xc7\x9eU\xc1'\x10\xdf`\x0c\xdd\xb9\xd4\x97\x8d\x0c\xe0&\x0bm\xed\x07\xcb\x7f\x1a\xfa+7\xd2\xff\x11\xc0\x07W\xe7;+\x9b\xceMP\x17X\x00r\xaa\xc3\x84mc1\x16\xd3\x99\xd9\xe1\xfe\x22\xc0{\x99\xfcm\x93\x8e\xac\x96\xe2n\xa3\x85\xe94\x028\x9cX\x1f\x02\xde\x0ad\x97\xb7f^\xd9tnb:\x1ezhG\xdfZ\xbb\xab\xb2\xc9\x8fn\xb2\xd0\x06\xe0\x04\xf6%p\xf4P\xa2|\xb6Q\x9c\x86\x00\xe1Vcak\xc1\x95+\xab\x17@]h\x97\xb2\x09\x03{\xa7\xfd`\xf9\x02@n\xb4\xe7\x9e\xc4\x92At\x00P\xb7\xa1_jf`\xe7\xc3T\xef.A\x00\x9c\xdf\xb2\x0d~\xc68\xf9\x02\x00\xbc.\xacX\xb3L\xee\x7f\xd3^_\x06\x0e\xc8\xdd\x01\xb4\xc2\xf6\x81\x15\x09\x00,\xdaIY7\x80\x99\x11f%2\xc0C\x02:k\x96\xac\xd0j\x09$\x96\xb6mu\x00\x0f\xa3\x03\x88\xdf\x04\xa7\xb6=\xf5m\xab%0\xb3k;>\x0d\x02\xf9\xc8\x00f\x965\xe3\xf8@&\x7f\x02 \x1ek\x9f\xc1X\xc4\xd0.\xd1%\xe3\x8f\xd5R|\x06\xc0\xcb\xccu\x03oc\xfa!2\xc0\xa3\xd2\x81,\xc6\x83X\xa0)\x80[\xc3\xc7\x9e\xc5b:\x03\xdc\xafF\xab\x95\xa3\xba\xf2\x11,TT\xf9\xb8\x90t7\x90\x0c9)`\xf9\xe9\xfe}7\x22\x03\x14\x92\xee\x86\xc48\xc6i/\xed\x8f\x03\xcc\x0e\xf7\x17W\xd7\xe2=\xc0\x17R\x90\x07\xd6\x81u\xa4\xbc\x99>\x7f\xbc\x16\xef\x9b\x1b\x19X\x01\xf0\xd2\xfe$0h\x0a\xc6\xee^<\xf9\xbcQ\x9c\xa6\xf2\xd2~\xaaz\xb1\x8c\xb7\xd4A2oz\xferx\x81\xf9S\xcd\xdc\x9bo\xb3\xa4\x1c/\x91\xff\x1ac\x02\xb8mr&s\xa3=\xf7\xea\xc2f\xe6\xba\xabi\x1f4#\x95[\xeb\xfd\xaa\xd9u\x1c\xe1A\xe2\x9fC\x5c\x01\x8eJ,\x991\x8b\xf17\x00\xe2\x0d\xc2\x1d\xe3\x02\xcb\xa6`,7\xfan\xc3\x85\xf7B\x00\x10\xde\x90\x87\x12\xe5\xb3T\x9fd\x86u\x86\xf1U4\xd9]\x1ce\x9f\xee\xdfw\xe3\x7f\xd5|O{z\xe5\xf4/\x95?G\xacm\xe50s\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02J\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdf\x04\x19\x10\x14\x1a8\xc77\xd0\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x01\xaeIDATx\xda\xed\x9bI\x92\xc3 \x0cE#]\xdc\xf6\xc9\xd3\xbb\xaeT\x06&\xe9\x7f\x09\x8c\xd6]2\xef!h \xf0x\xec\xd8\xb1\xe3\xce!\xcc\x8f\x9d\xe7\xf9l\xfc;YB@+p\xa4\x10\xc9\x0a\xcd\x92!\xb3\x80\xa3D\xc8\x8c\xf0\x9e\x12dFpO\x112;\xbcU\x82\xcc\x0en\x15!+\xc1\x8fH\x90\xd5\xe0{%\xe8^\x0a/\xd8\xfb=U V\xf8\xe38\xfes\x5c\xd7E\x11\xf5\xfa\xcd\xdawk\x12\xd4\xbba\xef\x8dC\xc3[C\x11\xa5\x8f\x920\x92\xb7\xc6\xa0\xa8q\xef-\xc1\x92\xaf\xc4b\x1e\x02\xa5\xf1\xe7%\xa1\x94\xc7:\xef\x88W\xef\xa3\x1a\xe9\x99\xf7\xdb\x84\xe86\x09\x22*\x01\xd9\xf3\x90\xff\x02\x9e\x12\x18\xf0_\x87\x80\xc7\xa2\xc7\xdax$\xfc\xfb0\x80,\x85-\x95\xc0\xeay\xf8^`D\x02\x1b\x1e\xbe\x19\xea\x91\x10\x01\xff1\x07\xa06=586\xfc\xeb<@\xd9\x0e\x8f\xce\x09\x8c\xcd\x15\xed<\xa0\x17\x86\xb5\xb3\xa4\x1e\x88\xb4B\xb1\xe0\xe9\x02Z\xe0\x98\xf0!\x02,\xeb\x80\xe9\x05\xb4\xc21%h6x\xb6\x04\x8d\x86g\x9c'\x84\x0ah\x81\x8f\x94\x00\xd9\x0d\x8e\xf6\x00\x00\x88K\x04\xd39.\x90?\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xb6\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x18\x00\x00\x00\x11\x08\x06\x00\x00\x00\xc7xl0\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b,\x0d\x1fC\xaa\xe1\x00\x00\x006IDAT8\xcbc` \x01,Z\xb4\xe8\xff\xa2E\x8b\xfe\x93\xa2\x87\x89\x81\xc6`\xd4\x82\x11`\x01#\xa9\xc9t\xd0\xf9\x80\x85\x1cMqqq\x8c\xa3\xa9h\xd4\x82ad\x01\x001\xb5\x09\xec\x1fK\xb4\x15\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02B\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xb3\x00y\x00y\xdc\xddS\xfc\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdf\x04\x19\x10\x17;_\x83tM\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x01\xa6IDATx\xda\xed\x9b\xdb\x0e\xc3 \x0cC\x9bh\xff\xdd\xf6\xcb\xb7\xb7i\x9avIK\xec\x98B^7Q|p(\x85\xb0,3f\xcc\x189\x8c\xf9\xb0m\xdb\xee\xc1\xff\xd9%\x00D\x05W\x021U\xd1,\x18\xd6\x8bp\x14\x08\xebQ|&\x04\xebQx&\x08\xeb]|+\x04\xeb]x+\x08\xbb\x92\xf83\x10\xecj\xe2\x8fB\xb8Uvr]\xd7g'\xf7}/\x01lU\xa3\xff*\x1e\x05!\xe2\x02S\x11_\x05\xc1+m\x7f\xe6wj\x0ad\x8f\xfe\x11q\x99N\xf8\xe5\x02S\x14\xcf\x84\xe0\xd5\xb6\xff%\x92\x91\x0e\x86\x1e\xfd\xa8x\xc6\xc4\xf8\xc9\x05\xae2\xf2UNp%\xdbW@0\x84\xfd[\xed\x8cL\x87\xf74p\x85\x91\xaft\x82\xab\x89gCpE\xf1L\x08\x96\x91\xff\xe8WXv\xfb\xaf\xf3\x80+\x8e<\xd3\x09\xae.\x1e\x0d\xc1{\x10\x8f\x84\xe0\xccN*\xb6O]\x07(\xb6\xefj9\xc9N;W\xcbI\xf6\x9c\xe3\xc8\x9c\xcc\x82\x80\x9cpS\xe6\x00$\x04\xf4\xdb&\xf5k0\xbb\xb3\x08\xf1\xd0\xaf\xc1L'\xb0\xd6\x19\xd4u@\x14\x02s\x91\x05\xd9\x11j\x81\xc0^aB7E\x8f\x8aA\x8b\xa7o\x8a\x1eqB\xc5\xb7\x05\x1c@\x14B\x95\xf8\xaf)\x90\x99\x06-\xeb\x81\xcb\x9c\x0c\x9d\x11\xc3\xaa\x17\xa0\x1e\x8eF\x9d\xc0<\x22\xa7\x1f\x8f\xff\x13\xc7\xae\x14))\x90\xf8\xe6\x04\x84\xf8\x7f\x05\x12e%2\xef\x10*\xc4\x87\x01 !\xa0\x22Z%\xe6\xcb\xe01\x0b%O4>n\xa9\xac2\x08Z\xb1\xb4\x22\x84\x92ry\x15\x08\xad\x97&\xe6\x95\x19@\xc7\xc6\xbc4\x85\x84\xd1\xd5\xb5\xb9\x0c \xcc\x8b\x933F\x8f\x07S!r\xe7\x176+c\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02\xd4\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x02QIDATX\x85\xed\x96AKTQ\x14\xc7\x7f\xe7\x8d\xb8\xd0&0wi\x84\xe1\xaa)\x90A\xc7\x92^\xa0\x1b\xa1\x8d\x0a\xf5\x19Z;3\xda\xd8j\x16A6\x83\xf3\xbe\x87A\x8d\xad\xc2M\xf6\x14\xf4\x0d\x99H\x0e\x11\xe2\xaa\x11\xdb\x184\xa8\x0b\xc3wZ\xccH\x10\xf3t\xee\xe8\xae\xf9o\xef9\xfc\x7f\xf7\xdc{\xcf=\xd0TS\xff\xbb\xc4$8\x92.\xb6v\x86\x0f'T\x18\x07\x8d\x02]\xd5\xa5\x12\xcag\x11\xc9\xef\x97\xdb\xf3\xc5t\xe4\xf8\xd2\x01lg\xed1*\x19\xa0\x07\xe4\x0b\xaaKX\x94\x00D\xb5K\xb1\x86A\xef\x22\xec\x082\xedN\xc6\xde\x5c\x0a\xc0\x93\xf9\xf9\xd0\x8f\xdd\x9b\x19\x948\xf0^\x95\xd4Jbp\xb3V\xec\x90S\xe8\x0b\xf9:\x8b0\x0ad\x97\xcb\xb1\x14i\xf1\xeb\xdddM\xd9\x8e7g\xe7\xbc\x93\x87\xceZ\xb2\xee\x9c\x9c7e\xe7\xbc\x13;\xe7e\xce\x8b=\xb3\x02\xd5\xb2\xbf\x16$\xe9\xc6cs\xf5\x02Tr\xbdi\x94W\x08\x13\xcb\x93\x83yc\x80H\xba\xd8z\xed\xea\xc1WA\xbf\xb9\xf1{\x8fL\xccO\xf5\xc0),\x8aj\xcf\xcf\xf2\x95H\xd0\xc5\xb4\x82\x92;\xc3\x87\x13\xc0-_e\xa6\x11s\x00\xcb\x97g@oG\xf8`,0&h\xa1\xf2\xd4\xd8\x0c\xbap\xf5\xc8M\x0cl\xa8\xb2%`\x0e\x00\x1a\x15\xf4c\xa3\xe6\xa7\x12\xf8\x80\xd0\xdf\x00\x00\xd7\x15)]\x14@a\x97\xbf\x0d\xcb\x08\x00\xc4\xacS\xd64\x10\x11 \xb0\x17\x9c\x05\xb0\x87O\xf7E\x01\x14\xed\x02\xf6\xcc\x01\x94O\x0a\xc3\x17\x05\x00F\x80\x821\x80\x88\xe4E\xb83\xe4\x14\xfa\x1au\xb6\x9d\xd5(p\x1b\xd1w\xc6\x00\xfb\xe5\xf6<\xc2N\xc8\xd7\xd9\x86\xdcU\x05\xb52\xc0\xf6Q[\xcb\x821@1\x1d9Ve\x0aa\xd4\xceyS\xa6\xfev\xceK\x01#\xa2~r\xfdi\xffoc\x00\x80\x95\xf8\xe0[ \x0b\xcc\xd6\x0d\xa1*\xf6\xdc\xda\x0c\x22/D\xc8\xb8\x89\xfb\x81\xe5\x87z\xe6\x81\xb4Zv\xb8\xf0\x12a\x1aX\x14\xb5Rnb`\xa3V\xa8\xed\xacF\xabe\x1f\x11!\xe3\xfe\x8a=?\xef;6\x18H\xbcq\x94,\xd0\xab\xca\x96\x08K\x08\xdf\x01PnPy1\x11`[\xd4O\x9e\xb7sc\x00\xa8\xfc\x90\x1d\xe1\x831\xaa#\x99 \xdd\x15\x7f-\x89\xca:\x96\xe6\x8f\xdaZ\x16\xce:\xf3\xa6\x9aj\xea_\xfd\x01\xd3\x1c\xd9\x7f^\xb93\xcd\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\x9f\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne4\xac\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x0bN\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x0aOiCCPPhotoshop ICC profile\x00\x00x\xda\x9dSgTS\xe9\x16=\xf7\xde\xf4BK\x88\x80\x94KoR\x15\x08 RB\x8b\x80\x14\x91&*!\x09\x10J\x88!\xa1\xd9\x15Q\xc1\x11EE\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15Q,\x0c\x8a\x0a\xd8\x07\xe4!\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1{\xa3k\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7>\xe7\xac\xf3\x9d\xb3\xcf\x07\xc0\x08\x0c\x96H3Q5\x80\x0c\xa9B\x1e\x11\xe0\x83\xc7\xc4\xc6\xe1\xe4.@\x81\x0a$p\x00\x10\x08\xb3d!s\xfd#\x01\x00\xf8~<<+\x22\xc0\x07\xbe\x00\x01x\xd3\x0b\x08\x00\xc0M\x9b\xc00\x1c\x87\xff\x0f\xeaB\x99\x5c\x01\x80\x84\x01\xc0t\x918K\x08\x80\x14\x00@z\x8eB\xa6\x00@F\x01\x80\x9d\x98&S\x00\xa0\x04\x00`\xcbcb\xe3\x00P-\x00`'\x7f\xe6\xd3\x00\x80\x9d\xf8\x99{\x01\x00[\x94!\x15\x01\xa0\x91\x00 \x13e\x88D\x00h;\x00\xac\xcfV\x8aE\x00X0\x00\x14fK\xc49\x00\xd8-\x000IWfH\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x000Q\x88\x85)\x00\x04{\x00`\xc8##x\x00\x84\x99\x00\x14F\xf2W<\xf1+\xae\x10\xe7*\x00\x00x\x99\xb2<\xb9$9E\x81[\x08-q\x07WW.\x1e(\xceI\x17+\x146a\x02a\x9a@.\xc2y\x99\x192\x814\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\xf3\xfdx\xce\x0e\xae\xce\xce6\x8e\xb6\x0e_-\xea\xbf\x06\xff\x22bb\xe3\xfe\xe5\xcf\xabp@\x00\x00\xe1t~\xd1\xfe,/\xb3\x1a\x80;\x06\x80m\xfe\xa2%\xee\x04h^\x0b\xa0u\xf7\x8bf\xb2\x0f@\xb5\x00\xa0\xe9\xdaW\xf3p\xf8~<\xdf5\x00\xb0j>\x01{\x91-\xa8]c\x03\xf6K'\x10Xt\xc0\xe2\xf7\x00\x00\xf2\xbbo\xc1\xd4(\x08\x03\x80h\x83\xe1\xcfw\xff\xef?\xfdG\xa0%\x00\x80fI\x92q\x00\x00^D$.T\xca\xb3?\xc7\x08\x00\x00D\xa0\x81*\xb0A\x1b\xf4\xc1\x18,\xc0\x06\x1c\xc1\x05\xdc\xc1\x0b\xfc`6\x84B$\xc4\xc2B\x10B\x0ad\x80\x1cr`)\xac\x82B(\x86\xcd\xb0\x1d*`/\xd4@\x1d4\xc0Qh\x86\x93p\x0e.\xc2U\xb8\x0e=p\x0f\xfaa\x08\x9e\xc1(\xbc\x81\x09\x04A\xc8\x08\x13a!\xda\x88\x01b\x8aX#\x8e\x08\x17\x99\x85\xf8!\xc1H\x04\x12\x8b$ \xc9\x88\x14Q\x22K\x915H1R\x8aT UH\x1d\xf2=r\x029\x87\x5cF\xba\x91;\xc8\x002\x82\xfc\x86\xbcG1\x94\x81\xb2Q=\xd4\x0c\xb5C\xb9\xa87\x1a\x84F\xa2\x0b\xd0dt1\x9a\x8f\x16\xa0\x9b\xd0r\xb4\x1a=\x8c6\xa1\xe7\xd0\xabh\x0f\xda\x8f>C\xc70\xc0\xe8\x18\x073\xc4l0.\xc6\xc3B\xb18,\x09\x93c\xcb\xb1\x22\xac\x0c\xab\xc6\x1a\xb0V\xac\x03\xbb\x89\xf5c\xcf\xb1w\x04\x12\x81E\xc0\x096\x04wB a\x1eAHXLXN\xd8H\xa8 \x1c$4\x11\xda\x097\x09\x03\x84Q\xc2'\x22\x93\xa8K\xb4&\xba\x11\xf9\xc4\x18b21\x87XH,#\xd6\x12\x8f\x13/\x10{\x88C\xc47$\x12\x89C2'\xb9\x90\x02I\xb1\xa4T\xd2\x12\xd2F\xd2nR#\xe9,\xa9\x9b4H\x1a#\x93\xc9\xdadk\xb2\x079\x94, +\xc8\x85\xe4\x9d\xe4\xc3\xe43\xe4\x1b\xe4!\xf2[\x0a\x9db@q\xa4\xf8S\xe2(R\xcajJ\x19\xe5\x10\xe54\xe5\x06e\x982AU\xa3\x9aR\xdd\xa8\xa1T\x115\x8fZB\xad\xa1\xb6R\xafQ\x87\xa8\x134u\x9a9\xcd\x83\x16IK\xa5\xad\xa2\x95\xd3\x1ah\x17h\xf7i\xaf\xe8t\xba\x11\xdd\x95\x1eN\x97\xd0W\xd2\xcb\xe9G\xe8\x97\xe8\x03\xf4w\x0c\x0d\x86\x15\x83\xc7\x88g(\x19\x9b\x18\x07\x18g\x19w\x18\xaf\x98L\xa6\x19\xd3\x8b\x19\xc7T071\xeb\x98\xe7\x99\x0f\x99oUX*\xb6*|\x15\x91\xca\x0a\x95J\x95&\x95\x1b*/T\xa9\xaa\xa6\xaa\xde\xaa\x0bU\xf3U\xcbT\x8f\xa9^S}\xaeFU3S\xe3\xa9\x09\xd4\x96\xabU\xaa\x9dP\xebS\x1bSg\xa9;\xa8\x87\xaag\xa8oT?\xa4~Y\xfd\x89\x06Y\xc3L\xc3OC\xa4Q\xa0\xb1_\xe3\xbc\xc6 \x0bc\x19\xb3x,!k\x0d\xab\x86u\x815\xc4&\xb1\xcd\xd9|v*\xbb\x98\xfd\x1d\xbb\x8b=\xaa\xa9\xa19C3J3W\xb3R\xf3\x94f?\x07\xe3\x98q\xf8\x9ctN\x09\xe7(\xa7\x97\xf3~\x8a\xde\x14\xef)\xe2)\x1b\xa64L\xb91e\x5ck\xaa\x96\x97\x96X\xabH\xabQ\xabG\xeb\xbd6\xae\xed\xa7\x9d\xa6\xbdE\xbbY\xfb\x81\x0eA\xc7J'\x5c'Gg\x8f\xce\x05\x9d\xe7S\xd9S\xdd\xa7\x0a\xa7\x16M=:\xf5\xae.\xaak\xa5\x1b\xa1\xbbDw\xbfn\xa7\xee\x98\x9e\xbe^\x80\x9eLo\xa7\xdey\xbd\xe7\xfa\x1c}/\xfdT\xfdm\xfa\xa7\xf5G\x0cX\x06\xb3\x0c$\x06\xdb\x0c\xce\x18<\xc55qo<\x1d/\xc7\xdb\xf1QC]\xc3@C\xa5a\x95a\x97\xe1\x84\x91\xb9\xd1<\xa3\xd5F\x8dF\x0f\x8ci\xc6\x5c\xe3$\xe3m\xc6m\xc6\xa3&\x06&!&KM\xeaM\xee\x9aRM\xb9\xa6)\xa6;L;L\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x995\x9b=1\xd72\xe7\x9b\xe7\x9b\xd7\x9b\xdf\xb7`ZxZ,\xb6\xa8\xb6\xb8eI\xb2\xe4Z\xa6Y\xee\xb6\xbcn\x85Z9Y\xa5XUZ]\xb3F\xad\x9d\xad%\xd6\xbb\xad\xbb\xa7\x11\xa7\xb9N\x93N\xab\x9e\xd6g\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\x19\xb0\xe5\xd8\x06\xdb\xae\xb6m\xb6}agb\x17g\xb7\xc5\xae\xc3\xee\x93\xbd\x93}\xba}\x8d\xfd=\x07\x0d\x87\xd9\x0e\xab\x1dZ\x1d~s\xb4r\x14:V:\xde\x9a\xce\x9c\xee?}\xc5\xf4\x96\xe9/gX\xcf\x10\xcf\xd83\xe3\xb6\x13\xcb)\xc4i\x9dS\x9b\xd3Gg\x17g\xb9s\x83\xf3\x88\x8b\x89K\x82\xcb.\x97>.\x9b\x1b\xc6\xdd\xc8\xbd\xe4Jt\xf5q]\xe1z\xd2\xf5\x9d\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee6\xeei\xee\x87\xdc\x9f\xcc4\x9f)\x9eY3s\xd0\xc3\xc8C\xe0Q\xe5\xd1?\x0b\x9f\x950k\xdf\xac~OCO\x81g\xb5\xe7#/c/\x91W\xad\xd7\xb0\xb7\xa5w\xaa\xf7a\xef\x17>\xf6>r\x9f\xe3>\xe3<7\xde2\xdeY_\xcc7\xc0\xb7\xc8\xb7\xcbO\xc3o\x9e_\x85\xdfC\x7f#\xffd\xffz\xff\xd1\x00\xa7\x80%\x01g\x03\x89\x81A\x81[\x02\xfb\xf8z|!\xbf\x8e?:\xdbe\xf6\xb2\xd9\xedA\x8c\xa0\xb9A\x15A\x8f\x82\xad\x82\xe5\xc1\xad!h\xc8\xec\x90\xad!\xf7\xe7\x98\xce\x91\xcei\x0e\x85P~\xe8\xd6\xd0\x07a\xe6a\x8b\xc3~\x0c'\x85\x87\x85W\x86?\x8ep\x88X\x1a\xd11\x975w\xd1\xdcCs\xdfD\xfaD\x96D\xde\x9bg1O9\xaf-J5*>\xaa.j<\xda7\xba4\xba?\xc6.fY\xcc\xd5X\x9dXIlK\x1c9.*\xae6nl\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3{\x17\x98/\xc8]py\xa1\xce\xc2\xf4\x85\xa7\x16\xa9.\x12,:\x96@L\x88N8\x94\xf0A\x10*\xa8\x16\x8c%\xf2\x13w%\x8e\x0ay\xc2\x1d\xc2g\x22/\xd16\xd1\x88\xd8C\x5c*\x1eN\xf2H*Mz\x92\xec\x91\xbc5y$\xc53\xa5,\xe5\xb9\x84'\xa9\x90\xbcL\x0dL\xdd\x9b:\x9e\x16\x9av m2=:\xbd1\x83\x92\x91\x90qB\xaa!M\x93\xb6g\xeag\xe6fv\xcb\xace\x85\xb2\xfe\xc5n\x8b\xb7/\x1e\x95\x07\xc9k\xb3\x90\xac\x05Y-\x0a\xb6B\xa6\xe8TZ(\xd7*\x07\xb2geWf\xbf\xcd\x89\xca9\x96\xab\x9e+\xcd\xed\xcc\xb3\xca\xdb\x907\x9c\xef\x9f\xff\xed\x12\xc2\x12\xe1\x92\xb6\xa5\x86KW-\x1dX\xe6\xbd\xacj9\xb2\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8(\xdcx\xe5\x1b\x87o\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9d\xcff\xd2f\xe9\xe6\xde-\x9e[\x0e\x96\xaa\x97\xe6\x97\x0en\x0d\xd9\xda\xb4\x0d\xdfV\xb4\xed\xf5\xf6E\xdb/\x97\xcd(\xdb\xbb\x83\xb6C\xb9\xa3\xbf<\xb8\xbce\xa7\xc9\xce\xcd;?T\xa4T\xf4T\xfaT6\xee\xd2\xdd\xb5a\xd7\xf8n\xd1\xee\x1b{\xbc\xf64\xec\xd5\xdb[\xbc\xf7\xfd>\xc9\xbe\xdbU\x01UM\xd5f\xd5e\xfbI\xfb\xb3\xf7?\xae\x89\xaa\xe9\xf8\x96\xfbm]\xadNmq\xed\xc7\x03\xd2\x03\xfd\x07#\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2=TR\x8f\xd6+\xebG\x0e\xc7\x1f\xbe\xfe\x9d\xefw-\x0d6\x0dU\x8d\x9c\xc6\xe2#pDy\xe4\xe9\xf7\x09\xdf\xf7\x1e\x0d:\xdav\x8c{\xac\xe1\x07\xd3\x1fv\x1dg\x1d/jB\x9a\xf2\x9aF\x9bS\x9a\xfb[b[\xbaO\xcc>\xd1\xd6\xea\xdez\xfcG\xdb\x1f\x0f\x9c499\xe2?r\xfd\xe9\xfc\xa7C\xcfd\xcf&\x9e\x17\xfe\xa2\xfe\xcb\xae\x17\x16/~\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\x93\xbfm|\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\xbe\xc9x31^\xf4V\xfb\xed\xc1w\xdcw\x1d\xef\xa3\xdf\x0fO\xe4| \x7f(\xffh\xf9\xb1\xf5S\xd0\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfcc3-\xdb\x00\x00\x00 cHRM\x00\x00z%\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17o\x92_\xc5F\x00\x00\x00yIDATx\xda\xec\x971\x0a\xc00\x0c\x03%\x93_\xf5\xfd}\x97\xb3\xb4\x10h\x07gPR\xa8$\x07\x14f&vV`s\xb5\xbb9I\x00X%\x07\x8fK\xf9Q\x81\x95^\xe4C\x817J\xd5\xd2\xca\x0dP!{\x15\x80J\xef?\xf7\x0a\x0c`\x00\x03\x18\xc0\x00\x060\x80\x01\x0c\x10\xd5\xf4\xaaJ\xc61\x13\xa1\x15\xb1\xbc\xcd\x0e(-\xe0\x22\xdb9\xee\xe2\xef\x7f\xc7\x1d\x00\x00\xff\xff\x03\x00>H\x12?\xd7\xafML\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xc3\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x0b\x07\x09.7\xffD\xe8\xf0\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x00'IDATx\xda\xed\xc1\x01\x0d\x00\x00\x00\xc2\xa0\xf7Om\x0e7\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80w\x03@@\x00\x01\xafz\x0e\xe8\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x0bN\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x0aOiCCPPhotoshop ICC profile\x00\x00x\xda\x9dSgTS\xe9\x16=\xf7\xde\xf4BK\x88\x80\x94KoR\x15\x08 RB\x8b\x80\x14\x91&*!\x09\x10J\x88!\xa1\xd9\x15Q\xc1\x11EE\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15Q,\x0c\x8a\x0a\xd8\x07\xe4!\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1{\xa3k\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7>\xe7\xac\xf3\x9d\xb3\xcf\x07\xc0\x08\x0c\x96H3Q5\x80\x0c\xa9B\x1e\x11\xe0\x83\xc7\xc4\xc6\xe1\xe4.@\x81\x0a$p\x00\x10\x08\xb3d!s\xfd#\x01\x00\xf8~<<+\x22\xc0\x07\xbe\x00\x01x\xd3\x0b\x08\x00\xc0M\x9b\xc00\x1c\x87\xff\x0f\xeaB\x99\x5c\x01\x80\x84\x01\xc0t\x918K\x08\x80\x14\x00@z\x8eB\xa6\x00@F\x01\x80\x9d\x98&S\x00\xa0\x04\x00`\xcbcb\xe3\x00P-\x00`'\x7f\xe6\xd3\x00\x80\x9d\xf8\x99{\x01\x00[\x94!\x15\x01\xa0\x91\x00 \x13e\x88D\x00h;\x00\xac\xcfV\x8aE\x00X0\x00\x14fK\xc49\x00\xd8-\x000IWfH\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x000Q\x88\x85)\x00\x04{\x00`\xc8##x\x00\x84\x99\x00\x14F\xf2W<\xf1+\xae\x10\xe7*\x00\x00x\x99\xb2<\xb9$9E\x81[\x08-q\x07WW.\x1e(\xceI\x17+\x146a\x02a\x9a@.\xc2y\x99\x192\x814\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\xf3\xfdx\xce\x0e\xae\xce\xce6\x8e\xb6\x0e_-\xea\xbf\x06\xff\x22bb\xe3\xfe\xe5\xcf\xabp@\x00\x00\xe1t~\xd1\xfe,/\xb3\x1a\x80;\x06\x80m\xfe\xa2%\xee\x04h^\x0b\xa0u\xf7\x8bf\xb2\x0f@\xb5\x00\xa0\xe9\xdaW\xf3p\xf8~<\xdf5\x00\xb0j>\x01{\x91-\xa8]c\x03\xf6K'\x10Xt\xc0\xe2\xf7\x00\x00\xf2\xbbo\xc1\xd4(\x08\x03\x80h\x83\xe1\xcfw\xff\xef?\xfdG\xa0%\x00\x80fI\x92q\x00\x00^D$.T\xca\xb3?\xc7\x08\x00\x00D\xa0\x81*\xb0A\x1b\xf4\xc1\x18,\xc0\x06\x1c\xc1\x05\xdc\xc1\x0b\xfc`6\x84B$\xc4\xc2B\x10B\x0ad\x80\x1cr`)\xac\x82B(\x86\xcd\xb0\x1d*`/\xd4@\x1d4\xc0Qh\x86\x93p\x0e.\xc2U\xb8\x0e=p\x0f\xfaa\x08\x9e\xc1(\xbc\x81\x09\x04A\xc8\x08\x13a!\xda\x88\x01b\x8aX#\x8e\x08\x17\x99\x85\xf8!\xc1H\x04\x12\x8b$ \xc9\x88\x14Q\x22K\x915H1R\x8aT UH\x1d\xf2=r\x029\x87\x5cF\xba\x91;\xc8\x002\x82\xfc\x86\xbcG1\x94\x81\xb2Q=\xd4\x0c\xb5C\xb9\xa87\x1a\x84F\xa2\x0b\xd0dt1\x9a\x8f\x16\xa0\x9b\xd0r\xb4\x1a=\x8c6\xa1\xe7\xd0\xabh\x0f\xda\x8f>C\xc70\xc0\xe8\x18\x073\xc4l0.\xc6\xc3B\xb18,\x09\x93c\xcb\xb1\x22\xac\x0c\xab\xc6\x1a\xb0V\xac\x03\xbb\x89\xf5c\xcf\xb1w\x04\x12\x81E\xc0\x096\x04wB a\x1eAHXLXN\xd8H\xa8 \x1c$4\x11\xda\x097\x09\x03\x84Q\xc2'\x22\x93\xa8K\xb4&\xba\x11\xf9\xc4\x18b21\x87XH,#\xd6\x12\x8f\x13/\x10{\x88C\xc47$\x12\x89C2'\xb9\x90\x02I\xb1\xa4T\xd2\x12\xd2F\xd2nR#\xe9,\xa9\x9b4H\x1a#\x93\xc9\xdadk\xb2\x079\x94, +\xc8\x85\xe4\x9d\xe4\xc3\xe43\xe4\x1b\xe4!\xf2[\x0a\x9db@q\xa4\xf8S\xe2(R\xcajJ\x19\xe5\x10\xe54\xe5\x06e\x982AU\xa3\x9aR\xdd\xa8\xa1T\x115\x8fZB\xad\xa1\xb6R\xafQ\x87\xa8\x134u\x9a9\xcd\x83\x16IK\xa5\xad\xa2\x95\xd3\x1ah\x17h\xf7i\xaf\xe8t\xba\x11\xdd\x95\x1eN\x97\xd0W\xd2\xcb\xe9G\xe8\x97\xe8\x03\xf4w\x0c\x0d\x86\x15\x83\xc7\x88g(\x19\x9b\x18\x07\x18g\x19w\x18\xaf\x98L\xa6\x19\xd3\x8b\x19\xc7T071\xeb\x98\xe7\x99\x0f\x99oUX*\xb6*|\x15\x91\xca\x0a\x95J\x95&\x95\x1b*/T\xa9\xaa\xa6\xaa\xde\xaa\x0bU\xf3U\xcbT\x8f\xa9^S}\xaeFU3S\xe3\xa9\x09\xd4\x96\xabU\xaa\x9dP\xebS\x1bSg\xa9;\xa8\x87\xaag\xa8oT?\xa4~Y\xfd\x89\x06Y\xc3L\xc3OC\xa4Q\xa0\xb1_\xe3\xbc\xc6 \x0bc\x19\xb3x,!k\x0d\xab\x86u\x815\xc4&\xb1\xcd\xd9|v*\xbb\x98\xfd\x1d\xbb\x8b=\xaa\xa9\xa19C3J3W\xb3R\xf3\x94f?\x07\xe3\x98q\xf8\x9ctN\x09\xe7(\xa7\x97\xf3~\x8a\xde\x14\xef)\xe2)\x1b\xa64L\xb91e\x5ck\xaa\x96\x97\x96X\xabH\xabQ\xabG\xeb\xbd6\xae\xed\xa7\x9d\xa6\xbdE\xbbY\xfb\x81\x0eA\xc7J'\x5c'Gg\x8f\xce\x05\x9d\xe7S\xd9S\xdd\xa7\x0a\xa7\x16M=:\xf5\xae.\xaak\xa5\x1b\xa1\xbbDw\xbfn\xa7\xee\x98\x9e\xbe^\x80\x9eLo\xa7\xdey\xbd\xe7\xfa\x1c}/\xfdT\xfdm\xfa\xa7\xf5G\x0cX\x06\xb3\x0c$\x06\xdb\x0c\xce\x18<\xc55qo<\x1d/\xc7\xdb\xf1QC]\xc3@C\xa5a\x95a\x97\xe1\x84\x91\xb9\xd1<\xa3\xd5F\x8dF\x0f\x8ci\xc6\x5c\xe3$\xe3m\xc6m\xc6\xa3&\x06&!&KM\xeaM\xee\x9aRM\xb9\xa6)\xa6;L;L\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x995\x9b=1\xd72\xe7\x9b\xe7\x9b\xd7\x9b\xdf\xb7`ZxZ,\xb6\xa8\xb6\xb8eI\xb2\xe4Z\xa6Y\xee\xb6\xbcn\x85Z9Y\xa5XUZ]\xb3F\xad\x9d\xad%\xd6\xbb\xad\xbb\xa7\x11\xa7\xb9N\x93N\xab\x9e\xd6g\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\x19\xb0\xe5\xd8\x06\xdb\xae\xb6m\xb6}agb\x17g\xb7\xc5\xae\xc3\xee\x93\xbd\x93}\xba}\x8d\xfd=\x07\x0d\x87\xd9\x0e\xab\x1dZ\x1d~s\xb4r\x14:V:\xde\x9a\xce\x9c\xee?}\xc5\xf4\x96\xe9/gX\xcf\x10\xcf\xd83\xe3\xb6\x13\xcb)\xc4i\x9dS\x9b\xd3Gg\x17g\xb9s\x83\xf3\x88\x8b\x89K\x82\xcb.\x97>.\x9b\x1b\xc6\xdd\xc8\xbd\xe4Jt\xf5q]\xe1z\xd2\xf5\x9d\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee6\xeei\xee\x87\xdc\x9f\xcc4\x9f)\x9eY3s\xd0\xc3\xc8C\xe0Q\xe5\xd1?\x0b\x9f\x950k\xdf\xac~OCO\x81g\xb5\xe7#/c/\x91W\xad\xd7\xb0\xb7\xa5w\xaa\xf7a\xef\x17>\xf6>r\x9f\xe3>\xe3<7\xde2\xdeY_\xcc7\xc0\xb7\xc8\xb7\xcbO\xc3o\x9e_\x85\xdfC\x7f#\xffd\xffz\xff\xd1\x00\xa7\x80%\x01g\x03\x89\x81A\x81[\x02\xfb\xf8z|!\xbf\x8e?:\xdbe\xf6\xb2\xd9\xedA\x8c\xa0\xb9A\x15A\x8f\x82\xad\x82\xe5\xc1\xad!h\xc8\xec\x90\xad!\xf7\xe7\x98\xce\x91\xcei\x0e\x85P~\xe8\xd6\xd0\x07a\xe6a\x8b\xc3~\x0c'\x85\x87\x85W\x86?\x8ep\x88X\x1a\xd11\x975w\xd1\xdcCs\xdfD\xfaD\x96D\xde\x9bg1O9\xaf-J5*>\xaa.j<\xda7\xba4\xba?\xc6.fY\xcc\xd5X\x9dXIlK\x1c9.*\xae6nl\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3{\x17\x98/\xc8]py\xa1\xce\xc2\xf4\x85\xa7\x16\xa9.\x12,:\x96@L\x88N8\x94\xf0A\x10*\xa8\x16\x8c%\xf2\x13w%\x8e\x0ay\xc2\x1d\xc2g\x22/\xd16\xd1\x88\xd8C\x5c*\x1eN\xf2H*Mz\x92\xec\x91\xbc5y$\xc53\xa5,\xe5\xb9\x84'\xa9\x90\xbcL\x0dL\xdd\x9b:\x9e\x16\x9av m2=:\xbd1\x83\x92\x91\x90qB\xaa!M\x93\xb6g\xeag\xe6fv\xcb\xace\x85\xb2\xfe\xc5n\x8b\xb7/\x1e\x95\x07\xc9k\xb3\x90\xac\x05Y-\x0a\xb6B\xa6\xe8TZ(\xd7*\x07\xb2geWf\xbf\xcd\x89\xca9\x96\xab\x9e+\xcd\xed\xcc\xb3\xca\xdb\x907\x9c\xef\x9f\xff\xed\x12\xc2\x12\xe1\x92\xb6\xa5\x86KW-\x1dX\xe6\xbd\xacj9\xb2\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8(\xdcx\xe5\x1b\x87o\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9d\xcff\xd2f\xe9\xe6\xde-\x9e[\x0e\x96\xaa\x97\xe6\x97\x0en\x0d\xd9\xda\xb4\x0d\xdfV\xb4\xed\xf5\xf6E\xdb/\x97\xcd(\xdb\xbb\x83\xb6C\xb9\xa3\xbf<\xb8\xbce\xa7\xc9\xce\xcd;?T\xa4T\xf4T\xfaT6\xee\xd2\xdd\xb5a\xd7\xf8n\xd1\xee\x1b{\xbc\xf64\xec\xd5\xdb[\xbc\xf7\xfd>\xc9\xbe\xdbU\x01UM\xd5f\xd5e\xfbI\xfb\xb3\xf7?\xae\x89\xaa\xe9\xf8\x96\xfbm]\xadNmq\xed\xc7\x03\xd2\x03\xfd\x07#\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2=TR\x8f\xd6+\xebG\x0e\xc7\x1f\xbe\xfe\x9d\xefw-\x0d6\x0dU\x8d\x9c\xc6\xe2#pDy\xe4\xe9\xf7\x09\xdf\xf7\x1e\x0d:\xdav\x8c{\xac\xe1\x07\xd3\x1fv\x1dg\x1d/jB\x9a\xf2\x9aF\x9bS\x9a\xfb[b[\xbaO\xcc>\xd1\xd6\xea\xdez\xfcG\xdb\x1f\x0f\x9c499\xe2?r\xfd\xe9\xfc\xa7C\xcfd\xcf&\x9e\x17\xfe\xa2\xfe\xcb\xae\x17\x16/~\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\x93\xbfm|\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\xbe\xc9x31^\xf4V\xfb\xed\xc1w\xdcw\x1d\xef\xa3\xdf\x0fO\xe4| \x7f(\xffh\xf9\xb1\xf5S\xd0\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfcc3-\xdb\x00\x00\x00 cHRM\x00\x00z%\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17o\x92_\xc5F\x00\x00\x00yIDATx\xda\xec\x971\x0a\xc00\x0c\x03%\x93_\xf5\xfd}\x97\xb3\xb4\x10h\x07gPR\xa8$\x07\x14f&vV`s\xb5\xbb9I\x00X%\x07\x8fK\xf9Q\x81\x95^\xe4C\x817J\xd5\xd2\xca\x0dP!{\x15\x80J\xef?\xf7\x0a\x0c`\x00\x03\x18\xc0\x00\x060\x80\x01\x0c\x10\xd5\xf4\xaaJ\xc61\x13\xa1\x15\xb1\xbc\xcd\x0e(-\xe0\x22\xdb9\xee\xe2\xef\x7f\xc7\x1d\x00\x00\xff\xff\x03\x00>H\x12?\xd7\xafML\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xef\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00Q\x00\x00\x00:\x08\x06\x00\x00\x00\xc8\xbc\xb5\xaf\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b*2\xff\x7f Z\x00\x00\x00oIDATx\xda\xed\xd0\xb1\x0d\x000\x08\x03A\xc8\xa0\x0c\xc7\xa2I\xcf\x04(\xba/]Y\x97\xb1\xb4\xee\xbes\xab\xaa\xdc\xf8\xf5\x84 B\x84(\x88\x10!B\x14D\x88\x10!\x0a\x22D\x88\x10\x05\x11\x22D\x88\x82\x08\x11\x22DA\x84\x08Q\x10!B\x84(\x88\x10!B\x14D\x88\x10!\x0a\x22D\x88\x10\x05\x11\x22D\x88\x82\x08\x11\x22DA\x84\x08Q\x10!B\xfc\xaa\x07\x12U\x04tV\x9e\x9eT\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02V\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdf\x04\x19\x10\x14-\x80z\x92\xdf\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x01\xbaIDATx\xda\xed\x9b[\x92\x02!\x0cEM\x16\xa6\x1b\xd0\xd5\x8e\x1b\xd0\x8d\xe9\x9fe9\xda<\x92{\x13h\xf2=\x95\xe6\x1c\x1eC\x10\x0e\x87\x15+V\xec9\x84\xf9\xb1\xbf\xe3\xf1Q\xf3w\x97\xfb]\xa6\x10P\x0b\x1c)D\xb2B\xb3d\xc8(\xe0(\x112\x22\xbc\xa7\x04\x19\x11\xdcS\x84\x8c\x0eo\x95 \xa3\x83[E\xc8L\xf0=\x12d6\xf8V\x09\xba\xb6\xc2\x13\xf6~\xcb(\x10+\xfc\xf9v{\xe5\xb8\x9eN\x14Q\xef\xdf,}\xb7$A\xbd\x1b\xf6\xd984\xbc5\x141\xf4Q\x12z\xf2\x96\x18\x145\xef\xbd%X\xf2m\xb1\x98\xa7\xc0\xd6\xfc\xf3\x92\xb0\x95\xc7\xba\xee\x88W\xef\xa3\x1a\xe9\x99\xf7\xdb\x82\xe8\xb6\x08\x22F\x02\xb2\xe7!\xff\x05<%0\xe0\xbfN\x01\x8fM\x8f\xb5\xf1H\xf8\xcfi\x00\xd9\x0a[F\x02\xab\xe7\xe1\xb5@\x8f\x046<\xbc\x18j\x91\x10\x01\xffo\x0d@\x15=%86\xfc\xfb:@)\x87{\xd7\x04FqE;\x0fh\x85aU\x96\xd4\x03\x91Z(\x16<]@\x0d\x1c\x13>D\x80e\x1f0\xbc\x80Z8\xa6\x04\xcd\x06\xcf\x96\xa0\xd1\xf0\x8c\xf3\x84P\x015\xf0\x91\x12 \xd5`o\xcf36E\x94j\xb0\x17&b$h\xa69\x1f!A3\xc1GHp;\x14E\xcca\xef|\xd0CQ\xc4\x02\xc6\x18\x09\x9a\x15\x9e%\xe1g\x82\xdai\xc0\xaa\xe7\xad\xdf\xf9\xf5#i\xc8\x99`\x86|E\x01\x96\x9bW\xa8\xc6\xf6\xe6\xddb\xd1\xec=\x8f\xceo\xbe \x91=J#y]\x91\xa9M\xb6n\x89M\x1a\xeb\xa2dk\xf2]_\x95\xcd,\x82vY:\xa3\x84\x90\xeb\xf2Y$X\x1fM\xac'3\xde\x0d\xdb\xed\xa3)\xa4\x8c\xa1\x9e\xcdy\x08a>\x9c\x5c\xb1\xf7x\x02Q\xa0Z\x91w\xd2\x02#\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x0b\x95\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x0aOiCCPPhotoshop ICC profile\x00\x00x\xda\x9dSgTS\xe9\x16=\xf7\xde\xf4BK\x88\x80\x94KoR\x15\x08 RB\x8b\x80\x14\x91&*!\x09\x10J\x88!\xa1\xd9\x15Q\xc1\x11EE\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15Q,\x0c\x8a\x0a\xd8\x07\xe4!\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1{\xa3k\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7>\xe7\xac\xf3\x9d\xb3\xcf\x07\xc0\x08\x0c\x96H3Q5\x80\x0c\xa9B\x1e\x11\xe0\x83\xc7\xc4\xc6\xe1\xe4.@\x81\x0a$p\x00\x10\x08\xb3d!s\xfd#\x01\x00\xf8~<<+\x22\xc0\x07\xbe\x00\x01x\xd3\x0b\x08\x00\xc0M\x9b\xc00\x1c\x87\xff\x0f\xeaB\x99\x5c\x01\x80\x84\x01\xc0t\x918K\x08\x80\x14\x00@z\x8eB\xa6\x00@F\x01\x80\x9d\x98&S\x00\xa0\x04\x00`\xcbcb\xe3\x00P-\x00`'\x7f\xe6\xd3\x00\x80\x9d\xf8\x99{\x01\x00[\x94!\x15\x01\xa0\x91\x00 \x13e\x88D\x00h;\x00\xac\xcfV\x8aE\x00X0\x00\x14fK\xc49\x00\xd8-\x000IWfH\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x000Q\x88\x85)\x00\x04{\x00`\xc8##x\x00\x84\x99\x00\x14F\xf2W<\xf1+\xae\x10\xe7*\x00\x00x\x99\xb2<\xb9$9E\x81[\x08-q\x07WW.\x1e(\xceI\x17+\x146a\x02a\x9a@.\xc2y\x99\x192\x814\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\xf3\xfdx\xce\x0e\xae\xce\xce6\x8e\xb6\x0e_-\xea\xbf\x06\xff\x22bb\xe3\xfe\xe5\xcf\xabp@\x00\x00\xe1t~\xd1\xfe,/\xb3\x1a\x80;\x06\x80m\xfe\xa2%\xee\x04h^\x0b\xa0u\xf7\x8bf\xb2\x0f@\xb5\x00\xa0\xe9\xdaW\xf3p\xf8~<\xdf5\x00\xb0j>\x01{\x91-\xa8]c\x03\xf6K'\x10Xt\xc0\xe2\xf7\x00\x00\xf2\xbbo\xc1\xd4(\x08\x03\x80h\x83\xe1\xcfw\xff\xef?\xfdG\xa0%\x00\x80fI\x92q\x00\x00^D$.T\xca\xb3?\xc7\x08\x00\x00D\xa0\x81*\xb0A\x1b\xf4\xc1\x18,\xc0\x06\x1c\xc1\x05\xdc\xc1\x0b\xfc`6\x84B$\xc4\xc2B\x10B\x0ad\x80\x1cr`)\xac\x82B(\x86\xcd\xb0\x1d*`/\xd4@\x1d4\xc0Qh\x86\x93p\x0e.\xc2U\xb8\x0e=p\x0f\xfaa\x08\x9e\xc1(\xbc\x81\x09\x04A\xc8\x08\x13a!\xda\x88\x01b\x8aX#\x8e\x08\x17\x99\x85\xf8!\xc1H\x04\x12\x8b$ \xc9\x88\x14Q\x22K\x915H1R\x8aT UH\x1d\xf2=r\x029\x87\x5cF\xba\x91;\xc8\x002\x82\xfc\x86\xbcG1\x94\x81\xb2Q=\xd4\x0c\xb5C\xb9\xa87\x1a\x84F\xa2\x0b\xd0dt1\x9a\x8f\x16\xa0\x9b\xd0r\xb4\x1a=\x8c6\xa1\xe7\xd0\xabh\x0f\xda\x8f>C\xc70\xc0\xe8\x18\x073\xc4l0.\xc6\xc3B\xb18,\x09\x93c\xcb\xb1\x22\xac\x0c\xab\xc6\x1a\xb0V\xac\x03\xbb\x89\xf5c\xcf\xb1w\x04\x12\x81E\xc0\x096\x04wB a\x1eAHXLXN\xd8H\xa8 \x1c$4\x11\xda\x097\x09\x03\x84Q\xc2'\x22\x93\xa8K\xb4&\xba\x11\xf9\xc4\x18b21\x87XH,#\xd6\x12\x8f\x13/\x10{\x88C\xc47$\x12\x89C2'\xb9\x90\x02I\xb1\xa4T\xd2\x12\xd2F\xd2nR#\xe9,\xa9\x9b4H\x1a#\x93\xc9\xdadk\xb2\x079\x94, +\xc8\x85\xe4\x9d\xe4\xc3\xe43\xe4\x1b\xe4!\xf2[\x0a\x9db@q\xa4\xf8S\xe2(R\xcajJ\x19\xe5\x10\xe54\xe5\x06e\x982AU\xa3\x9aR\xdd\xa8\xa1T\x115\x8fZB\xad\xa1\xb6R\xafQ\x87\xa8\x134u\x9a9\xcd\x83\x16IK\xa5\xad\xa2\x95\xd3\x1ah\x17h\xf7i\xaf\xe8t\xba\x11\xdd\x95\x1eN\x97\xd0W\xd2\xcb\xe9G\xe8\x97\xe8\x03\xf4w\x0c\x0d\x86\x15\x83\xc7\x88g(\x19\x9b\x18\x07\x18g\x19w\x18\xaf\x98L\xa6\x19\xd3\x8b\x19\xc7T071\xeb\x98\xe7\x99\x0f\x99oUX*\xb6*|\x15\x91\xca\x0a\x95J\x95&\x95\x1b*/T\xa9\xaa\xa6\xaa\xde\xaa\x0bU\xf3U\xcbT\x8f\xa9^S}\xaeFU3S\xe3\xa9\x09\xd4\x96\xabU\xaa\x9dP\xebS\x1bSg\xa9;\xa8\x87\xaag\xa8oT?\xa4~Y\xfd\x89\x06Y\xc3L\xc3OC\xa4Q\xa0\xb1_\xe3\xbc\xc6 \x0bc\x19\xb3x,!k\x0d\xab\x86u\x815\xc4&\xb1\xcd\xd9|v*\xbb\x98\xfd\x1d\xbb\x8b=\xaa\xa9\xa19C3J3W\xb3R\xf3\x94f?\x07\xe3\x98q\xf8\x9ctN\x09\xe7(\xa7\x97\xf3~\x8a\xde\x14\xef)\xe2)\x1b\xa64L\xb91e\x5ck\xaa\x96\x97\x96X\xabH\xabQ\xabG\xeb\xbd6\xae\xed\xa7\x9d\xa6\xbdE\xbbY\xfb\x81\x0eA\xc7J'\x5c'Gg\x8f\xce\x05\x9d\xe7S\xd9S\xdd\xa7\x0a\xa7\x16M=:\xf5\xae.\xaak\xa5\x1b\xa1\xbbDw\xbfn\xa7\xee\x98\x9e\xbe^\x80\x9eLo\xa7\xdey\xbd\xe7\xfa\x1c}/\xfdT\xfdm\xfa\xa7\xf5G\x0cX\x06\xb3\x0c$\x06\xdb\x0c\xce\x18<\xc55qo<\x1d/\xc7\xdb\xf1QC]\xc3@C\xa5a\x95a\x97\xe1\x84\x91\xb9\xd1<\xa3\xd5F\x8dF\x0f\x8ci\xc6\x5c\xe3$\xe3m\xc6m\xc6\xa3&\x06&!&KM\xeaM\xee\x9aRM\xb9\xa6)\xa6;L;L\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x995\x9b=1\xd72\xe7\x9b\xe7\x9b\xd7\x9b\xdf\xb7`ZxZ,\xb6\xa8\xb6\xb8eI\xb2\xe4Z\xa6Y\xee\xb6\xbcn\x85Z9Y\xa5XUZ]\xb3F\xad\x9d\xad%\xd6\xbb\xad\xbb\xa7\x11\xa7\xb9N\x93N\xab\x9e\xd6g\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\x19\xb0\xe5\xd8\x06\xdb\xae\xb6m\xb6}agb\x17g\xb7\xc5\xae\xc3\xee\x93\xbd\x93}\xba}\x8d\xfd=\x07\x0d\x87\xd9\x0e\xab\x1dZ\x1d~s\xb4r\x14:V:\xde\x9a\xce\x9c\xee?}\xc5\xf4\x96\xe9/gX\xcf\x10\xcf\xd83\xe3\xb6\x13\xcb)\xc4i\x9dS\x9b\xd3Gg\x17g\xb9s\x83\xf3\x88\x8b\x89K\x82\xcb.\x97>.\x9b\x1b\xc6\xdd\xc8\xbd\xe4Jt\xf5q]\xe1z\xd2\xf5\x9d\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee6\xeei\xee\x87\xdc\x9f\xcc4\x9f)\x9eY3s\xd0\xc3\xc8C\xe0Q\xe5\xd1?\x0b\x9f\x950k\xdf\xac~OCO\x81g\xb5\xe7#/c/\x91W\xad\xd7\xb0\xb7\xa5w\xaa\xf7a\xef\x17>\xf6>r\x9f\xe3>\xe3<7\xde2\xdeY_\xcc7\xc0\xb7\xc8\xb7\xcbO\xc3o\x9e_\x85\xdfC\x7f#\xffd\xffz\xff\xd1\x00\xa7\x80%\x01g\x03\x89\x81A\x81[\x02\xfb\xf8z|!\xbf\x8e?:\xdbe\xf6\xb2\xd9\xedA\x8c\xa0\xb9A\x15A\x8f\x82\xad\x82\xe5\xc1\xad!h\xc8\xec\x90\xad!\xf7\xe7\x98\xce\x91\xcei\x0e\x85P~\xe8\xd6\xd0\x07a\xe6a\x8b\xc3~\x0c'\x85\x87\x85W\x86?\x8ep\x88X\x1a\xd11\x975w\xd1\xdcCs\xdfD\xfaD\x96D\xde\x9bg1O9\xaf-J5*>\xaa.j<\xda7\xba4\xba?\xc6.fY\xcc\xd5X\x9dXIlK\x1c9.*\xae6nl\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3{\x17\x98/\xc8]py\xa1\xce\xc2\xf4\x85\xa7\x16\xa9.\x12,:\x96@L\x88N8\x94\xf0A\x10*\xa8\x16\x8c%\xf2\x13w%\x8e\x0ay\xc2\x1d\xc2g\x22/\xd16\xd1\x88\xd8C\x5c*\x1eN\xf2H*Mz\x92\xec\x91\xbc5y$\xc53\xa5,\xe5\xb9\x84'\xa9\x90\xbcL\x0dL\xdd\x9b:\x9e\x16\x9av m2=:\xbd1\x83\x92\x91\x90qB\xaa!M\x93\xb6g\xeag\xe6fv\xcb\xace\x85\xb2\xfe\xc5n\x8b\xb7/\x1e\x95\x07\xc9k\xb3\x90\xac\x05Y-\x0a\xb6B\xa6\xe8TZ(\xd7*\x07\xb2geWf\xbf\xcd\x89\xca9\x96\xab\x9e+\xcd\xed\xcc\xb3\xca\xdb\x907\x9c\xef\x9f\xff\xed\x12\xc2\x12\xe1\x92\xb6\xa5\x86KW-\x1dX\xe6\xbd\xacj9\xb2\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8(\xdcx\xe5\x1b\x87o\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9d\xcff\xd2f\xe9\xe6\xde-\x9e[\x0e\x96\xaa\x97\xe6\x97\x0en\x0d\xd9\xda\xb4\x0d\xdfV\xb4\xed\xf5\xf6E\xdb/\x97\xcd(\xdb\xbb\x83\xb6C\xb9\xa3\xbf<\xb8\xbce\xa7\xc9\xce\xcd;?T\xa4T\xf4T\xfaT6\xee\xd2\xdd\xb5a\xd7\xf8n\xd1\xee\x1b{\xbc\xf64\xec\xd5\xdb[\xbc\xf7\xfd>\xc9\xbe\xdbU\x01UM\xd5f\xd5e\xfbI\xfb\xb3\xf7?\xae\x89\xaa\xe9\xf8\x96\xfbm]\xadNmq\xed\xc7\x03\xd2\x03\xfd\x07#\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2=TR\x8f\xd6+\xebG\x0e\xc7\x1f\xbe\xfe\x9d\xefw-\x0d6\x0dU\x8d\x9c\xc6\xe2#pDy\xe4\xe9\xf7\x09\xdf\xf7\x1e\x0d:\xdav\x8c{\xac\xe1\x07\xd3\x1fv\x1dg\x1d/jB\x9a\xf2\x9aF\x9bS\x9a\xfb[b[\xbaO\xcc>\xd1\xd6\xea\xdez\xfcG\xdb\x1f\x0f\x9c499\xe2?r\xfd\xe9\xfc\xa7C\xcfd\xcf&\x9e\x17\xfe\xa2\xfe\xcb\xae\x17\x16/~\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\x93\xbfm|\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\xbe\xc9x31^\xf4V\xfb\xed\xc1w\xdcw\x1d\xef\xa3\xdf\x0fO\xe4| \x7f(\xffh\xf9\xb1\xf5S\xd0\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfcc3-\xdb\x00\x00\x00 cHRM\x00\x00z%\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17o\x92_\xc5F\x00\x00\x00\xc0IDATx\xda\xdcVA\x0e\x830\x0c\xab\xa3\xfe\xff=\xfb\x9dw\x021ThJ\xe2 -'\xd4\x03vl'-\xda\xa7\x1d\x8b\xad\xa6\xb0}\xf4b\xe0s\xa3\xb0\x17\xc0\x7f\x88\xf4\x99D\xa2\xce\xf7\xb2B\xf0\xe1\xbfM\x0c\xceA\xd7\x98)\xa0\x90\xfb2gV\xe5\xf5\x85\x1aR\x05\x5ceE\xdd\xbbCX\x0a\xfew\x16,w\x9fI\xe0\x11x\x16\x01*-`\x10\x00\x11\x02\x9eM\xc6\x08\xf8\x1d\x01:\xce\xa8\x9a\x02&\xf8\x8d\x08\x01\x04s\x81\x8c\x10*\xdf\x04\xee\x10B\x91\xfa\xd51\x84\x12\xdc\xbb\x88\xf0\x96\x05+$\xa0&p\x07\x82\x0a\x05dv\xf4\xc1\x8cCL\x82\x91M\x98~sv\xc5\x15\xbb\x9a\x81\xb2\xad7\xb2\xd3\xaaW\xef9K\xdf\x01\x00h\x95#\xfe/d\x9d\xea\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa6\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1d\x00\xb0\xd55\xa3\x00\x00\x00*IDAT\x08\xd7c`\xc0\x06\xfe\x9fg``B0\xa1\x1c\x08\x93\x81\x81\x09\xc1d``b``4D\xe2 s\x19\x90\x8d@\x02\x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\x96\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\x00\x00\x00\x02bKGD\x00\xd3\xb5W\xa0\x5c\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x0b\x07\x0c\x0d\x1bu\xfe1\x99\x00\x00\x00'IDAT\x08\xd7e\x8c\xb1\x0d\x00\x00\x08\x83\xe0\xff\xa3up\xb1\xca\xd4\x90Px\x08U!\x14\xb6Tp\xe6H\x8d\x87\xcc\x0f\x0d\xe0\xf0\x08\x024\xe2+\xa7\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa0\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa5\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xbb\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00?\x00\x00\x00\x07\x08\x06\x00\x00\x00\xbfv\x95\x1f\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x095+U\xcaRj\x00\x00\x00;IDAT8\xcbc`\x18\x05#\x130\x12\xa3\xa8\xbe}*%v\xfc\xa7\x97;\xd1\xc1\xaa\xa5s\x18\xae_9\x8fS\x9ei4\xe6\x09\x00M\x1d\xc3!\x19\xf3\x0c\x0c\x0cxc~\x14\x8cT\x00\x00id\x0b\x05\xfdkX\xca\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xe4\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x006\x00\x00\x00\x0a\x08\x06\x00\x00\x00\xff\xfd\xad\x0b\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x06bKGD\x00\x7f\x00\x87\x00\x95\xe6\xde\xa6\xaf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x09*+\x98\x90\x5c\xf4\x00\x00\x00dIDATH\xc7c\xfc\xcf0<\x01\x0b\xa5\x064\xb4O\x85\x87\xcd\xaa\xa5s\x18\xae]9\xcfH+5\x14y\xcc\xd8\xc8\x88$\x03|\x89\xd0O-5\x84\xc0\xd9s\xe7\xe0l&\x86\x91\x92\x14\x91}MTR\x0cM&\xa8\x9fZjF\x93\xe2hR\x1c\x82I\x91\x91\xd2zLK\xc7\x10\xc5\x08l\xc54\xb5\xd4\xd0\xd5c\x83\x15\x00\x00z0J\x09q\xea-n\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xe0\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00Q\x00\x00\x00:\x08\x06\x00\x00\x00\xc8\xbc\xb5\xaf\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b)\x1c\x08\x84~V\x00\x00\x00`IDATx\xda\xed\xd9\xb1\x0d\x00 \x08\x00AqP\x86cQ\xed\x8d\x85%\x89w\xa5\x15\xf9HE\x8c\xa6\xaaj\x9do\x99\x19\x1dg\x9d\x03\x11E\x14\x11\x11E\x14QDD\x14QD\x11\x11QD\x11EDD\x11E\x14\x11\x11E\x14\xf1[\xd1u\xb0\xdb\xdd\xd9O\xb4\xce\x88(\x22\x00\x00\x00\x00\x00\x00\x00\x00\x00\xcf6\xcei\x07\x1e\xe99U@\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02\xd4\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x02QIDATX\x85\xed\x96AKTQ\x14\xc7\x7f\xe7\x8d\xb8\xd0&0wi\x84\xe1\xaa)\x90A\xc7\x92^\xa0\x1b\xa1\x8d\x0a\xf5\x19Z;3\xda\xd8j\x16A6\x83\xf3\xbe\x87A\x8d\xad\xc2M\xf6\x14\xf4\x0d\x99H\x0e\x11\xe2\xaa\x11\xdb\x184\xa8\x0b\xc3wZ\xccH\x10\xf3t\xee\xe8\xae\xf9o\xef9\xfc\x7f\xf7\xdc{\xcf=\xd0TS\xff\xbb\xc4$8\x92.\xb6v\x86\x0f'T\x18\x07\x8d\x02]\xd5\xa5\x12\xcag\x11\xc9\xef\x97\xdb\xf3\xc5t\xe4\xf8\xd2\x01lg\xed1*\x19\xa0\x07\xe4\x0b\xaaKX\x94\x00D\xb5K\xb1\x86A\xef\x22\xec\x082\xedN\xc6\xde\x5c\x0a\xc0\x93\xf9\xf9\xd0\x8f\xdd\x9b\x19\x948\xf0^\x95\xd4Jbp\xb3V\xec\x90S\xe8\x0b\xf9:\x8b0\x0ad\x97\xcb\xb1\x14i\xf1\xeb\xdddM\xd9\x8e7g\xe7\xbc\x93\x87\xceZ\xb2\xee\x9c\x9c7e\xe7\xbc\x13;\xe7e\xce\x8b=\xb3\x02\xd5\xb2\xbf\x16$\xe9\xc6cs\xf5\x02Tr\xbdi\x94W\x08\x13\xcb\x93\x83yc\x80H\xba\xd8z\xed\xea\xc1WA\xbf\xb9\xf1{\x8fL\xccO\xf5\xc0),\x8aj\xcf\xcf\xf2\x95H\xd0\xc5\xb4\x82\x92;\xc3\x87\x13\xc0-_e\xa6\x11s\x00\xcb\x97g@oG\xf8`,0&h\xa1\xf2\xd4\xd8\x0c\xbap\xf5\xc8M\x0cl\xa8\xb2%`\x0e\x00\x1a\x15\xf4c\xa3\xe6\xa7\x12\xf8\x80\xd0\xdf\x00\x00\xd7\x15)]\x14@a\x97\xbf\x0d\xcb\x08\x00\xc4\xacS\xd64\x10\x11 \xb0\x17\x9c\x05\xb0\x87O\xf7E\x01\x14\xed\x02\xf6\xcc\x01\x94O\x0a\xc3\x17\x05\x00F\x80\x821\x80\x88\xe4E\xb83\xe4\x14\xfa\x1au\xb6\x9d\xd5(p\x1b\xd1w\xc6\x00\xfb\xe5\xf6<\xc2N\xc8\xd7\xd9\x86\xdcU\x05\xb52\xc0\xf6Q[\xcb\x821@1\x1d9Ve\x0aa\xd4\xceyS\xa6\xfev\xceK\x01#\xa2~r\xfdi\xffoc\x00\x80\x95\xf8\xe0[ \x0b\xcc\xd6\x0d\xa1*\xf6\xdc\xda\x0c\x22/D\xc8\xb8\x89\xfb\x81\xe5\x87z\xe6\x81\xb4Zv\xb8\xf0\x12a\x1aX\x14\xb5Rnb`\xa3V\xa8\xed\xacF\xabe\x1f\x11!\xe3\xfe\x8a=?\xef;6\x18H\xbcq\x94,\xd0\xab\xca\x96\x08K\x08\xdf\x01PnPy1\x11`[\xd4O\x9e\xb7sc\x00\xa8\xfc\x90\x1d\xe1\x831\xaa#\x99 \xdd\x15\x7f-\x89\xca:\x96\xe6\x8f\xdaZ\x16\xce:\xf3\xa6\x9aj\xea_\xfd\x01\xd3\x1c\xd9\x7f^\xb93\xcd\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\x93\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\x00\x00\x00\x02bKGD\x00\xd3\xb5W\xa0\x5c\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x0b\x07\x0c\x0c+J<0t\x00\x00\x00$IDAT\x08\xd7c`@\x05\xff\xff\xc3XL\xc8\x5c&dY&d\xc5p\x0e##\x9c\xc3\xc8\x88a\x1a\x0a\x00\x00\x9e\x14\x0a\x05+\xca\xe5u\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa6\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x1b\x0e\x16M[o\x00\x00\x00*IDAT\x08\xd7c`\xc0\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\x81\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x01\x03\x00\x00\x00%=m\x22\x00\x00\x00\x06PLTE\x00\x00\x00\xae\xae\xaewk\xd6-\x00\x00\x00\x01tRNS\x00@\xe6\xd8f\x00\x00\x00)IDATx^\x05\xc0\xb1\x0d\x00 \x08\x04\xc0\xc3X\xd8\xfe\x0a\xcc\xc2p\x8cm(\x0e\x97Gh\x86Uq\xda\x1do%\xba\xcd\xd8\xfd5\x0a\x04\x1b\xd6\xd9\x1a\x92\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xdc\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00@\x08\x06\x00\x00\x00\x13}\xf7\x96\x00\x00\x00\x06bKGD\x00\xb3\x00y\x00y\xdc\xddS\xfc\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdf\x04\x19\x10-\x19\xafJ\xeb\xd0\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x00@IDATX\xc3\xed\xce1\x0a\x00 \x0c\x03@\xf5\xa3}[_\xaaS\xc1\xc9\xc5E\xe42\x05\x1a\x8e\xb6v\x99^%\x22f\xf5\xcc\xec\xfb\xe8t\x1b\xb7\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf06\xf0A\x16\x0bB\x08x\x15WD\xa2\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x0bN\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x0aOiCCPPhotoshop ICC profile\x00\x00x\xda\x9dSgTS\xe9\x16=\xf7\xde\xf4BK\x88\x80\x94KoR\x15\x08 RB\x8b\x80\x14\x91&*!\x09\x10J\x88!\xa1\xd9\x15Q\xc1\x11EE\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15Q,\x0c\x8a\x0a\xd8\x07\xe4!\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1{\xa3k\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7>\xe7\xac\xf3\x9d\xb3\xcf\x07\xc0\x08\x0c\x96H3Q5\x80\x0c\xa9B\x1e\x11\xe0\x83\xc7\xc4\xc6\xe1\xe4.@\x81\x0a$p\x00\x10\x08\xb3d!s\xfd#\x01\x00\xf8~<<+\x22\xc0\x07\xbe\x00\x01x\xd3\x0b\x08\x00\xc0M\x9b\xc00\x1c\x87\xff\x0f\xeaB\x99\x5c\x01\x80\x84\x01\xc0t\x918K\x08\x80\x14\x00@z\x8eB\xa6\x00@F\x01\x80\x9d\x98&S\x00\xa0\x04\x00`\xcbcb\xe3\x00P-\x00`'\x7f\xe6\xd3\x00\x80\x9d\xf8\x99{\x01\x00[\x94!\x15\x01\xa0\x91\x00 \x13e\x88D\x00h;\x00\xac\xcfV\x8aE\x00X0\x00\x14fK\xc49\x00\xd8-\x000IWfH\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x000Q\x88\x85)\x00\x04{\x00`\xc8##x\x00\x84\x99\x00\x14F\xf2W<\xf1+\xae\x10\xe7*\x00\x00x\x99\xb2<\xb9$9E\x81[\x08-q\x07WW.\x1e(\xceI\x17+\x146a\x02a\x9a@.\xc2y\x99\x192\x814\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\xf3\xfdx\xce\x0e\xae\xce\xce6\x8e\xb6\x0e_-\xea\xbf\x06\xff\x22bb\xe3\xfe\xe5\xcf\xabp@\x00\x00\xe1t~\xd1\xfe,/\xb3\x1a\x80;\x06\x80m\xfe\xa2%\xee\x04h^\x0b\xa0u\xf7\x8bf\xb2\x0f@\xb5\x00\xa0\xe9\xdaW\xf3p\xf8~<\xdf5\x00\xb0j>\x01{\x91-\xa8]c\x03\xf6K'\x10Xt\xc0\xe2\xf7\x00\x00\xf2\xbbo\xc1\xd4(\x08\x03\x80h\x83\xe1\xcfw\xff\xef?\xfdG\xa0%\x00\x80fI\x92q\x00\x00^D$.T\xca\xb3?\xc7\x08\x00\x00D\xa0\x81*\xb0A\x1b\xf4\xc1\x18,\xc0\x06\x1c\xc1\x05\xdc\xc1\x0b\xfc`6\x84B$\xc4\xc2B\x10B\x0ad\x80\x1cr`)\xac\x82B(\x86\xcd\xb0\x1d*`/\xd4@\x1d4\xc0Qh\x86\x93p\x0e.\xc2U\xb8\x0e=p\x0f\xfaa\x08\x9e\xc1(\xbc\x81\x09\x04A\xc8\x08\x13a!\xda\x88\x01b\x8aX#\x8e\x08\x17\x99\x85\xf8!\xc1H\x04\x12\x8b$ \xc9\x88\x14Q\x22K\x915H1R\x8aT UH\x1d\xf2=r\x029\x87\x5cF\xba\x91;\xc8\x002\x82\xfc\x86\xbcG1\x94\x81\xb2Q=\xd4\x0c\xb5C\xb9\xa87\x1a\x84F\xa2\x0b\xd0dt1\x9a\x8f\x16\xa0\x9b\xd0r\xb4\x1a=\x8c6\xa1\xe7\xd0\xabh\x0f\xda\x8f>C\xc70\xc0\xe8\x18\x073\xc4l0.\xc6\xc3B\xb18,\x09\x93c\xcb\xb1\x22\xac\x0c\xab\xc6\x1a\xb0V\xac\x03\xbb\x89\xf5c\xcf\xb1w\x04\x12\x81E\xc0\x096\x04wB a\x1eAHXLXN\xd8H\xa8 \x1c$4\x11\xda\x097\x09\x03\x84Q\xc2'\x22\x93\xa8K\xb4&\xba\x11\xf9\xc4\x18b21\x87XH,#\xd6\x12\x8f\x13/\x10{\x88C\xc47$\x12\x89C2'\xb9\x90\x02I\xb1\xa4T\xd2\x12\xd2F\xd2nR#\xe9,\xa9\x9b4H\x1a#\x93\xc9\xdadk\xb2\x079\x94, +\xc8\x85\xe4\x9d\xe4\xc3\xe43\xe4\x1b\xe4!\xf2[\x0a\x9db@q\xa4\xf8S\xe2(R\xcajJ\x19\xe5\x10\xe54\xe5\x06e\x982AU\xa3\x9aR\xdd\xa8\xa1T\x115\x8fZB\xad\xa1\xb6R\xafQ\x87\xa8\x134u\x9a9\xcd\x83\x16IK\xa5\xad\xa2\x95\xd3\x1ah\x17h\xf7i\xaf\xe8t\xba\x11\xdd\x95\x1eN\x97\xd0W\xd2\xcb\xe9G\xe8\x97\xe8\x03\xf4w\x0c\x0d\x86\x15\x83\xc7\x88g(\x19\x9b\x18\x07\x18g\x19w\x18\xaf\x98L\xa6\x19\xd3\x8b\x19\xc7T071\xeb\x98\xe7\x99\x0f\x99oUX*\xb6*|\x15\x91\xca\x0a\x95J\x95&\x95\x1b*/T\xa9\xaa\xa6\xaa\xde\xaa\x0bU\xf3U\xcbT\x8f\xa9^S}\xaeFU3S\xe3\xa9\x09\xd4\x96\xabU\xaa\x9dP\xebS\x1bSg\xa9;\xa8\x87\xaag\xa8oT?\xa4~Y\xfd\x89\x06Y\xc3L\xc3OC\xa4Q\xa0\xb1_\xe3\xbc\xc6 \x0bc\x19\xb3x,!k\x0d\xab\x86u\x815\xc4&\xb1\xcd\xd9|v*\xbb\x98\xfd\x1d\xbb\x8b=\xaa\xa9\xa19C3J3W\xb3R\xf3\x94f?\x07\xe3\x98q\xf8\x9ctN\x09\xe7(\xa7\x97\xf3~\x8a\xde\x14\xef)\xe2)\x1b\xa64L\xb91e\x5ck\xaa\x96\x97\x96X\xabH\xabQ\xabG\xeb\xbd6\xae\xed\xa7\x9d\xa6\xbdE\xbbY\xfb\x81\x0eA\xc7J'\x5c'Gg\x8f\xce\x05\x9d\xe7S\xd9S\xdd\xa7\x0a\xa7\x16M=:\xf5\xae.\xaak\xa5\x1b\xa1\xbbDw\xbfn\xa7\xee\x98\x9e\xbe^\x80\x9eLo\xa7\xdey\xbd\xe7\xfa\x1c}/\xfdT\xfdm\xfa\xa7\xf5G\x0cX\x06\xb3\x0c$\x06\xdb\x0c\xce\x18<\xc55qo<\x1d/\xc7\xdb\xf1QC]\xc3@C\xa5a\x95a\x97\xe1\x84\x91\xb9\xd1<\xa3\xd5F\x8dF\x0f\x8ci\xc6\x5c\xe3$\xe3m\xc6m\xc6\xa3&\x06&!&KM\xeaM\xee\x9aRM\xb9\xa6)\xa6;L;L\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x995\x9b=1\xd72\xe7\x9b\xe7\x9b\xd7\x9b\xdf\xb7`ZxZ,\xb6\xa8\xb6\xb8eI\xb2\xe4Z\xa6Y\xee\xb6\xbcn\x85Z9Y\xa5XUZ]\xb3F\xad\x9d\xad%\xd6\xbb\xad\xbb\xa7\x11\xa7\xb9N\x93N\xab\x9e\xd6g\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\x19\xb0\xe5\xd8\x06\xdb\xae\xb6m\xb6}agb\x17g\xb7\xc5\xae\xc3\xee\x93\xbd\x93}\xba}\x8d\xfd=\x07\x0d\x87\xd9\x0e\xab\x1dZ\x1d~s\xb4r\x14:V:\xde\x9a\xce\x9c\xee?}\xc5\xf4\x96\xe9/gX\xcf\x10\xcf\xd83\xe3\xb6\x13\xcb)\xc4i\x9dS\x9b\xd3Gg\x17g\xb9s\x83\xf3\x88\x8b\x89K\x82\xcb.\x97>.\x9b\x1b\xc6\xdd\xc8\xbd\xe4Jt\xf5q]\xe1z\xd2\xf5\x9d\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee6\xeei\xee\x87\xdc\x9f\xcc4\x9f)\x9eY3s\xd0\xc3\xc8C\xe0Q\xe5\xd1?\x0b\x9f\x950k\xdf\xac~OCO\x81g\xb5\xe7#/c/\x91W\xad\xd7\xb0\xb7\xa5w\xaa\xf7a\xef\x17>\xf6>r\x9f\xe3>\xe3<7\xde2\xdeY_\xcc7\xc0\xb7\xc8\xb7\xcbO\xc3o\x9e_\x85\xdfC\x7f#\xffd\xffz\xff\xd1\x00\xa7\x80%\x01g\x03\x89\x81A\x81[\x02\xfb\xf8z|!\xbf\x8e?:\xdbe\xf6\xb2\xd9\xedA\x8c\xa0\xb9A\x15A\x8f\x82\xad\x82\xe5\xc1\xad!h\xc8\xec\x90\xad!\xf7\xe7\x98\xce\x91\xcei\x0e\x85P~\xe8\xd6\xd0\x07a\xe6a\x8b\xc3~\x0c'\x85\x87\x85W\x86?\x8ep\x88X\x1a\xd11\x975w\xd1\xdcCs\xdfD\xfaD\x96D\xde\x9bg1O9\xaf-J5*>\xaa.j<\xda7\xba4\xba?\xc6.fY\xcc\xd5X\x9dXIlK\x1c9.*\xae6nl\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3{\x17\x98/\xc8]py\xa1\xce\xc2\xf4\x85\xa7\x16\xa9.\x12,:\x96@L\x88N8\x94\xf0A\x10*\xa8\x16\x8c%\xf2\x13w%\x8e\x0ay\xc2\x1d\xc2g\x22/\xd16\xd1\x88\xd8C\x5c*\x1eN\xf2H*Mz\x92\xec\x91\xbc5y$\xc53\xa5,\xe5\xb9\x84'\xa9\x90\xbcL\x0dL\xdd\x9b:\x9e\x16\x9av m2=:\xbd1\x83\x92\x91\x90qB\xaa!M\x93\xb6g\xeag\xe6fv\xcb\xace\x85\xb2\xfe\xc5n\x8b\xb7/\x1e\x95\x07\xc9k\xb3\x90\xac\x05Y-\x0a\xb6B\xa6\xe8TZ(\xd7*\x07\xb2geWf\xbf\xcd\x89\xca9\x96\xab\x9e+\xcd\xed\xcc\xb3\xca\xdb\x907\x9c\xef\x9f\xff\xed\x12\xc2\x12\xe1\x92\xb6\xa5\x86KW-\x1dX\xe6\xbd\xacj9\xb2\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8(\xdcx\xe5\x1b\x87o\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9d\xcff\xd2f\xe9\xe6\xde-\x9e[\x0e\x96\xaa\x97\xe6\x97\x0en\x0d\xd9\xda\xb4\x0d\xdfV\xb4\xed\xf5\xf6E\xdb/\x97\xcd(\xdb\xbb\x83\xb6C\xb9\xa3\xbf<\xb8\xbce\xa7\xc9\xce\xcd;?T\xa4T\xf4T\xfaT6\xee\xd2\xdd\xb5a\xd7\xf8n\xd1\xee\x1b{\xbc\xf64\xec\xd5\xdb[\xbc\xf7\xfd>\xc9\xbe\xdbU\x01UM\xd5f\xd5e\xfbI\xfb\xb3\xf7?\xae\x89\xaa\xe9\xf8\x96\xfbm]\xadNmq\xed\xc7\x03\xd2\x03\xfd\x07#\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2=TR\x8f\xd6+\xebG\x0e\xc7\x1f\xbe\xfe\x9d\xefw-\x0d6\x0dU\x8d\x9c\xc6\xe2#pDy\xe4\xe9\xf7\x09\xdf\xf7\x1e\x0d:\xdav\x8c{\xac\xe1\x07\xd3\x1fv\x1dg\x1d/jB\x9a\xf2\x9aF\x9bS\x9a\xfb[b[\xbaO\xcc>\xd1\xd6\xea\xdez\xfcG\xdb\x1f\x0f\x9c499\xe2?r\xfd\xe9\xfc\xa7C\xcfd\xcf&\x9e\x17\xfe\xa2\xfe\xcb\xae\x17\x16/~\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\x93\xbfm|\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\xbe\xc9x31^\xf4V\xfb\xed\xc1w\xdcw\x1d\xef\xa3\xdf\x0fO\xe4| \x7f(\xffh\xf9\xb1\xf5S\xd0\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfcc3-\xdb\x00\x00\x00 cHRM\x00\x00z%\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17o\x92_\xc5F\x00\x00\x00yIDATx\xda\xec\x971\x0a\xc00\x0c\x03%\x93_\xf5\xfd}\x97\xb3\xb4\x10h\x07gPR\xa8$\x07\x14f&vV`s\xb5\xbb9I\x00X%\x07\x8fK\xf9Q\x81\x95^\xe4C\x817J\xd5\xd2\xca\x0dP!{\x15\x80J\xef?\xf7\x0a\x0c`\x00\x03\x18\xc0\x00\x060\x80\x01\x0c\x10\xd5\xf4\xaaJ\xc61\x13\xa1\x15\xb1\xbc\xcd\x0e(-\xe0\x22\xdb9\xee\xe2\xef\x7f\xc7\x1d\x00\x00\xff\xff\x03\x00>H\x12?\xd7\xafML\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02V\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdf\x04\x19\x10\x15\x00\xdc\xbe\xff\xeb\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x01\xbaIDATx\xda\xed\x9b[\x92\x02!\x0cEM\xd67.H\x17\xa0\x0b\xd2\xfd\xe9\x9fe9\xda<\x92{\x13h\xf2=\x95\xe6\x1c\x1eC\x10\x0e\x87\x15+V\xec9\x84\xf9\xb1\xdb\xe9\xf4\xa8\xf9\xbb\xe3\xf5*S\x08\xa8\x05\x8e\x14\x22Y\xa1Y2d\x14p\x94\x08\x19\x11\xdeS\x82\x8c\x08\xee)BF\x87\xb7J\x90\xd1\xc1\xad\x22d&\xf8\x1e\x092\x1b|\xab\x04][\xe1\x09{\xbfe\x14\x88\x15\xfe\xefry\xe5\xb8\x9f\xcf\x14Q\xef\xdf,}\xb7$A\xbd\x1b\xf6\xd984\xbc5\x141\xf4Q\x12z\xf2\x96\x18\x145\xef\xbd%X\xf2m\xb1\x98\xa7\xc0\xd6\xfc\xf3\x92\xb0\x95\xc7\xba\xee\x88W\xef\xa3\x1a\xe9\x99\xf7\xdb\x82\xe8\xb6\x08\x22F\x02\xb2\xe7!\xff\x05<%0\xe0\xbfN\x01\x8fM\x8f\xb5\xf1H\xf8\xcfi\x00\xd9\x0a[F\x02\xab\xe7\xe1\xb5@\x8f\x046<\xbc\x18j\x91\x10\x01\xffo\x0d@\x15=%86\xfc\xfb:@)\x87{\xd7\x04FqE;\x0fh\x85aU\x96\xd4\x03\x91Z(\x16<]@\x0d\x1c\x13>D\x80e\x1f0\xbc\x80Z8\xa6\x04\xcd\x06\xcf\x96\xa0\xd1\xf0\x8c\xf3\x84P\x015\xf0\x91\x12 \xd5`o\xcf36E\x94j\xb0\x17&b$h\xa69\x1f!A3\xc1GHp;\x14E\xcca\xef|\xd0CQ\xc4\x02\xc6\x18\x09\x9a\x15\x9e%\xe1g\x82\xdai\xc0\xaa\xe7\xad\xdf\xf9\xf5#i\xc8\x99`\x86|E\x01\x96\x9bW\xa8\xc6\xf6\xe6\xddb\xd1\xec=\x8f\xceo\xbe \x91=J#y]\x91\xa9M\xb6n\x89M\x1a\xeb\xa2dk\xf2]_\x95\xcd,\x82vY:\xa3\x84\x90\xeb\xf2Y$X\x1fM\xac'3\xde\x0d\xdb\xed\xa3)\xa4\x8c\xa1\x9e\xcdy\x08a>\x9c\x5c\xb1\xf7x\x02G\xb0[\x07:D>\x01\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa0\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfcR+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x03\xa5\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x03\x22IDATX\x85\xed\x96MlTU\x14\xc7\x7f\xe7\x0d\xa9\x09\xcc\x90Pv\xb6\xc6``\xe3\xa3\x864\xf4\xc3\xc6g\xa4\x1b\xa2\x98@\x13]\xc9\x1a6\xda\x84~Y\x5c\xcd\xce:\xa43\x09\xcb\xaee\x83\x89\x19L\x04\xc3\xc6:\x98\xb4o\x22bK'\xc64\xac\x9c\x067\x94t\x98\x92P:\xef\xef\xe2M\xa75\x99\xe9\xccCv\xf4\xbf\xba\xe7\xbds\xef\xf9\xdds\xee\x17\xeciO\xaf\xba,\x8a\xb3\x9b,\xb4\x1dN\xac\x0f\xc98\x07\xea\x06:\xaa\xbf\x8a\x88\xdf\xcd,\xfb\xa8t [H\xba\x1b/\x1d\xc0\xcb\xcc\x7f\x82,\x05\x1c\x01\xbb\x8f4\x8bC\x11\xc0\xa4\x0e\xe1\x9c\x02ua<0l\x22w\xa9\xf7\xfb\x97\x02\xf0\xe9\xf5\xeb\xb1\x7fV\xdeL!F\x80\x9f$&\x7f\x1d\xed[\xa8\xe7;\x90\xc9\x9f\x88\x05\x9a\xc28\x0d\x5c\xb9S\xea\x9d$iA\xab\x93\xac+/\xe3O{i\xbf\xf2~f~\xac\xe5>i\x7f\xdcK\xfb\x15/\xed\xa7\x9a\xf9\xee\x9a\x81j\xda\xbf3l,7\xd2;\x0d\xf0\xe1\xd5\xe5\xd7\x9e<\x7f|\xd1\xe03Y\xd0\x15\x0eb\x8b\x18\xd7\xe2\xb1\xf6\x99[\xc3\xc7\x9eU\xc1'\x10\xdf`\x0c\xdd\xb9\xd4\x97\x8d\x0c\xe0&\x0bm\xed\x07\xcb\x7f\x1a\xfa+7\xd2\xff\x11\xc0\x07W\xe7;+\x9b\xceMP\x17X\x00r\xaa\xc3\x84mc1\x16\xd3\x99\xd9\xe1\xfe\x22\xc0{\x99\xfcm\x93\x8e\xac\x96\xe2n\xa3\x85\xe94\x028\x9cX\x1f\x02\xde\x0ad\x97\xb7f^\xd9tnb:\x1ezhG\xdfZ\xbb\xab\xb2\xc9\x8fn\xb2\xd0\x06\xe0\x04\xf6%p\xf4P\xa2|\xb6Q\x9c\x86\x00\xe1Vcak\xc1\x95+\xab\x17@]h\x97\xb2\x09\x03{\xa7\xfd`\xf9\x02@n\xb4\xe7\x9e\xc4\x92At\x00P\xb7\xa1_jf`\xe7\xc3T\xef.A\x00\x9c\xdf\xb2\x0d~\xc68\xf9\x02\x00\xbc.\xacX\xb3L\xee\x7f\xd3^_\x06\x0e\xc8\xdd\x01\xb4\xc2\xf6\x81\x15\x09\x00,\xdaIY7\x80\x99\x11f%2\xc0C\x02:k\x96\xac\xd0j\x09$\x96\xb6mu\x00\x0f\xa3\x03\x88\xdf\x04\xa7\xb6=\xf5m\xab%0\xb3k;>\x0d\x02\xf9\xc8\x00f\x965\xe3\xf8@&\x7f\x02 \x1ek\x9f\xc1X\xc4\xd0.\xd1%\xe3\x8f\xd5R|\x06\xc0\xcb\xccu\x03oc\xfa!2\xc0\xa3\xd2\x81,\xc6\x83X\xa0)\x80[\xc3\xc7\x9e\xc5b:\x03\xdc\xafF\xab\x95\xa3\xba\xf2\x11,TT\xf9\xb8\x90t7\x90\x0c9)`\xf9\xe9\xfe}7\x22\x03\x14\x92\xee\x86\xc48\xc6i/\xed\x8f\x03\xcc\x0e\xf7\x17W\xd7\xe2=\xc0\x17R\x90\x07\xd6\x81u\xa4\xbc\x99>\x7f\xbc\x16\xef\x9b\x1b\x19X\x01\xf0\xd2\xfe$0h\x0a\xc6\xee^<\xf9\xbcQ\x9c\xa6\xf2\xd2~\xaaz\xb1\x8c\xb7\xd4A2oz\xferx\x81\xf9S\xcd\xdc\x9bo\xb3\xa4\x1c/\x91\xff\x1ac\x02\xb8mr&s\xa3=\xf7\xea\xc2f\xe6\xba\xabi\x1f4#\x95[\xeb\xfd\xaa\xd9u\x1c\xe1A\xe2\x9fC\x5c\x01\x8eJ,\x991\x8b\xf17\x00\xe2\x0d\xc2\x1d\xe3\x02\xcb\xa6`,7\xfan\xc3\x85\xf7B\x00\x10\xde\x90\x87\x12\xe5\xb3T\x9fd\x86u\x86\xf1U4\xd9]\x1ce\x9f\xee\xdfw\xe3\x7f\xd5|O{z\xe5\xf4/\x95?G\xacm\xe50s\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa6\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa0\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x1b)\xb3G\xee\x04\x00\x00\x00$IDAT\x08\xd7c`@\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x01\xed\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x01jIDATX\x85\xed\x97\xcbN\xc2@\x14\x86\xbfC\x08x}\x00\xf4\x15\xd4\x84w\x91ei\x0bq\xa1\xef#\xae\x9aq\xa8K|\x077\xae\x09\xe1\x1d\xc4\xbd\x17\xe4\x92\x1e\x17\xa5\xa6\x06\xd8\x98!\x18\xed\xbf\x9av&\xfd\xbeN\xa6\xcd9\xf0\xdf#\xf9\x0bU\x15kLP\x12\xb9T8\x05v\x1cq>\x04\x86@\xc7\x0b\x02+\x22\xba$\xa0\xaa\x12\x1bs\xab\x22M`\x02\xf4\x11yu\x82W=\x00\xea@\x15\x11\xd3\xf4\xfdv&Q\xce\xd6Xc\x02I\xe1\x8f\xa5r\xb9\xe1y\xde\xc8\x09|\x918\x8ek\xc9|\xdeC5\xb4\xd6>\x00]\x80R\xb6\xa0$r\x09L\x128w\x0d\x07\xf0\xbb\x86gi\xb7\xdbO@\x9f\xf4|}\x17\x00v\x81\xf7M\xc1sy\x03\xf6V\x09l%\x85\xc0\xd6\x05\xca\xeb&\xac1\xban\xee'\xf1\xc3PV\xdd\xdf\xfa\x0e\x14\x02\x85@!\xb0\xf6?\xb0\xee\xbbu\x9d\xad\xef@!\xf0\xab\x04\xc6\xe4*\x95\x0df\x7f\xc1Z\x12\x18\x02\xf58\x8ek\x9b\x22[k\x8fI\xcb\xf3\xc1\x92\x80\xc0\x0dPMf\xb3\xfb(\x8a\x8e6\x02O\x92\x1eP\x11\xe8\xe4\xb8iTU\xba\xd6F\xa8\x86\xc0\x94\xb41yqBW=$}\xf3\x8aB\xe4\x07\xc1E\xd6\x98,\xb7f\xd6z\x8b\xba\xfd\x8c\xb4Rv\x9110@\xf5\xdao\xb5\xee\x1c=\xf3\x8f\xe4\x13\xfb6zV\x11\xde\xcf\xd8\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x00\xa6\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02\xd4\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x02QIDATX\x85\xed\x96AKTQ\x14\xc7\x7f\xe7\x8d\xb8\xd0&0wi\x84\xe1\xaa)\x90A\xc7\x92^\xa0\x1b\xa1\x8d\x0a\xf5\x19Z;3\xda\xd8j\x16A6\x83\xf3\xbe\x87A\x8d\xad\xc2M\xf6\x14\xf4\x0d\x99H\x0e\x11\xe2\xaa\x11\xdb\x184\xa8\x0b\xc3wZ\xccH\x10\xf3t\xee\xe8\xae\xf9o\xef9\xfc\x7f\xf7\xdc{\xcf=\xd0TS\xff\xbb\xc4$8\x92.\xb6v\x86\x0f'T\x18\x07\x8d\x02]\xd5\xa5\x12\xcag\x11\xc9\xef\x97\xdb\xf3\xc5t\xe4\xf8\xd2\x01lg\xed1*\x19\xa0\x07\xe4\x0b\xaaKX\x94\x00D\xb5K\xb1\x86A\xef\x22\xec\x082\xedN\xc6\xde\x5c\x0a\xc0\x93\xf9\xf9\xd0\x8f\xdd\x9b\x19\x948\xf0^\x95\xd4Jbp\xb3V\xec\x90S\xe8\x0b\xf9:\x8b0\x0ad\x97\xcb\xb1\x14i\xf1\xeb\xdddM\xd9\x8e7g\xe7\xbc\x93\x87\xceZ\xb2\xee\x9c\x9c7e\xe7\xbc\x13;\xe7e\xce\x8b=\xb3\x02\xd5\xb2\xbf\x16$\xe9\xc6cs\xf5\x02Tr\xbdi\x94W\x08\x13\xcb\x93\x83yc\x80H\xba\xd8z\xed\xea\xc1WA\xbf\xb9\xf1{\x8fL\xccO\xf5\xc0),\x8aj\xcf\xcf\xf2\x95H\xd0\xc5\xb4\x82\x92;\xc3\x87\x13\xc0-_e\xa6\x11s\x00\xcb\x97g@oG\xf8`,0&h\xa1\xf2\xd4\xd8\x0c\xbap\xf5\xc8M\x0cl\xa8\xb2%`\x0e\x00\x1a\x15\xf4c\xa3\xe6\xa7\x12\xf8\x80\xd0\xdf\x00\x00\xd7\x15)]\x14@a\x97\xbf\x0d\xcb\x08\x00\xc4\xacS\xd64\x10\x11 \xb0\x17\x9c\x05\xb0\x87O\xf7E\x01\x14\xed\x02\xf6\xcc\x01\x94O\x0a\xc3\x17\x05\x00F\x80\x821\x80\x88\xe4E\xb83\xe4\x14\xfa\x1au\xb6\x9d\xd5(p\x1b\xd1w\xc6\x00\xfb\xe5\xf6<\xc2N\xc8\xd7\xd9\x86\xdcU\x05\xb52\xc0\xf6Q[\xcb\x821@1\x1d9Ve\x0aa\xd4\xceyS\xa6\xfev\xceK\x01#\xa2~r\xfdi\xffoc\x00\x80\x95\xf8\xe0[ \x0b\xcc\xd6\x0d\xa1*\xf6\xdc\xda\x0c\x22/D\xc8\xb8\x89\xfb\x81\xe5\x87z\xe6\x81\xb4Zv\xb8\xf0\x12a\x1aX\x14\xb5Rnb`\xa3V\xa8\xed\xacF\xabe\x1f\x11!\xe3\xfe\x8a=?\xef;6\x18H\xbcq\x94,\xd0\xab\xca\x96\x08K\x08\xdf\x01PnPy1\x11`[\xd4O\x9e\xb7sc\x00\xa8\xfc\x90\x1d\xe1\x831\xaa#\x99 \xdd\x15\x7f-\x89\xca:\x96\xe6\x8f\xdaZ\x16\xce:\xf3\xa6\x9aj\xea_\xfd\x01\xd3\x1c\xd9\x7f^\xb93\xcd\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x0b\x95\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x0aOiCCPPhotoshop ICC profile\x00\x00x\xda\x9dSgTS\xe9\x16=\xf7\xde\xf4BK\x88\x80\x94KoR\x15\x08 RB\x8b\x80\x14\x91&*!\x09\x10J\x88!\xa1\xd9\x15Q\xc1\x11EE\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15Q,\x0c\x8a\x0a\xd8\x07\xe4!\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1{\xa3k\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7>\xe7\xac\xf3\x9d\xb3\xcf\x07\xc0\x08\x0c\x96H3Q5\x80\x0c\xa9B\x1e\x11\xe0\x83\xc7\xc4\xc6\xe1\xe4.@\x81\x0a$p\x00\x10\x08\xb3d!s\xfd#\x01\x00\xf8~<<+\x22\xc0\x07\xbe\x00\x01x\xd3\x0b\x08\x00\xc0M\x9b\xc00\x1c\x87\xff\x0f\xeaB\x99\x5c\x01\x80\x84\x01\xc0t\x918K\x08\x80\x14\x00@z\x8eB\xa6\x00@F\x01\x80\x9d\x98&S\x00\xa0\x04\x00`\xcbcb\xe3\x00P-\x00`'\x7f\xe6\xd3\x00\x80\x9d\xf8\x99{\x01\x00[\x94!\x15\x01\xa0\x91\x00 \x13e\x88D\x00h;\x00\xac\xcfV\x8aE\x00X0\x00\x14fK\xc49\x00\xd8-\x000IWfH\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x000Q\x88\x85)\x00\x04{\x00`\xc8##x\x00\x84\x99\x00\x14F\xf2W<\xf1+\xae\x10\xe7*\x00\x00x\x99\xb2<\xb9$9E\x81[\x08-q\x07WW.\x1e(\xceI\x17+\x146a\x02a\x9a@.\xc2y\x99\x192\x814\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\xf3\xfdx\xce\x0e\xae\xce\xce6\x8e\xb6\x0e_-\xea\xbf\x06\xff\x22bb\xe3\xfe\xe5\xcf\xabp@\x00\x00\xe1t~\xd1\xfe,/\xb3\x1a\x80;\x06\x80m\xfe\xa2%\xee\x04h^\x0b\xa0u\xf7\x8bf\xb2\x0f@\xb5\x00\xa0\xe9\xdaW\xf3p\xf8~<\xdf5\x00\xb0j>\x01{\x91-\xa8]c\x03\xf6K'\x10Xt\xc0\xe2\xf7\x00\x00\xf2\xbbo\xc1\xd4(\x08\x03\x80h\x83\xe1\xcfw\xff\xef?\xfdG\xa0%\x00\x80fI\x92q\x00\x00^D$.T\xca\xb3?\xc7\x08\x00\x00D\xa0\x81*\xb0A\x1b\xf4\xc1\x18,\xc0\x06\x1c\xc1\x05\xdc\xc1\x0b\xfc`6\x84B$\xc4\xc2B\x10B\x0ad\x80\x1cr`)\xac\x82B(\x86\xcd\xb0\x1d*`/\xd4@\x1d4\xc0Qh\x86\x93p\x0e.\xc2U\xb8\x0e=p\x0f\xfaa\x08\x9e\xc1(\xbc\x81\x09\x04A\xc8\x08\x13a!\xda\x88\x01b\x8aX#\x8e\x08\x17\x99\x85\xf8!\xc1H\x04\x12\x8b$ \xc9\x88\x14Q\x22K\x915H1R\x8aT UH\x1d\xf2=r\x029\x87\x5cF\xba\x91;\xc8\x002\x82\xfc\x86\xbcG1\x94\x81\xb2Q=\xd4\x0c\xb5C\xb9\xa87\x1a\x84F\xa2\x0b\xd0dt1\x9a\x8f\x16\xa0\x9b\xd0r\xb4\x1a=\x8c6\xa1\xe7\xd0\xabh\x0f\xda\x8f>C\xc70\xc0\xe8\x18\x073\xc4l0.\xc6\xc3B\xb18,\x09\x93c\xcb\xb1\x22\xac\x0c\xab\xc6\x1a\xb0V\xac\x03\xbb\x89\xf5c\xcf\xb1w\x04\x12\x81E\xc0\x096\x04wB a\x1eAHXLXN\xd8H\xa8 \x1c$4\x11\xda\x097\x09\x03\x84Q\xc2'\x22\x93\xa8K\xb4&\xba\x11\xf9\xc4\x18b21\x87XH,#\xd6\x12\x8f\x13/\x10{\x88C\xc47$\x12\x89C2'\xb9\x90\x02I\xb1\xa4T\xd2\x12\xd2F\xd2nR#\xe9,\xa9\x9b4H\x1a#\x93\xc9\xdadk\xb2\x079\x94, +\xc8\x85\xe4\x9d\xe4\xc3\xe43\xe4\x1b\xe4!\xf2[\x0a\x9db@q\xa4\xf8S\xe2(R\xcajJ\x19\xe5\x10\xe54\xe5\x06e\x982AU\xa3\x9aR\xdd\xa8\xa1T\x115\x8fZB\xad\xa1\xb6R\xafQ\x87\xa8\x134u\x9a9\xcd\x83\x16IK\xa5\xad\xa2\x95\xd3\x1ah\x17h\xf7i\xaf\xe8t\xba\x11\xdd\x95\x1eN\x97\xd0W\xd2\xcb\xe9G\xe8\x97\xe8\x03\xf4w\x0c\x0d\x86\x15\x83\xc7\x88g(\x19\x9b\x18\x07\x18g\x19w\x18\xaf\x98L\xa6\x19\xd3\x8b\x19\xc7T071\xeb\x98\xe7\x99\x0f\x99oUX*\xb6*|\x15\x91\xca\x0a\x95J\x95&\x95\x1b*/T\xa9\xaa\xa6\xaa\xde\xaa\x0bU\xf3U\xcbT\x8f\xa9^S}\xaeFU3S\xe3\xa9\x09\xd4\x96\xabU\xaa\x9dP\xebS\x1bSg\xa9;\xa8\x87\xaag\xa8oT?\xa4~Y\xfd\x89\x06Y\xc3L\xc3OC\xa4Q\xa0\xb1_\xe3\xbc\xc6 \x0bc\x19\xb3x,!k\x0d\xab\x86u\x815\xc4&\xb1\xcd\xd9|v*\xbb\x98\xfd\x1d\xbb\x8b=\xaa\xa9\xa19C3J3W\xb3R\xf3\x94f?\x07\xe3\x98q\xf8\x9ctN\x09\xe7(\xa7\x97\xf3~\x8a\xde\x14\xef)\xe2)\x1b\xa64L\xb91e\x5ck\xaa\x96\x97\x96X\xabH\xabQ\xabG\xeb\xbd6\xae\xed\xa7\x9d\xa6\xbdE\xbbY\xfb\x81\x0eA\xc7J'\x5c'Gg\x8f\xce\x05\x9d\xe7S\xd9S\xdd\xa7\x0a\xa7\x16M=:\xf5\xae.\xaak\xa5\x1b\xa1\xbbDw\xbfn\xa7\xee\x98\x9e\xbe^\x80\x9eLo\xa7\xdey\xbd\xe7\xfa\x1c}/\xfdT\xfdm\xfa\xa7\xf5G\x0cX\x06\xb3\x0c$\x06\xdb\x0c\xce\x18<\xc55qo<\x1d/\xc7\xdb\xf1QC]\xc3@C\xa5a\x95a\x97\xe1\x84\x91\xb9\xd1<\xa3\xd5F\x8dF\x0f\x8ci\xc6\x5c\xe3$\xe3m\xc6m\xc6\xa3&\x06&!&KM\xeaM\xee\x9aRM\xb9\xa6)\xa6;L;L\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x995\x9b=1\xd72\xe7\x9b\xe7\x9b\xd7\x9b\xdf\xb7`ZxZ,\xb6\xa8\xb6\xb8eI\xb2\xe4Z\xa6Y\xee\xb6\xbcn\x85Z9Y\xa5XUZ]\xb3F\xad\x9d\xad%\xd6\xbb\xad\xbb\xa7\x11\xa7\xb9N\x93N\xab\x9e\xd6g\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\x19\xb0\xe5\xd8\x06\xdb\xae\xb6m\xb6}agb\x17g\xb7\xc5\xae\xc3\xee\x93\xbd\x93}\xba}\x8d\xfd=\x07\x0d\x87\xd9\x0e\xab\x1dZ\x1d~s\xb4r\x14:V:\xde\x9a\xce\x9c\xee?}\xc5\xf4\x96\xe9/gX\xcf\x10\xcf\xd83\xe3\xb6\x13\xcb)\xc4i\x9dS\x9b\xd3Gg\x17g\xb9s\x83\xf3\x88\x8b\x89K\x82\xcb.\x97>.\x9b\x1b\xc6\xdd\xc8\xbd\xe4Jt\xf5q]\xe1z\xd2\xf5\x9d\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee6\xeei\xee\x87\xdc\x9f\xcc4\x9f)\x9eY3s\xd0\xc3\xc8C\xe0Q\xe5\xd1?\x0b\x9f\x950k\xdf\xac~OCO\x81g\xb5\xe7#/c/\x91W\xad\xd7\xb0\xb7\xa5w\xaa\xf7a\xef\x17>\xf6>r\x9f\xe3>\xe3<7\xde2\xdeY_\xcc7\xc0\xb7\xc8\xb7\xcbO\xc3o\x9e_\x85\xdfC\x7f#\xffd\xffz\xff\xd1\x00\xa7\x80%\x01g\x03\x89\x81A\x81[\x02\xfb\xf8z|!\xbf\x8e?:\xdbe\xf6\xb2\xd9\xedA\x8c\xa0\xb9A\x15A\x8f\x82\xad\x82\xe5\xc1\xad!h\xc8\xec\x90\xad!\xf7\xe7\x98\xce\x91\xcei\x0e\x85P~\xe8\xd6\xd0\x07a\xe6a\x8b\xc3~\x0c'\x85\x87\x85W\x86?\x8ep\x88X\x1a\xd11\x975w\xd1\xdcCs\xdfD\xfaD\x96D\xde\x9bg1O9\xaf-J5*>\xaa.j<\xda7\xba4\xba?\xc6.fY\xcc\xd5X\x9dXIlK\x1c9.*\xae6nl\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3{\x17\x98/\xc8]py\xa1\xce\xc2\xf4\x85\xa7\x16\xa9.\x12,:\x96@L\x88N8\x94\xf0A\x10*\xa8\x16\x8c%\xf2\x13w%\x8e\x0ay\xc2\x1d\xc2g\x22/\xd16\xd1\x88\xd8C\x5c*\x1eN\xf2H*Mz\x92\xec\x91\xbc5y$\xc53\xa5,\xe5\xb9\x84'\xa9\x90\xbcL\x0dL\xdd\x9b:\x9e\x16\x9av m2=:\xbd1\x83\x92\x91\x90qB\xaa!M\x93\xb6g\xeag\xe6fv\xcb\xace\x85\xb2\xfe\xc5n\x8b\xb7/\x1e\x95\x07\xc9k\xb3\x90\xac\x05Y-\x0a\xb6B\xa6\xe8TZ(\xd7*\x07\xb2geWf\xbf\xcd\x89\xca9\x96\xab\x9e+\xcd\xed\xcc\xb3\xca\xdb\x907\x9c\xef\x9f\xff\xed\x12\xc2\x12\xe1\x92\xb6\xa5\x86KW-\x1dX\xe6\xbd\xacj9\xb2\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8(\xdcx\xe5\x1b\x87o\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9d\xcff\xd2f\xe9\xe6\xde-\x9e[\x0e\x96\xaa\x97\xe6\x97\x0en\x0d\xd9\xda\xb4\x0d\xdfV\xb4\xed\xf5\xf6E\xdb/\x97\xcd(\xdb\xbb\x83\xb6C\xb9\xa3\xbf<\xb8\xbce\xa7\xc9\xce\xcd;?T\xa4T\xf4T\xfaT6\xee\xd2\xdd\xb5a\xd7\xf8n\xd1\xee\x1b{\xbc\xf64\xec\xd5\xdb[\xbc\xf7\xfd>\xc9\xbe\xdbU\x01UM\xd5f\xd5e\xfbI\xfb\xb3\xf7?\xae\x89\xaa\xe9\xf8\x96\xfbm]\xadNmq\xed\xc7\x03\xd2\x03\xfd\x07#\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2=TR\x8f\xd6+\xebG\x0e\xc7\x1f\xbe\xfe\x9d\xefw-\x0d6\x0dU\x8d\x9c\xc6\xe2#pDy\xe4\xe9\xf7\x09\xdf\xf7\x1e\x0d:\xdav\x8c{\xac\xe1\x07\xd3\x1fv\x1dg\x1d/jB\x9a\xf2\x9aF\x9bS\x9a\xfb[b[\xbaO\xcc>\xd1\xd6\xea\xdez\xfcG\xdb\x1f\x0f\x9c499\xe2?r\xfd\xe9\xfc\xa7C\xcfd\xcf&\x9e\x17\xfe\xa2\xfe\xcb\xae\x17\x16/~\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\x93\xbfm|\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\xbe\xc9x31^\xf4V\xfb\xed\xc1w\xdcw\x1d\xef\xa3\xdf\x0fO\xe4| \x7f(\xffh\xf9\xb1\xf5S\xd0\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfcc3-\xdb\x00\x00\x00 cHRM\x00\x00z%\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17o\x92_\xc5F\x00\x00\x00\xc0IDATx\xda\xdcVA\x0e\x830\x0c\xab\xa3\xfe\xff=\xfb\x9dw\x021ThJ\xe2 -'\xd4\x03vl'-\xda\xa7\x1d\x8b\xad\xa6\xb0}\xf4b\xe0s\xa3\xb0\x17\xc0\x7f\x88\xf4\x99D\xa2\xce\xf7\xb2B\xf0\xe1\xbfM\x0c\xceA\xd7\x98)\xa0\x90\xfb2gV\xe5\xf5\x85\x1aR\x05\x5ceE\xdd\xbbCX\x0a\xfew\x16,w\x9fI\xe0\x11x\x16\x01*-`\x10\x00\x11\x02\x9eM\xc6\x08\xf8\x1d\x01:\xce\xa8\x9a\x02&\xf8\x8d\x08\x01\x04s\x81\x8c\x10*\xdf\x04\xee\x10B\x91\xfa\xd51\x84\x12\xdc\xbb\x88\xf0\x96\x05+$\xa0&p\x07\x82\x0a\x05dv\xf4\xc1\x8cCL\x82\x91M\x98~sv\xc5\x15\xbb\x9a\x81\xb2\xad7\xb2\xd3\xaaW\xef9K\xdf\x01\x00h\x95#\xfe/d\x9d\xea\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x03\xa5\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x03\x22IDATX\x85\xed\x96MlTU\x14\xc7\x7f\xe7\x0d\xa9\x09\xcc\x90Pv\xb6\xc6``\xe3\xa3\x864\xf4\xc3\xc6g\xa4\x1b\xa2\x98@\x13]\xc9\x1a6\xda\x84~Y\x5c\xcd\xce:\xa43\x09\xcb\xaee\x83\x89\x19L\x04\xc3\xc6:\x98\xb4o\x22bK'\xc64\xac\x9c\x067\x94t\x98\x92P:\xef\xef\xe2M\xa75\x99\xe9\xccCv\xf4\xbf\xba\xe7\xbds\xef\xf9\xdds\xee\x17\xeciO\xaf\xba,\x8a\xb3\x9b,\xb4\x1dN\xac\x0f\xc98\x07\xea\x06:\xaa\xbf\x8a\x88\xdf\xcd,\xfb\xa8t [H\xba\x1b/\x1d\xc0\xcb\xcc\x7f\x82,\x05\x1c\x01\xbb\x8f4\x8bC\x11\xc0\xa4\x0e\xe1\x9c\x02ua<0l\x22w\xa9\xf7\xfb\x97\x02\xf0\xe9\xf5\xeb\xb1\x7fV\xdeL!F\x80\x9f$&\x7f\x1d\xed[\xa8\xe7;\x90\xc9\x9f\x88\x05\x9a\xc28\x0d\x5c\xb9S\xea\x9d$iA\xab\x93\xac+/\xe3O{i\xbf\xf2~f~\xac\xe5>i\x7f\xdcK\xfb\x15/\xed\xa7\x9a\xf9\xee\x9a\x81j\xda\xbf3l,7\xd2;\x0d\xf0\xe1\xd5\xe5\xd7\x9e<\x7f|\xd1\xe03Y\xd0\x15\x0eb\x8b\x18\xd7\xe2\xb1\xf6\x99[\xc3\xc7\x9eU\xc1'\x10\xdf`\x0c\xdd\xb9\xd4\x97\x8d\x0c\xe0&\x0bm\xed\x07\xcb\x7f\x1a\xfa+7\xd2\xff\x11\xc0\x07W\xe7;+\x9b\xceMP\x17X\x00r\xaa\xc3\x84mc1\x16\xd3\x99\xd9\xe1\xfe\x22\xc0{\x99\xfcm\x93\x8e\xac\x96\xe2n\xa3\x85\xe94\x028\x9cX\x1f\x02\xde\x0ad\x97\xb7f^\xd9tnb:\x1ezhG\xdfZ\xbb\xab\xb2\xc9\x8fn\xb2\xd0\x06\xe0\x04\xf6%p\xf4P\xa2|\xb6Q\x9c\x86\x00\xe1Vcak\xc1\x95+\xab\x17@]h\x97\xb2\x09\x03{\xa7\xfd`\xf9\x02@n\xb4\xe7\x9e\xc4\x92At\x00P\xb7\xa1_jf`\xe7\xc3T\xef.A\x00\x9c\xdf\xb2\x0d~\xc68\xf9\x02\x00\xbc.\xacX\xb3L\xee\x7f\xd3^_\x06\x0e\xc8\xdd\x01\xb4\xc2\xf6\x81\x15\x09\x00,\xdaIY7\x80\x99\x11f%2\xc0C\x02:k\x96\xac\xd0j\x09$\x96\xb6mu\x00\x0f\xa3\x03\x88\xdf\x04\xa7\xb6=\xf5m\xab%0\xb3k;>\x0d\x02\xf9\xc8\x00f\x965\xe3\xf8@&\x7f\x02 \x1ek\x9f\xc1X\xc4\xd0.\xd1%\xe3\x8f\xd5R|\x06\xc0\xcb\xccu\x03oc\xfa!2\xc0\xa3\xd2\x81,\xc6\x83X\xa0)\x80[\xc3\xc7\x9e\xc5b:\x03\xdc\xafF\xab\x95\xa3\xba\xf2\x11,TT\xf9\xb8\x90t7\x90\x0c9)`\xf9\xe9\xfe}7\x22\x03\x14\x92\xee\x86\xc48\xc6i/\xed\x8f\x03\xcc\x0e\xf7\x17W\xd7\xe2=\xc0\x17R\x90\x07\xd6\x81u\xa4\xbc\x99>\x7f\xbc\x16\xef\x9b\x1b\x19X\x01\xf0\xd2\xfe$0h\x0a\xc6\xee^<\xf9\xbcQ\x9c\xa6\xf2\xd2~\xaaz\xb1\x8c\xb7\xd4A2oz\xferx\x81\xf9S\xcd\xdc\x9bo\xb3\xa4\x1c/\x91\xff\x1ac\x02\xb8mr&s\xa3=\xf7\xea\xc2f\xe6\xba\xabi\x1f4#\x95[\xeb\xfd\xaa\xd9u\x1c\xe1A\xe2\x9fC\x5c\x01\x8eJ,\x991\x8b\xf17\x00\xe2\x0d\xc2\x1d\xe3\x02\xcb\xa6`,7\xfan\xc3\x85\xf7B\x00\x10\xde\x90\x87\x12\xe5\xb3T\x9fd\x86u\x86\xf1U4\xd9]\x1ce\x9f\xee\xdfw\xe3\x7f\xd5|O{z\xe5\xf4/\x95?G\xacm\xe50s\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02\x02\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x01\x7fIDATX\x85\xed\x97\xcbJBQ\x14\x86\xbfe\xa5\xd9\xe5\x01\xacW\xc8@(\xa3\xd2\x9e\x22\x87\xdd\x88\x0663\xa1\x9e\xa1\x896\xa9F]iX\xef\x10\x1c\x8d\xb4@\xa2w\xc8\xe6]\xac,W\x83:\xa2\x1c\xcf$\xb6\x18u\xfe\xd9^\x1b\xf6\xf7\xb1`o\xf6\x82\xff\x1eiZ\xa9J,[X\x14\x95$B\x18\xe85\xc4yA\xb9\x05\xd9\xb1\xd6\xc6\x8f\x10Q\xa7\x80\xaa\xccl\x15\x0fU\x99\x07^\x05J\x8a>\x9a\xa0\x0b2\xa0\x10\x01\x02 \x07Vj|\xd9\x96\xa8\x0b\xc42\x97K\x82\xec\x83\xe6\x91\xee\x84\x95\x1a+\x9b\x80\xdb\x89g\xafC\xe8\xc7)0\xa5\xcaB.=q\x0c\xe0\xab[\xaa$\x81\xd7\xaew\xdf\xaci8\x80\x95\x1a+\xd7\xaa\xd5\x04\xf0&\xc2\xaa]\xaf\x0b \x8c\x08\x94\xce\xd7\xa3\xf7\xa6\xe1v\xf2\x1b\xb1;\xa0\x04\x84\x9d\x02\x10Txn\x17\xbc!O@_+\x81\x8e\xc4\x13\xe8\xb8@\xb7\xdbF\xe7\xac\xf3\x9d\xb3\xcf\x07\xc0\x08\x0c\x96H3Q5\x80\x0c\xa9B\x1e\x11\xe0\x83\xc7\xc4\xc6\xe1\xe4.@\x81\x0a$p\x00\x10\x08\xb3d!s\xfd#\x01\x00\xf8~<<+\x22\xc0\x07\xbe\x00\x01x\xd3\x0b\x08\x00\xc0M\x9b\xc00\x1c\x87\xff\x0f\xeaB\x99\x5c\x01\x80\x84\x01\xc0t\x918K\x08\x80\x14\x00@z\x8eB\xa6\x00@F\x01\x80\x9d\x98&S\x00\xa0\x04\x00`\xcbcb\xe3\x00P-\x00`'\x7f\xe6\xd3\x00\x80\x9d\xf8\x99{\x01\x00[\x94!\x15\x01\xa0\x91\x00 \x13e\x88D\x00h;\x00\xac\xcfV\x8aE\x00X0\x00\x14fK\xc49\x00\xd8-\x000IWfH\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x000Q\x88\x85)\x00\x04{\x00`\xc8##x\x00\x84\x99\x00\x14F\xf2W<\xf1+\xae\x10\xe7*\x00\x00x\x99\xb2<\xb9$9E\x81[\x08-q\x07WW.\x1e(\xceI\x17+\x146a\x02a\x9a@.\xc2y\x99\x192\x814\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\xf3\xfdx\xce\x0e\xae\xce\xce6\x8e\xb6\x0e_-\xea\xbf\x06\xff\x22bb\xe3\xfe\xe5\xcf\xabp@\x00\x00\xe1t~\xd1\xfe,/\xb3\x1a\x80;\x06\x80m\xfe\xa2%\xee\x04h^\x0b\xa0u\xf7\x8bf\xb2\x0f@\xb5\x00\xa0\xe9\xdaW\xf3p\xf8~<\xdf5\x00\xb0j>\x01{\x91-\xa8]c\x03\xf6K'\x10Xt\xc0\xe2\xf7\x00\x00\xf2\xbbo\xc1\xd4(\x08\x03\x80h\x83\xe1\xcfw\xff\xef?\xfdG\xa0%\x00\x80fI\x92q\x00\x00^D$.T\xca\xb3?\xc7\x08\x00\x00D\xa0\x81*\xb0A\x1b\xf4\xc1\x18,\xc0\x06\x1c\xc1\x05\xdc\xc1\x0b\xfc`6\x84B$\xc4\xc2B\x10B\x0ad\x80\x1cr`)\xac\x82B(\x86\xcd\xb0\x1d*`/\xd4@\x1d4\xc0Qh\x86\x93p\x0e.\xc2U\xb8\x0e=p\x0f\xfaa\x08\x9e\xc1(\xbc\x81\x09\x04A\xc8\x08\x13a!\xda\x88\x01b\x8aX#\x8e\x08\x17\x99\x85\xf8!\xc1H\x04\x12\x8b$ \xc9\x88\x14Q\x22K\x915H1R\x8aT UH\x1d\xf2=r\x029\x87\x5cF\xba\x91;\xc8\x002\x82\xfc\x86\xbcG1\x94\x81\xb2Q=\xd4\x0c\xb5C\xb9\xa87\x1a\x84F\xa2\x0b\xd0dt1\x9a\x8f\x16\xa0\x9b\xd0r\xb4\x1a=\x8c6\xa1\xe7\xd0\xabh\x0f\xda\x8f>C\xc70\xc0\xe8\x18\x073\xc4l0.\xc6\xc3B\xb18,\x09\x93c\xcb\xb1\x22\xac\x0c\xab\xc6\x1a\xb0V\xac\x03\xbb\x89\xf5c\xcf\xb1w\x04\x12\x81E\xc0\x096\x04wB a\x1eAHXLXN\xd8H\xa8 \x1c$4\x11\xda\x097\x09\x03\x84Q\xc2'\x22\x93\xa8K\xb4&\xba\x11\xf9\xc4\x18b21\x87XH,#\xd6\x12\x8f\x13/\x10{\x88C\xc47$\x12\x89C2'\xb9\x90\x02I\xb1\xa4T\xd2\x12\xd2F\xd2nR#\xe9,\xa9\x9b4H\x1a#\x93\xc9\xdadk\xb2\x079\x94, +\xc8\x85\xe4\x9d\xe4\xc3\xe43\xe4\x1b\xe4!\xf2[\x0a\x9db@q\xa4\xf8S\xe2(R\xcajJ\x19\xe5\x10\xe54\xe5\x06e\x982AU\xa3\x9aR\xdd\xa8\xa1T\x115\x8fZB\xad\xa1\xb6R\xafQ\x87\xa8\x134u\x9a9\xcd\x83\x16IK\xa5\xad\xa2\x95\xd3\x1ah\x17h\xf7i\xaf\xe8t\xba\x11\xdd\x95\x1eN\x97\xd0W\xd2\xcb\xe9G\xe8\x97\xe8\x03\xf4w\x0c\x0d\x86\x15\x83\xc7\x88g(\x19\x9b\x18\x07\x18g\x19w\x18\xaf\x98L\xa6\x19\xd3\x8b\x19\xc7T071\xeb\x98\xe7\x99\x0f\x99oUX*\xb6*|\x15\x91\xca\x0a\x95J\x95&\x95\x1b*/T\xa9\xaa\xa6\xaa\xde\xaa\x0bU\xf3U\xcbT\x8f\xa9^S}\xaeFU3S\xe3\xa9\x09\xd4\x96\xabU\xaa\x9dP\xebS\x1bSg\xa9;\xa8\x87\xaag\xa8oT?\xa4~Y\xfd\x89\x06Y\xc3L\xc3OC\xa4Q\xa0\xb1_\xe3\xbc\xc6 \x0bc\x19\xb3x,!k\x0d\xab\x86u\x815\xc4&\xb1\xcd\xd9|v*\xbb\x98\xfd\x1d\xbb\x8b=\xaa\xa9\xa19C3J3W\xb3R\xf3\x94f?\x07\xe3\x98q\xf8\x9ctN\x09\xe7(\xa7\x97\xf3~\x8a\xde\x14\xef)\xe2)\x1b\xa64L\xb91e\x5ck\xaa\x96\x97\x96X\xabH\xabQ\xabG\xeb\xbd6\xae\xed\xa7\x9d\xa6\xbdE\xbbY\xfb\x81\x0eA\xc7J'\x5c'Gg\x8f\xce\x05\x9d\xe7S\xd9S\xdd\xa7\x0a\xa7\x16M=:\xf5\xae.\xaak\xa5\x1b\xa1\xbbDw\xbfn\xa7\xee\x98\x9e\xbe^\x80\x9eLo\xa7\xdey\xbd\xe7\xfa\x1c}/\xfdT\xfdm\xfa\xa7\xf5G\x0cX\x06\xb3\x0c$\x06\xdb\x0c\xce\x18<\xc55qo<\x1d/\xc7\xdb\xf1QC]\xc3@C\xa5a\x95a\x97\xe1\x84\x91\xb9\xd1<\xa3\xd5F\x8dF\x0f\x8ci\xc6\x5c\xe3$\xe3m\xc6m\xc6\xa3&\x06&!&KM\xeaM\xee\x9aRM\xb9\xa6)\xa6;L;L\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x995\x9b=1\xd72\xe7\x9b\xe7\x9b\xd7\x9b\xdf\xb7`ZxZ,\xb6\xa8\xb6\xb8eI\xb2\xe4Z\xa6Y\xee\xb6\xbcn\x85Z9Y\xa5XUZ]\xb3F\xad\x9d\xad%\xd6\xbb\xad\xbb\xa7\x11\xa7\xb9N\x93N\xab\x9e\xd6g\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\x19\xb0\xe5\xd8\x06\xdb\xae\xb6m\xb6}agb\x17g\xb7\xc5\xae\xc3\xee\x93\xbd\x93}\xba}\x8d\xfd=\x07\x0d\x87\xd9\x0e\xab\x1dZ\x1d~s\xb4r\x14:V:\xde\x9a\xce\x9c\xee?}\xc5\xf4\x96\xe9/gX\xcf\x10\xcf\xd83\xe3\xb6\x13\xcb)\xc4i\x9dS\x9b\xd3Gg\x17g\xb9s\x83\xf3\x88\x8b\x89K\x82\xcb.\x97>.\x9b\x1b\xc6\xdd\xc8\xbd\xe4Jt\xf5q]\xe1z\xd2\xf5\x9d\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee6\xeei\xee\x87\xdc\x9f\xcc4\x9f)\x9eY3s\xd0\xc3\xc8C\xe0Q\xe5\xd1?\x0b\x9f\x950k\xdf\xac~OCO\x81g\xb5\xe7#/c/\x91W\xad\xd7\xb0\xb7\xa5w\xaa\xf7a\xef\x17>\xf6>r\x9f\xe3>\xe3<7\xde2\xdeY_\xcc7\xc0\xb7\xc8\xb7\xcbO\xc3o\x9e_\x85\xdfC\x7f#\xffd\xffz\xff\xd1\x00\xa7\x80%\x01g\x03\x89\x81A\x81[\x02\xfb\xf8z|!\xbf\x8e?:\xdbe\xf6\xb2\xd9\xedA\x8c\xa0\xb9A\x15A\x8f\x82\xad\x82\xe5\xc1\xad!h\xc8\xec\x90\xad!\xf7\xe7\x98\xce\x91\xcei\x0e\x85P~\xe8\xd6\xd0\x07a\xe6a\x8b\xc3~\x0c'\x85\x87\x85W\x86?\x8ep\x88X\x1a\xd11\x975w\xd1\xdcCs\xdfD\xfaD\x96D\xde\x9bg1O9\xaf-J5*>\xaa.j<\xda7\xba4\xba?\xc6.fY\xcc\xd5X\x9dXIlK\x1c9.*\xae6nl\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3{\x17\x98/\xc8]py\xa1\xce\xc2\xf4\x85\xa7\x16\xa9.\x12,:\x96@L\x88N8\x94\xf0A\x10*\xa8\x16\x8c%\xf2\x13w%\x8e\x0ay\xc2\x1d\xc2g\x22/\xd16\xd1\x88\xd8C\x5c*\x1eN\xf2H*Mz\x92\xec\x91\xbc5y$\xc53\xa5,\xe5\xb9\x84'\xa9\x90\xbcL\x0dL\xdd\x9b:\x9e\x16\x9av m2=:\xbd1\x83\x92\x91\x90qB\xaa!M\x93\xb6g\xeag\xe6fv\xcb\xace\x85\xb2\xfe\xc5n\x8b\xb7/\x1e\x95\x07\xc9k\xb3\x90\xac\x05Y-\x0a\xb6B\xa6\xe8TZ(\xd7*\x07\xb2geWf\xbf\xcd\x89\xca9\x96\xab\x9e+\xcd\xed\xcc\xb3\xca\xdb\x907\x9c\xef\x9f\xff\xed\x12\xc2\x12\xe1\x92\xb6\xa5\x86KW-\x1dX\xe6\xbd\xacj9\xb2\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8(\xdcx\xe5\x1b\x87o\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9d\xcff\xd2f\xe9\xe6\xde-\x9e[\x0e\x96\xaa\x97\xe6\x97\x0en\x0d\xd9\xda\xb4\x0d\xdfV\xb4\xed\xf5\xf6E\xdb/\x97\xcd(\xdb\xbb\x83\xb6C\xb9\xa3\xbf<\xb8\xbce\xa7\xc9\xce\xcd;?T\xa4T\xf4T\xfaT6\xee\xd2\xdd\xb5a\xd7\xf8n\xd1\xee\x1b{\xbc\xf64\xec\xd5\xdb[\xbc\xf7\xfd>\xc9\xbe\xdbU\x01UM\xd5f\xd5e\xfbI\xfb\xb3\xf7?\xae\x89\xaa\xe9\xf8\x96\xfbm]\xadNmq\xed\xc7\x03\xd2\x03\xfd\x07#\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2=TR\x8f\xd6+\xebG\x0e\xc7\x1f\xbe\xfe\x9d\xefw-\x0d6\x0dU\x8d\x9c\xc6\xe2#pDy\xe4\xe9\xf7\x09\xdf\xf7\x1e\x0d:\xdav\x8c{\xac\xe1\x07\xd3\x1fv\x1dg\x1d/jB\x9a\xf2\x9aF\x9bS\x9a\xfb[b[\xbaO\xcc>\xd1\xd6\xea\xdez\xfcG\xdb\x1f\x0f\x9c499\xe2?r\xfd\xe9\xfc\xa7C\xcfd\xcf&\x9e\x17\xfe\xa2\xfe\xcb\xae\x17\x16/~\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\x93\xbfm|\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\xbe\xc9x31^\xf4V\xfb\xed\xc1w\xdcw\x1d\xef\xa3\xdf\x0fO\xe4| \x7f(\xffh\xf9\xb1\xf5S\xd0\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfcc3-\xdb\x00\x00\x00 cHRM\x00\x00z%\x00\x00\x80\x83\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17o\x92_\xc5F\x00\x00\x00\xc0IDATx\xda\xdcVA\x0e\x830\x0c\xab\xa3\xfe\xff=\xfb\x9dw\x021ThJ\xe2 -'\xd4\x03vl'-\xda\xa7\x1d\x8b\xad\xa6\xb0}\xf4b\xe0s\xa3\xb0\x17\xc0\x7f\x88\xf4\x99D\xa2\xce\xf7\xb2B\xf0\xe1\xbfM\x0c\xceA\xd7\x98)\xa0\x90\xfb2gV\xe5\xf5\x85\x1aR\x05\x5ceE\xdd\xbbCX\x0a\xfew\x16,w\x9fI\xe0\x11x\x16\x01*-`\x10\x00\x11\x02\x9eM\xc6\x08\xf8\x1d\x01:\xce\xa8\x9a\x02&\xf8\x8d\x08\x01\x04s\x81\x8c\x10*\xdf\x04\xee\x10B\x91\xfa\xd51\x84\x12\xdc\xbb\x88\xf0\x96\x05+$\xa0&p\x07\x82\x0a\x05dv\xf4\xc1\x8cCL\x82\x91M\x98~sv\xc5\x15\xbb\x9a\x81\xb2\xad7\xb2\xd3\xaaW\xef9K\xdf\x01\x00h\x95#\xfe/d\x9d\xea\x00\x00\x00\x00IEND\xaeB`\x82" qt_resource_name = b"\x00\x09\x09_\x97\x13\x00q\x00s\x00s\x00_\x00i\x00c\x00o\x00n\x00s\x00\x0a\x09$M%\x00q\x00d\x00a\x00r\x00k\x00s\x00t\x00y\x00l\x00e\x00\x09\x00(\xad#\x00s\x00t\x00y\x00l\x00e\x00.\x00q\x00s\x00s\x00\x02\x00\x00\x07\x83\x00r\x00c\x00\x11\x0a\xe5l\x07\x00r\x00a\x00d\x00i\x00o\x00_\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00.\x00p\x00n\x00g\x00\x09\x06\x98\x83'\x00c\x00l\x00o\x00s\x00e\x00.\x00p\x00n\x00g\x00\x11\x08\x8cj\xa7\x00H\x00s\x00e\x00p\x00a\x00r\x00t\x00o\x00o\x00l\x00b\x00a\x00r\x00.\x00p\x00n\x00g\x00\x1a\x01!\xebG\x00s\x00t\x00y\x00l\x00e\x00s\x00h\x00e\x00e\x00t\x00-\x00b\x00r\x00a\x00n\x00c\x00h\x00-\x00m\x00o\x00r\x00e\x00.\x00p\x00n\x00g\x00\x0a\x05\x95\xde'\x00u\x00n\x00d\x00o\x00c\x00k\x00.\x00p\x00n\x00g\x00\x13\x08\xc8\x96\xe7\x00r\x00a\x00d\x00i\x00o\x00_\x00u\x00n\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00.\x00p\x00n\x00g\x00\x15\x0f\xf3\xc0\x07\x00u\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x1f\x0a\xae'G\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00u\x00n\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x0f\x0c\xe2hg\x00t\x00r\x00a\x00n\x00s\x00p\x00a\x00r\x00e\x00n\x00t\x00.\x00p\x00n\x00g\x00\x16\x01u\xcc\x87\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00u\x00n\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00.\x00p\x00n\x00g\x00\x14\x0b\xc5\xd7\xc7\x00s\x00t\x00y\x00l\x00e\x00s\x00h\x00e\x00e\x00t\x00-\x00v\x00l\x00i\x00n\x00e\x00.\x00p\x00n\x00g\x00\x11\x08\x90\x94g\x00c\x00l\x00o\x00s\x00e\x00-\x00p\x00r\x00e\x00s\x00s\x00e\x00d\x00.\x00p\x00n\x00g\x00\x14\x07\xec\xd1\xc7\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00.\x00p\x00n\x00g\x00\x0e\x0e\xde\xfa\xc7\x00l\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\x00\x12\x07\x8f\x9d'\x00b\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00-\x00o\x00n\x00.\x00p\x00n\x00g\x00\x0f\x02\x9f\x05\x87\x00r\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\x00\x0e\x04\xa2\xfc\xa7\x00d\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\x00\x11\x08\xc4j\xa7\x00V\x00s\x00e\x00p\x00a\x00r\x00t\x00o\x00o\x00l\x00b\x00a\x00r\x00.\x00p\x00n\x00g\x00\x10\x01\x07J\xa7\x00V\x00m\x00o\x00v\x00e\x00t\x00o\x00o\x00l\x00b\x00a\x00r\x00.\x00p\x00n\x00g\x00\x19\x08>\xcc\x07\x00s\x00t\x00y\x00l\x00e\x00s\x00h\x00e\x00e\x00t\x00-\x00b\x00r\x00a\x00n\x00c\x00h\x00-\x00e\x00n\x00d\x00.\x00p\x00n\x00g\x00\x1c\x01\xe0J\x07\x00r\x00a\x00d\x00i\x00o\x00_\x00u\x00n\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x14\x06^,\x07\x00b\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00-\x00o\x00n\x00.\x00p\x00n\x00g\x00\x0f\x06S%\xa7\x00b\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\x00\x0c\x06A@\x87\x00s\x00i\x00z\x00e\x00g\x00r\x00i\x00p\x00.\x00p\x00n\x00g\x00\x10\x01\x00\xca\xa7\x00H\x00m\x00o\x00v\x00e\x00t\x00o\x00o\x00l\x00b\x00a\x00r\x00.\x00p\x00n\x00g\x00\x1c\x08?\xdag\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00u\x00n\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00f\x00o\x00c\x00u\x00s\x00.\x00p\x00n\x00g\x00\x0f\x01\xf4\x81G\x00c\x00l\x00o\x00s\x00e\x00-\x00h\x00o\x00v\x00e\x00r\x00.\x00p\x00n\x00g\x00\x18\x03\x8e\xdeg\x00r\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x1a\x0e\xbc\xc3g\x00r\x00a\x00d\x00i\x00o\x00_\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x17\x0c\xabQ\x07\x00d\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x11\x0b\xda0\xa7\x00b\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\x00\x1a\x01\x87\xaeg\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00i\x00n\x00d\x00e\x00t\x00e\x00r\x00m\x00i\x00n\x00a\x00t\x00e\x00.\x00p\x00n\x00g\x00\x17\x0ce\xce\x07\x00l\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\x00\x19\x0bYn\x87\x00r\x00a\x00d\x00i\x00o\x00_\x00u\x00n\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00f\x00o\x00c\x00u\x00s\x00.\x00p\x00n\x00g\x00\x1a\x05\x11\xe0\xe7\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00f\x00o\x00c\x00u\x00s\x00.\x00p\x00n\x00g\x00\x17\x0f\x1e\x9bG\x00r\x00a\x00d\x00i\x00o\x00_\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00f\x00o\x00c\x00u\x00s\x00.\x00p\x00n\x00g\x00 \x09\xd7\x1f\xa7\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00i\x00n\x00d\x00e\x00t\x00e\x00r\x00m\x00i\x00n\x00a\x00t\x00e\x00_\x00f\x00o\x00c\x00u\x00s\x00.\x00p\x00n\x00g\x00\x0c\x06\xe6\xe6g\x00u\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\x00\x1d\x09\x07\x81\x07\x00c\x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00c\x00h\x00e\x00c\x00k\x00e\x00d\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g" qt_resource_struct = b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x18\x00\x02\x00\x00\x00\x01\x00\x00\x00+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00J\x00\x02\x00\x00\x00'\x00\x00\x00\x04\x00\x00\x04P\x00\x00\x00\x00\x00\x01\x00\x00\xa2\x0d\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x00\x9b\xa3\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x00k\x87\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x00~\x1b\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\xb64\x00\x00\x03\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x9do\x00\x00\x04\xb4\x00\x00\x00\x00\x00\x01\x00\x00\xae?\x00\x00\x02\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x99\x97\x00\x00\x04\xd8\x00\x00\x00\x00\x00\x01\x00\x00\xb0\x99\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x9a;\x00\x00\x06J\x00\x00\x00\x00\x00\x01\x00\x00\xbb\xa7\x00\x00\x00\xf6\x00\x00\x00\x00\x00\x01\x00\x00lA\x00\x00\x042\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x88\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xa0\xde\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\xa0G\x00\x00\x00|\x00\x00\x00\x00\x00\x01\x00\x00h\x89\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\xcc\xef\x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00\x98\xfd\x00\x00\x02\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xba\x00\x00\x03j\x00\x00\x00\x00\x00\x01\x00\x00\x9c\x8b\x00\x00\x04v\x00\x00\x00\x00\x00\x01\x00\x00\xa2\xed\x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00j\xd7\x00\x00\x024\x00\x00\x00\x00\x00\x01\x00\x00\x8a`\x00\x00\x03\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x9a\xe4\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00n\x87\x00\x00\x07\x1c\x00\x00\x00\x00\x00\x01\x00\x00\xcd\x91\x00\x00\x06\xb8\x00\x00\x00\x00\x00\x01\x00\x00\xca\xe9\x00\x00\x01l\x00\x00\x00\x00\x00\x01\x00\x00r\x02\x00\x00\x00T\x00\x00\x00\x00\x00\x01\x00\x00d\xe0\x00\x00\x06\x12\x00\x00\x00\x00\x00\x01\x00\x00\xb8\xcf\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x89m\x00\x00\x05|\x00\x00\x00\x00\x00\x01\x00\x00\xb5\x90\x00\x00\x05\xde\x00\x00\x00\x00\x00\x01\x00\x00\xb8%\x00\x00\x05H\x00\x00\x00\x00\x00\x01\x00\x00\xb4\xe6\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00}T\x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xb1=\x00\x00\x02\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x98S\x00\x00\x06\x84\x00\x00\x00\x00\x00\x01\x00\x00\xc7@\x00\x00\x01<\x00\x00\x00\x00\x00\x01\x00\x00q_\x00\x00\x002\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00" -def qInitResources() -> None: +def qInitResources(): QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) -def qCleanupResources() -> None: +def qCleanupResources(): QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) qInitResources() diff --git a/toxygen/styles/style.qrc b/toxygen/styles/style.qrc index 7ceed90..ac14bc5 100644 --- a/toxygen/styles/style.qrc +++ b/toxygen/styles/style.qrc @@ -41,9 +41,6 @@ rc/radio_unchecked.png - dark_style.qss - - style.qss diff --git a/toxygen/styles/style.qss b/toxygen/styles/style.qss index ff9f614..a672d38 100644 --- a/toxygen/styles/style.qss +++ b/toxygen/styles/style.qss @@ -1,6 +1,1216 @@ -#searchLineEdit +/* + * The MIT License (MIT) + * + * Copyright (c) <2013-2014> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +QToolTip { - padding-left: 22px; + border: 1px solid #3A3939; + background-color: rgb(90, 102, 117);; + color: white; + padding: 1px; + opacity: 200; +} + +QWidget +{ + color: silver; + background-color: #302F2F; + selection-background-color: #A9A9A9; + selection-color: black; + background-clip: border; + border-image: none; + outline: 0; +} + +QWidget:item:hover +{ + background-color: #78879b; + color: black; +} + +QWidget:item:selected +{ + background-color: #A9A9A9; +} + +QProgressBar:horizontal { + border: 1px solid #3A3939; + text-align: center; + padding: 1px; + background: #201F1F; +} +QProgressBar::chunk:horizontal { + background-color: qlineargradient(spread:reflect, x1:1, y1:0.545, x2:1, y2:0, stop:0 rgba(28, 66, 111, 255), stop:1 rgba(37, 87, 146, 255)); +} + +QCheckBox:disabled +{ + color: #777777; +} +QCheckBox::indicator, +QGroupBox::indicator +{ + width: 18px; + height: 18px; +} +QGroupBox::indicator +{ + margin-left: 2px; +} + +QCheckBox::indicator:unchecked, +QCheckBox::indicator:unchecked:hover, +QGroupBox::indicator:unchecked, +QGroupBox::indicator:unchecked:hover +{ + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:focus, +QCheckBox::indicator:unchecked:pressed, +QGroupBox::indicator:unchecked:focus, +QGroupBox::indicator:unchecked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); +} + +QCheckBox::indicator:checked, +QCheckBox::indicator:checked:hover, +QGroupBox::indicator:checked, +QGroupBox::indicator:checked:hover +{ + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QCheckBox::indicator:checked:focus, +QCheckBox::indicator:checked:pressed, +QGroupBox::indicator:checked:focus, +QGroupBox::indicator:checked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_checked_focus.png); +} + +QCheckBox::indicator:indeterminate, +QCheckBox::indicator:indeterminate:hover, +QCheckBox::indicator:indeterminate:pressed +QGroupBox::indicator:indeterminate, +QGroupBox::indicator:indeterminate:hover, +QGroupBox::indicator:indeterminate:pressed +{ + image: url(:/qss_icons/rc/checkbox_indeterminate.png); +} + +QCheckBox::indicator:indeterminate:focus, +QGroupBox::indicator:indeterminate:focus +{ + image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); +} + +QCheckBox::indicator:checked:disabled, +QGroupBox::indicator:checked:disabled +{ + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +QCheckBox::indicator:unchecked:disabled, +QGroupBox::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QRadioButton +{ + spacing: 5px; + outline: none; + color: #bbb; + margin-bottom: 2px; +} + +QRadioButton:disabled +{ + color: #777777; +} +QRadioButton::indicator +{ + width: 21px; + height: 21px; +} + +QRadioButton::indicator:unchecked, +QRadioButton::indicator:unchecked:hover +{ + image: url(:/qss_icons/rc/radio_unchecked.png); +} + +QRadioButton::indicator:unchecked:focus, +QRadioButton::indicator:unchecked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_unchecked_focus.png); +} + +QRadioButton::indicator:checked, +QRadioButton::indicator:checked:hover +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked.png); +} + +QRadioButton::indicator:checked:focus, +QRadioButton::indicato::menu-arrowr:checked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked_focus.png); +} + +QRadioButton::indicator:indeterminate, +QRadioButton::indicator:indeterminate:hover, +QRadioButton::indicator:indeterminate:pressed +{ + image: url(:/qss_icons/rc/radio_indeterminate.png); +} + +QRadioButton::indicator:checked:disabled +{ + outline: none; + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QRadioButton::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + + +QMenuBar +{ + background-color: #302F2F; + color: silver; +} + +QMenuBar::item +{ + background: transparent; +} + +QMenuBar::item:selected +{ + background: transparent; + border: 1px solid #A9A9A9; +} + +QMenuBar::item:pressed +{ + border: 1px solid #3A3939; + background-color: #A9A9A9; + color: black; + margin-bottom:-1px; + padding-bottom:1px; +} + +QMenu +{ + border: 1px solid #3A3939; + color: silver; + margin: 2px; +} + +QMenu::icon +{ + margin: 5px; +} + +QMenu::item +{ + padding: 5px 30px 5px 30px; + margin-left: 5px; + border: 1px solid transparent; /* reserve space for selection border */ +} + +QMenu::item:selected +{ + color: black; +} + +QMenu::separator { + height: 2px; + background: lightblue; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::indicator { + width: 18px; + height: 18px; +} + +/* non-exclusive indicator = check box style indicator + (see QActionGroup::setExclusive) */ +QMenu::indicator:non-exclusive:unchecked { + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QMenu::indicator:non-exclusive:unchecked:selected { + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QMenu::indicator:non-exclusive:checked { + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QMenu::indicator:non-exclusive:checked:selected { + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ +QMenu::indicator:exclusive:unchecked { + image: url(:/qss_icons/rc/radio_unchecked.png); +} + +QMenu::indicator:exclusive:unchecked:selected { + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + +QMenu::indicator:exclusive:checked { + image: url(:/qss_icons/rc/radio_checked.png); +} + +QMenu::indicator:exclusive:checked:selected { + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QMenu::right-arrow { + margin: 5px; + image: url(:/qss_icons/rc/right_arrow.png) +} + + +QWidget:disabled +{ + color: #404040; + background-color: #302F2F; +} + +QAbstractItemView +{ + alternate-background-color: #3A3939; + color: silver; + border: 1px solid 3A3939; + border-radius: 2px; + padding: 1px; +} + +QWidget:focus, QMenuBar:focus +{ + border: 1px solid #78879b; +} + +QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus +{ + border: none; +} + +QLineEdit +{ + background-color: #201F1F; + padding: 2px; + border-style: solid; + border: 1px solid #3A3939; + border-radius: 2px; + color: silver; +} + +QGroupBox { + border:1px solid #3A3939; + border-radius: 2px; + margin-top: 20px; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top center; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; +} + +QAbstractScrollArea +{ + border-radius: 2px; + border: 1px solid #3A3939; + background-color: transparent; +} + +QScrollBar:horizontal +{ + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent #2A2929; + border-radius: 4px; + background-color: #2A2929; +} + +QScrollBar::handle:horizontal +{ + background-color: #605F5F; + min-width: 5px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/right_arrow_disabled.png); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/left_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/right_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/left_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal +{ + background: none; +} + + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + background-color: #2A2929; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent #2A2929; + border-radius: 4px; +} + +QScrollBar::handle:vertical +{ + background-color: #605F5F; + min-height: 5px; + border-radius: 4px; +} + +QScrollBar::sub-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/up_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/down_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on +{ + + border-image: url(:/qss_icons/rc/up_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on +{ + border-image: url(:/qss_icons/rc/down_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical +{ + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #201F1F; + color: silver; + border: 1px solid #3A3939; +} + +QPlainTextEdit +{ + background-color: #201F1F;; + color: silver; + border-radius: 2px; + border: 1px solid #3A3939; +} + +QHeaderView::section +{ + background-color: #3A3939; + color: silver; + padding-left: 4px; + border: 1px solid #6c6c6c; +} + +QSizeGrip { + image: url(:/qss_icons/rc/sizegrip.png); + width: 12px; + height: 12px; +} + + +QMainWindow::separator +{ + background-color: #302F2F; + color: white; + padding-left: 4px; + spacing: 2px; + border: 1px dashed #3A3939; +} + +QMainWindow::separator:hover +{ + + background-color: #787876; + color: white; + padding-left: 4px; + border: 1px solid #3A3939; + spacing: 2px; +} + + +QMenu::separator +{ + height: 1px; + background-color: #3A3939; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + + +QFrame +{ + border-radius: 2px; + border: 1px solid #444; +} + +QFrame[frameShape="0"] +{ + border-radius: 2px; + border: 1px transparent #444; +} + +QStackedWidget +{ + border: 1px transparent black; +} + +QToolBar { + border: 1px transparent #393838; + background: 1px solid #302F2F; + font-weight: bold; +} + +QToolBar::handle:horizontal { + image: url(:/qss_icons/rc/Hmovetoolbar.png); +} +QToolBar::handle:vertical { + image: url(:/qss_icons/rc/Vmovetoolbar.png); +} +QToolBar::separator:horizontal { + image: url(:/qss_icons/rc/Hsepartoolbar.png); +} +QToolBar::separator:vertical { + image: url(:/qss_icons/rc/Vsepartoolbars.png); +} + +QPushButton +{ + color: silver; + background-color: #302F2F; + border-width: 1px; + border-color: #4A4949; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + border-radius: 2px; + outline: none; +} + +QPushButton:focus +{ + border-width: 1px; + border-color: #4A4949; + border-style: solid; +} + +QPushButton:disabled +{ + background-color: #302F2F; + border-width: 1px; + border-color: #3A3939; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + /*border-radius: 2px;*/ + color: #454545; +} + +QComboBox +{ + selection-background-color: #A9A9A9; + background-color: #201F1F; + border-style: solid; + border: 1px solid #3A3939; + border-radius: 2px; + padding: 2px; + min-width: 75px; +} + +QPushButton:hover +{ + background-color: #3d8ec9; + color: white; +} + +QComboBox:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:hover,QTreeView:hover +{ + border: 1px solid #78879b; + color: silver; +} + +QComboBox:on +{ + background-color: #626873; + padding-top: 3px; + padding-left: 4px; + selection-background-color: #4a4a4a; +} + +QComboBox QAbstractItemView +{ + background-color: #201F1F; + border-radius: 2px; + border: 1px solid #444; + selection-background-color: #A9A9A9; +} + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); +} + +QComboBox::down-arrow:on, QComboBox::down-arrow:hover, +QComboBox::down-arrow:focus +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + +QAbstractSpinBox { + padding-top: 2px; + padding-bottom: 2px; + border: 1px solid #3A3939; + background-color: #201F1F; + color: silver; + border-radius: 2px; + min-width: 75px; +} + +QAbstractSpinBox:up-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; +} + +QAbstractSpinBox:down-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; +} + +QAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off { + image: url(:/qss_icons/rc/up_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::up-arrow:hover +{ + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::down-arrow:hover +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + + +QLabel +{ + border: 0px solid black; + background-color: transparent; +} + +QTabWidget{ + border: 1px transparent black; +} + +QTabWidget::pane { + border: 1px solid #444; + border-radius: 3px; + padding: 3px; +} + +QTabBar +{ + qproperty-drawBase: 0; + left: 5px; /* move to the right by 5px */ +} + +QTabBar:focus +{ + border: 0px transparent black; +} + +QTabBar::close-button { + image: url(:/qss_icons/rc/close.png); + background: transparent; +} + +QTabBar::close-button:hover +{ + image: url(:/qss_icons/rc/close-hover.png); + background: transparent; +} + +QTabBar::close-button:pressed { + image: url(:/qss_icons/rc/close-pressed.png); + background: transparent; +} + +/* TOP TABS */ +QTabBar::tab:top { + color: #b1b1b1; + border: 1px solid #4A4949; + border-bottom: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-bottom: 1px transparent #4A4949; + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +QTabBar::tab:top:!selected:hover { + background-color: #48576b; +} + +/* BOTTOM TABS */ +QTabBar::tab:bottom { + color: #b1b1b1; + border: 1px solid #4A4949; + border-top: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-top: 1px transparent #4A4949; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +QTabBar::tab:bottom:!selected:hover { + background-color: #78879b; +} + +/* LEFT TABS */ +QTabBar::tab:left { + color: #b1b1b1; + border: 1px solid #4A4949; + border-left: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-right: 1px transparent #4A4949; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +QTabBar::tab:left:!selected:hover { + background-color: #48576b; +} + + +/* RIGHT TABS */ +QTabBar::tab:right { + color: #b1b1b1; + border: 1px solid #4A4949; + border-right: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-right: 1px transparent #4A4949; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +QTabBar::tab:right:!selected:hover { + background-color: #48576b; +} + +QTabBar QToolButton::right-arrow:enabled { + image: url(:/qss_icons/rc/right_arrow.png); + } + + QTabBar QToolButton::left-arrow:enabled { + image: url(:/qss_icons/rc/left_arrow.png); + } + +QTabBar QToolButton::right-arrow:disabled { + image: url(:/qss_icons/rc/right_arrow_disabled.png); + } + + QTabBar QToolButton::left-arrow:disabled { + image: url(:/qss_icons/rc/left_arrow_disabled.png); + } + + +QDockWidget { + border: 1px solid #403F3F; + titlebar-close-icon: url(:/qss_icons/rc/close.png); + titlebar-normal-icon: url(:/qss_icons/rc/undock.png); +} + +QDockWidget::close-button, QDockWidget::float-button { + border: 1px solid transparent; + border-radius: 2px; + background: transparent; +} + +QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background: rgba(255, 255, 255, 10); +} + +QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + padding: 1px -1px -1px 1px; + background: rgba(255, 255, 255, 10); +} + +QTreeView, QListView +{ + border: 1px solid #444; + background-color: #201F1F; +} + +QTreeView:branch:selected, QTreeView:branch:hover +{ + background: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:!adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_closed.png); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_open.png); +} + +QTreeView::branch:has-children:!has-siblings:closed:hover, +QTreeView::branch:closed:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_closed-on.png); + } + +QTreeView::branch:open:has-children:!has-siblings:hover, +QTreeView::branch:open:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_open-on.png); + } + +QListView::item:!selected:hover, QListView::item:!selected:hover, QTreeView::item:!selected:hover { + background: rgba(0, 0, 0, 0); + outline: 0; + color: #FFFFFF +} + +QListView::item:selected:hover, QListView::item:selected:hover, QTreeView::item:selected:hover { + background: #3d8ec9; + color: #FFFFFF; +} + +QSlider::groove:horizontal { + border: 1px solid #3A3939; + height: 8px; + background: #201F1F; + margin: 2px 0; + border-radius: 2px; +} + +QSlider::handle:horizontal { + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 silver, stop: 0.2 #a8a8a8, stop: 1 #727272); + border: 1px solid #3A3939; + width: 14px; + height: 14px; + margin: -4px 0; + border-radius: 2px; +} + +QSlider::groove:vertical { + border: 1px solid #3A3939; + width: 8px; + background: #201F1F; + margin: 0 0px; + border-radius: 2px; +} + +QSlider::handle:vertical { + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 silver, + stop: 0.2 #a8a8a8, stop: 1 #727272); + border: 1px solid #3A3939; + width: 14px; + height: 14px; + margin: 0 -4px; + border-radius: 2px; +} + +QToolButton { + background-color: transparent; + border: 1px transparent #4A4949; + border-radius: 2px; + margin: 3px; + padding: 3px; +} + +QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ + padding-right: 20px; /* make way for the popup button */ + border: 1px transparent #4A4949; + border-radius: 5px; +} + +QToolButton[popupMode="2"] { /* only for InstantPopup */ + padding-right: 10px; /* make way for the popup button */ + border: 1px transparent #4A4949; +} + + +QToolButton:hover, QToolButton::menu-button:hover { + background-color: transparent; + border: 1px solid #78879b; +} + +QToolButton:checked, QToolButton:pressed, + QToolButton::menu-button:pressed { + background-color: #4A4949; + border: 1px solid #78879b; +} + +/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ +QToolButton::menu-indicator { + image: url(:/qss_icons/rc/down_arrow.png); + top: -7px; left: -2px; /* shift it a bit */ +} + +/* the subcontrols below are used only in the MenuButtonPopup mode */ +QToolButton::menu-button { + border: 1px transparent #4A4949; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border = 20px allocated above */ + width: 16px; + outline: none; +} + +QToolButton::menu-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QToolButton::menu-arrow:open { + top: 1px; left: 1px; /* shift it a bit */ + border: 1px solid #3A3939; +} + +QPushButton::menu-indicator { + subcontrol-origin: padding; + subcontrol-position: bottom right; + left: 8px; +} + +QTableView +{ + border: 1px solid #444; + gridline-color: #6c6c6c; + background-color: #201F1F; +} + + +QTableView, QHeaderView +{ + border-radius: 0px; +} + +QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { + background: #78879b; + color: #FFFFFF; +} + +QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { + background: #3d8ec9; + color: #FFFFFF; +} + + +QHeaderView +{ + border: 1px transparent; + border-radius: 2px; + margin: 0px; + padding: 0px; +} + +QHeaderView::section { + background-color: #3A3939; + color: silver; + padding: 4px; + border: 1px solid #6c6c6c; + border-radius: 0px; + text-align: center; +} + +QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one +{ + border-top: 1px solid #6c6c6c; +} + +QHeaderView::section::vertical +{ + border-top: transparent; +} + +QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one +{ + border-left: 1px solid #6c6c6c; +} + +QHeaderView::section::horizontal +{ + border-left: transparent; +} + + +QHeaderView::section:checked + { + color: white; + background-color: #5A5959; + } + + /* style the sort indicator */ +QHeaderView::down-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QHeaderView::up-arrow { + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QTableCornerButton::section { + background-color: #3A3939; + border: 1px solid #3A3939; + border-radius: 2px; +} + +QToolBox { + padding: 3px; + border: 1px transparent black; +} + +QToolBox::tab { + color: #b1b1b1; + background-color: #302F2F; + border: 1px solid #4A4949; + border-bottom: 1px transparent #302F2F; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + + QToolBox::tab:selected { /* italicize selected tabs */ + font: italic; + background-color: #302F2F; + border-color: #3d8ec9; + } + +QStatusBar::item { + border: 1px solid #3A3939; + border-radius: 2px; + } + + +QFrame[height="3"], QFrame[width="3"] { + background-color: #444; +} + + +QSplitter::handle { + border: 1px dashed #3A3939; +} + +QSplitter::handle:hover { + background-color: #787876; + border: 1px solid #3A3939; +} + +QSplitter::handle:horizontal { + width: 1px; +} + +QSplitter::handle:vertical { + height: 1px; +} + +MessageItem +{ + border: none; } MessageEdit @@ -23,18 +1233,57 @@ MessageEdit:hover border: none; } -MessageEdit +QListWidget QPushButton +{ + background-color: transparent; + border: none; +} + +QPushButton:hover +{ + background-color: #4A4949; +} + +#messages:item:selected +{ + background-color: #1E90FF; +} + +MessageEdit { background-color: transparent; } -#warningLabel +#messages:item:selected QListWidgetItem { - color: #BC1C1C; + background-color: #1E90FF; } -#groupInvitesPushButton +#friends_list:item:selected { - background-color: #009c00; + background-color: #333333; } +#toxygen +{ + color: #A9A9A9; +} + +QCheckBox +{ + spacing: 5px; + outline: none; + color: #bbb; + margin-bottom: 2px; + text-align: center; +} + +QListWidget > QLabel +{ + color: #A9A9A9; +} + +#contact_name +{ + padding-left: 22px; +} \ No newline at end of file diff --git a/toxygen/tests/README.txt b/toxygen/tests/README.txt deleted file mode 100644 index b2c475f..0000000 --- a/toxygen/tests/README.txt +++ /dev/null @@ -1 +0,0 @@ -unused diff --git a/toxygen/tests/__init__.py b/toxygen/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/tests/conference_tests.py.bak b/toxygen/tests/conference_tests.py.bak deleted file mode 100644 index 8da5912..0000000 --- a/toxygen/tests/conference_tests.py.bak +++ /dev/null @@ -1,151 +0,0 @@ -if False: - @unittest.skip # to yet - def test_conference(self): - """ - t:group_new - t:conference_delete - t:conference_get_chatlist_size - t:conference_get_chatlist - t:conference_send_message - """ - bob_addr = self.bob.self_get_address() - alice_addr = self.alice.self_get_address() - - self.abid = self.alice.friend_by_public_key(bob_addr) - self.baid = self.bob.friend_by_public_key(alice_addr) - - assert self.bob_just_add_alice_as_friend() - - #: Test group add - privacy_state = enums.TOX_GROUP_PRIVACY_STATE['PUBLIC'] - group_name = 'test_group' - nick = 'test_nick' - status = None # dunno - self.group_id = self.bob.group_new(privacy_state, group_name, nick, status) - # :return group number on success, UINT32_MAX on failure. - assert self.group_id >= 0 - - self.loop(50) - - BID = self.abid - - def alices_on_conference_invite(self, fid, type_, data): - assert fid == BID - assert type_ == 0 - gn = self.conference_join(fid, data) - assert type_ == self.conference_get_type(gn) - self.gi = True - - def alices_on_conference_peer_list_changed(self, gid): - logging.debug("alices_on_conference_peer_list_changed") - assert gid == self.group_id - self.gn = True - - try: - AliceTox.on_conference_invite = alices_on_conference_invite - AliceTox.on_conference_peer_list_changed = alices_on_conference_peer_list_changed - - self.alice.gi = False - self.alice.gn = False - - self.wait_ensure_exec(self.bob.conference_invite, (self.aid, self.group_id)) - - assert self.wait_callback_trues(self.alice, ['gi', 'gn']) - except AssertionError as e: - raise - finally: - AliceTox.on_conference_invite = Tox.on_conference_invite - AliceTox.on_conference_peer_list_change = Tox.on_conference_peer_list_changed - - #: Test group number of peers - self.loop(50) - assert self.bob.conference_peer_count(self.group_id) == 2 - - #: Test group peername - self.alice.self_set_name('Alice') - self.bob.self_set_name('Bob') - - def alices_on_conference_peer_list_changed(self, gid): - logging.debug("alices_on_conference_peer_list_changed") - self.gn = True - try: - AliceTox.on_conference_peer_list_changed = alices_on_conference_peer_list_changed - self.alice.gn = False - - assert self.wait_callback_true(self.alice, 'gn') - except AssertionError as e: - raise - finally: - AliceTox.on_conference_peer_list_changed = Tox.on_conference_peer_list_changed - - peernames = [self.bob.conference_peer_get_name(self.group_id, i) for i in - range(self.bob.conference_peer_count(self.group_id))] - assert 'Alice' in peernames - assert 'Bob' in peernames - - #: Test title change - self.bob.conference_set_title(self.group_id, 'My special title') - assert self.bob.conference_get_title(self.group_id) == 'My special title' - - #: Test group message - AID = self.aid - BID = self.bid - MSG = 'Group message test' - - def alices_on_conference_message(self, gid, fgid, msg_type, message): - logging.debug("alices_on_conference_message" +repr(message)) - if fgid == AID: - assert gid == self.group_id - assert str(message, 'UTF-8') == MSG - self.alice.gm = True - - try: - AliceTox.on_conference_message = alices_on_conference_message - self.alice.gm = False - - self.wait_ensure_exec(self.bob.conference_send_message, ( - self.group_id, TOX_MESSAGE_TYPE['NORMAL'], MSG)) - assert self.wait_callback_true(self.alice, 'gm') - except AssertionError as e: - raise - finally: - AliceTox.on_conference_message = Tox.on_conference_message - - #: Test group action - AID = self.aid - BID = self.bid - MSG = 'Group action test' - - def on_conference_action(self, gid, fgid, msg_type, action): - if fgid == AID: - assert gid == self.group_id - assert msg_type == TOX_MESSAGE_TYPE['ACTION'] - assert str(action, 'UTF-8') == MSG - self.ga = True - - try: - AliceTox.on_conference_message = on_conference_action - self.alice.ga = False - - self.wait_ensure_exec(self.bob.conference_send_message, - (self.group_id, TOX_MESSAGE_TYPE['ACTION'], MSG)) - - assert self.wait_callback_true(self.alice, 'ga') - - #: Test chatlist - assert len(self.bob.conference_get_chatlist()) == self.bob.conference_get_chatlist_size(), \ - print(len(self.bob.conference_get_chatlist()), '!=', self.bob.conference_get_chatlist_size()) - assert len(self.alice.conference_get_chatlist()) == self.bob.conference_get_chatlist_size(), \ - print(len(self.alice.conference_get_chatlist()), '!=', self.bob.conference_get_chatlist_size()) - assert self.bob.conference_get_chatlist_size() == 1, \ - self.bob.conference_get_chatlist_size() - self.bob.conference_delete(self.group_id) - assert self.bob.conference_get_chatlist_size() == 0, \ - self.bob.conference_get_chatlist_size() - - except AssertionError as e: - raise - finally: - AliceTox.on_conference_message = Tox.on_conference_message - - diff --git a/toxygen/tests/socks.py b/toxygen/tests/socks.py deleted file mode 100644 index f9f730e..0000000 --- a/toxygen/tests/socks.py +++ /dev/null @@ -1,393 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -"""SocksiPy - Python SOCKS module. -Version 1.00 - -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. - - -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. - -""" - -""" - -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) -for use in PyLoris (http://pyloris.sourceforge.net/) - -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge - -Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/) - -""" - -import socket -import struct -import sys - -PROXY_TYPE_SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = 2 -PROXY_TYPE_HTTP = 3 - -_defaultproxy = None -_orgsocket = socket.socket - -class ProxyError(Exception): pass -class GeneralProxyError(ProxyError): pass -class Socks5AuthError(ProxyError): pass -class Socks5Error(ProxyError): pass -class Socks4Error(ProxyError): pass -class HTTPError(ProxyError): pass - -_generalerrors = ("success", - "invalid data", - "not connected", - "not available", - "bad proxy type", - "bad input") - -_socks5errors = ("succeeded", - "general SOCKS server failure", - "connection not allowed by ruleset", - "Network unreachable", - "Host unreachable", - "Connection refused", - "TTL expired", - "Command not supported", - "Address type not supported", - "Unknown error") - -_socks5autherrors = ("succeeded", - "authentication is required", - "all offered authentication methods were rejected", - "unknown username or invalid password", - "unknown error") - -_socks4errors = ("request granted", - "request rejected or failed", - "request rejected because SOCKS server cannot connect to identd on the client", - "request rejected because the client program and identd report different user-ids", - "unknown error") - -def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. - """ - global _defaultproxy - _defaultproxy = (proxytype, addr, port, rdns, username, password) - -def wrapmodule(module): - """wrapmodule(module) - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using setdefaultproxy(...) first. - This will only work on modules that import socket directly into the namespace; - most of the Python Standard Library falls into this category. - """ - if _defaultproxy != None: - module.socket.socket = socksocket - else: - raise GeneralProxyError((4, "no proxy specified")) - -class socksocket(socket.socket): - """socksocket([family[, type[, proto]]]) -> socket object - Open a SOCKS enabled socket. The parameters are the same as - those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET, type=SOCK_STREAM and proto=0. - """ - - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): - _orgsocket.__init__(self, family, type, proto, _sock) - if _defaultproxy != None: - self.__proxy = _defaultproxy - else: - self.__proxy = (None, None, None, None, None, None) - self.__proxysockname = None - self.__proxypeername = None - - def __recvall(self, count): - """__recvall(count) -> data - Receive EXACTLY the number of bytes requested from the socket. - Blocks until the required number of bytes have been received. - """ - data = self.recv(count) - while len(data) < count: - d = self.recv(count-len(data)) - if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) - data = data + d - return data - - def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets the proxy to be used. - proxytype - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP - addr - The address of the server (IP or DNS). - port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be preformed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. - username - Username to authenticate with to the server. - The default is no authentication. - password - Password to authenticate with to the server. - Only relevant when username is also provided. - """ - self.__proxy = (proxytype, addr, port, rdns, username, password) - - def __negotiatesocks5(self, destaddr, destport): - """__negotiatesocks5(self,destaddr,destport) - Negotiates a connection through a SOCKS5 server. - """ - # First we'll send the authentication packages we support. - if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): - # The username/password details were supplied to the - # setproxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) - # We'll receive the server's response to determine which - # method was selected - chosenauth = self.__recvall(2) - if chosenauth[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - # Check the chosen authentication method - if chosenauth[1:2] == chr(0x00).encode(): - # No authentication is required - pass - elif chosenauth[1:2] == chr(0x02).encode(): - # Okay, we need to perform a basic username/password - # authentication. - self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) - authstat = self.__recvall(2) - if authstat[0:1] != chr(0x01).encode(): - # Bad response - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if authstat[1:2] != chr(0x00).encode(): - # Authentication failed - self.close() - raise Socks5AuthError((3, _socks5autherrors[3])) - # Authentication succeeded - else: - # Reaching here is always bad - self.close() - if chosenauth[1] == chr(0xFF).encode(): - raise Socks5AuthError((2, _socks5autherrors[2])) - else: - raise GeneralProxyError((1, _generalerrors[1])) - # Now we can request the actual connection - req = struct.pack('BBB', 0x05, 0x01, 0x00) - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - ipaddr = socket.inet_aton(destaddr) - req = req + chr(0x01).encode() + ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. - if self.__proxy[3]: - # Resolve remotely - ipaddr = None - if type(destaddr) != type(b''): # python3 - destaddr_bytes = destaddr.encode(encoding='idna') - else: - destaddr_bytes = destaddr - req = req + chr(0x03).encode() + chr(len(destaddr_bytes)).encode() + destaddr_bytes - else: - # Resolve locally - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) - self.sendall(req) - # Get the response - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2])<=8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - boundaddr = self.__recvall(4) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - boundaddr = self.__recvall(ord(resp[4:5])) - else: - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - self.__proxysockname = (boundaddr, boundport) - if ipaddr != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def getproxysockname(self): - """getsockname() -> address info - Returns the bound IP address and port number at the proxy. - """ - return self.__proxysockname - - def getproxypeername(self): - """getproxypeername() -> address info - Returns the IP and port number of the proxy. - """ - return _orgsocket.getpeername(self) - - def getpeername(self): - """getpeername() -> address info - Returns the IP address and port number of the destination - machine (note: getproxypeername returns the proxy) - """ - return self.__proxypeername - - def __negotiatesocks4(self,destaddr,destport): - """__negotiatesocks4(self,destaddr,destport) - Negotiates a connection through a SOCKS4 server. - """ - # Check if the destination address provided is an IP address - rmtrslv = False - try: - ipaddr = socket.inet_aton(destaddr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if self.__proxy[3]: - ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) - rmtrslv = True - else: - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - # Construct the request packet - req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr - # The username parameter is considered userid for SOCKS4 - if self.__proxy[4] != None: - req = req + self.__proxy[4] - req = req + chr(0x00).encode() - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if rmtrslv: - req = req + destaddr + chr(0x00).encode() - self.sendall(req) - # Get the response from the server - resp = self.__recvall(8) - if resp[0:1] != chr(0x00).encode(): - # Bad data - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - if resp[1:2] != chr(0x5A).encode(): - # Server returned an error - self.close() - if ord(resp[1:2]) in (91, 92, 93): - self.close() - raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) - else: - raise Socks4Error((94, _socks4errors[4])) - # Get the bound address/port - self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) - if rmtrslv != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def __negotiatehttp(self, destaddr, destport): - """__negotiatehttp(self,destaddr,destport) - Negotiates a connection through an HTTP server. - """ - # If we need to resolve locally, we do this now - if not self.__proxy[3]: - addr = socket.gethostbyname(destaddr) - else: - addr = destaddr - self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode()) - # We read the response until we get the string "\r\n\r\n" - resp = self.recv(1) - while resp.find("\r\n\r\n".encode()) == -1: - recv = self.recv(1) - if not recv: - raise GeneralProxyError((1, _generalerrors[1])) - resp = resp + recv - # We just need the first line to check if the connection - # was successful - statusline = resp.splitlines()[0].split(" ".encode(), 2) - if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - try: - statuscode = int(statusline[1]) - except ValueError: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if statuscode != 200: - self.close() - raise HTTPError((statuscode, statusline[2])) - self.__proxysockname = ("0.0.0.0", 0) - self.__proxypeername = (addr, destport) - - def connect(self, destpair): - """connect(self, despair) - Connects to the specified destination through a proxy. - destpar - A tuple of the IP/DNS address and the port number. - (identical to socket's connect). - To select the proxy server use setproxy(). - """ - # Do a minimal input check first - if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): - raise GeneralProxyError((5, _generalerrors[5])) - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = int(self.__proxy[2]) - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks5(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_SOCKS4: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatesocks4(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 8080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatehttp(destpair[0], destpair[1]) - elif self.__proxy[0] == None: - _orgsocket.connect(self, (destpair[0], destpair[1])) - else: - raise GeneralProxyError((4, _generalerrors[4])) diff --git a/toxygen/tests/test_gdb.py b/toxygen/tests/test_gdb.py deleted file mode 100644 index 584987a..0000000 --- a/toxygen/tests/test_gdb.py +++ /dev/null @@ -1,938 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -# Verify that gdb can pretty-print the various PyObject* types -# -# The code for testing gdb was adapted from similar work in Unladen Swallow's -# Lib/test/test_jit_gdb.py - -import locale -import os -import re -import subprocess -import sys -import sysconfig -import textwrap -import unittest - -# Is this Python configured to support threads? -try: - import _thread -except ImportError: - _thread = None - -from test import support -from test.support import run_unittest, findfile, python_is_optimized - -def get_gdb_version(): - try: - proc = subprocess.Popen(["gdb", "-nx", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) - with proc: - version = proc.communicate()[0] - except OSError: - # This is what "no gdb" looks like. There may, however, be other - # errors that manifest this way too. - raise unittest.SkipTest("Couldn't find gdb on the path") - - # Regex to parse: - # 'GNU gdb (GDB; SUSE Linux Enterprise 12) 7.7\n' -> 7.7 - # 'GNU gdb (GDB) Fedora 7.9.1-17.fc22\n' -> 7.9 - # 'GNU gdb 6.1.1 [FreeBSD]\n' -> 6.1 - # 'GNU gdb (GDB) Fedora (7.5.1-37.fc18)\n' -> 7.5 - match = re.search(r"^GNU gdb.*?\b(\d+)\.(\d+)", version) - if match is None: - raise Exception("unable to parse GDB version: %r" % version) - return (version, int(match.group(1)), int(match.group(2))) - -gdb_version, gdb_major_version, gdb_minor_version = get_gdb_version() -if gdb_major_version < 7: - raise unittest.SkipTest("gdb versions before 7.0 didn't support python " - "embedding. Saw %s.%s:\n%s" - % (gdb_major_version, gdb_minor_version, - gdb_version)) - -if not sysconfig.is_python_build(): - raise unittest.SkipTest("test_gdb only works on source builds at the moment.") - -# Location of custom hooks file in a repository checkout. -checkout_hook_path = os.path.join(os.path.dirname(sys.executable), - 'python-gdb.py') - -PYTHONHASHSEED = '123' - -def run_gdb(*args, **env_vars): - """Runs gdb in --batch mode with the additional arguments given by *args. - - Returns its (stdout, stderr) decoded from utf-8 using the replace handler. - """ - if env_vars: - env = os.environ.copy() - env.update(env_vars) - else: - env = None - # -nx: Do not execute commands from any .gdbinit initialization files - # (issue #22188) - base_cmd = ('gdb', '--batch', '-nx') - if (gdb_major_version, gdb_minor_version) >= (7, 4): - base_cmd += ('-iex', 'add-auto-load-safe-path ' + checkout_hook_path) - proc = subprocess.Popen(base_cmd + args, - # Redirect stdin to prevent GDB from messing with - # the terminal settings - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env) - with proc: - out, err = proc.communicate() - return out.decode('utf-8', 'replace'), err.decode('utf-8', 'replace') - -# Verify that "gdb" was built with the embedded python support enabled: -gdbpy_version, _ = run_gdb("--eval-command=python import sys; print(sys.version_info)") -if not gdbpy_version: - raise unittest.SkipTest("gdb not built with embedded python support") - -# Verify that "gdb" can load our custom hooks, as OS security settings may -# disallow this without a customized .gdbinit. -_, gdbpy_errors = run_gdb('--args', sys.executable) -if "auto-loading has been declined" in gdbpy_errors: - msg = "gdb security settings prevent use of custom hooks: " - raise unittest.SkipTest(msg + gdbpy_errors.rstrip()) - -def gdb_has_frame_select(): - # Does this build of gdb have gdb.Frame.select ? - stdout, _ = run_gdb("--eval-command=python print(dir(gdb.Frame))") - m = re.match(r'.*\[(.*)\].*', stdout) - if not m: - raise unittest.SkipTest("Unable to parse output from gdb.Frame.select test") - gdb_frame_dir = m.group(1).split(', ') - return "'select'" in gdb_frame_dir - -HAS_PYUP_PYDOWN = gdb_has_frame_select() - -BREAKPOINT_FN='builtin_id' - -@unittest.skipIf(support.PGO, "not useful for PGO") -class DebuggerTests(unittest.TestCase): - - """Test that the debugger can debug Python.""" - - def get_stack_trace(self, source=None, script=None, - breakpoint=BREAKPOINT_FN, - cmds_after_breakpoint=None, - import_site=False): - ''' - Run 'python -c SOURCE' under gdb with a breakpoint. - - Support injecting commands after the breakpoint is reached - - Returns the stdout from gdb - - cmds_after_breakpoint: if provided, a list of strings: gdb commands - ''' - # We use "set breakpoint pending yes" to avoid blocking with a: - # Function "foo" not defined. - # Make breakpoint pending on future shared library load? (y or [n]) - # error, which typically happens python is dynamically linked (the - # breakpoints of interest are to be found in the shared library) - # When this happens, we still get: - # Function "textiowrapper_write" not defined. - # emitted to stderr each time, alas. - - # Initially I had "--eval-command=continue" here, but removed it to - # avoid repeated print breakpoints when traversing hierarchical data - # structures - - # Generate a list of commands in gdb's language: - commands = ['set breakpoint pending yes', - 'break %s' % breakpoint, - - # The tests assume that the first frame of printed - # backtrace will not contain program counter, - # that is however not guaranteed by gdb - # therefore we need to use 'set print address off' to - # make sure the counter is not there. For example: - # #0 in PyObject_Print ... - # is assumed, but sometimes this can be e.g. - # #0 0x00003fffb7dd1798 in PyObject_Print ... - 'set print address off', - - 'run'] - - # GDB as of 7.4 onwards can distinguish between the - # value of a variable at entry vs current value: - # http://sourceware.org/gdb/onlinedocs/gdb/Variables.html - # which leads to the selftests failing with errors like this: - # AssertionError: 'v@entry=()' != '()' - # Disable this: - if (gdb_major_version, gdb_minor_version) >= (7, 4): - commands += ['set print entry-values no'] - - if cmds_after_breakpoint: - commands += cmds_after_breakpoint - else: - commands += ['backtrace'] - - # print commands - - # Use "commands" to generate the arguments with which to invoke "gdb": - args = ['--eval-command=%s' % cmd for cmd in commands] - args += ["--args", - sys.executable] - args.extend(subprocess._args_from_interpreter_flags()) - - if not import_site: - # -S suppresses the default 'import site' - args += ["-S"] - - if source: - args += ["-c", source] - elif script: - args += [script] - - # print args - # print (' '.join(args)) - - # Use "args" to invoke gdb, capturing stdout, stderr: - out, err = run_gdb(*args, PYTHONHASHSEED=PYTHONHASHSEED) - - errlines = err.splitlines() - unexpected_errlines = [] - - # Ignore some benign messages on stderr. - ignore_patterns = ( - 'Function "%s" not defined.' % breakpoint, - 'Do you need "set solib-search-path" or ' - '"set sysroot"?', - # BFD: /usr/lib/debug/(...): unable to initialize decompress - # status for section .debug_aranges - 'BFD: ', - # ignore all warnings - 'warning: ', - ) - for line in errlines: - if not line: - continue - if not line.startswith(ignore_patterns): - unexpected_errlines.append(line) - - # Ensure no unexpected error messages: - self.assertEqual(unexpected_errlines, []) - return out - - def get_gdb_repr(self, source, - cmds_after_breakpoint=None, - import_site=False): - # Given an input python source representation of data, - # run "python -c'id(DATA)'" under gdb with a breakpoint on - # builtin_id and scrape out gdb's representation of the "op" - # parameter, and verify that the gdb displays the same string - # - # Verify that the gdb displays the expected string - # - # For a nested structure, the first time we hit the breakpoint will - # give us the top-level structure - - # NOTE: avoid decoding too much of the traceback as some - # undecodable characters may lurk there in optimized mode - # (issue #19743). - cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"] - gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN, - cmds_after_breakpoint=cmds_after_breakpoint, - import_site=import_site) - # gdb can insert additional '\n' and space characters in various places - # in its output, depending on the width of the terminal it's connected - # to (using its "wrap_here" function) - m = re.match(r'.*#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)\)\s+at\s+\S*Python/bltinmodule.c.*', - gdb_output, re.DOTALL) - if not m: - self.fail('Unexpected gdb output: %r\n%s' % (gdb_output, gdb_output)) - return m.group(1), gdb_output - - def assertEndsWith(self, actual, exp_end): - '''Ensure that the given "actual" string ends with "exp_end"''' - self.assertTrue(actual.endswith(exp_end), - msg='%r did not end with %r' % (actual, exp_end)) - - def assertMultilineMatches(self, actual, pattern): - m = re.match(pattern, actual, re.DOTALL) - if not m: - self.fail(msg='%r did not match %r' % (actual, pattern)) - - def get_sample_script(self): - return findfile('gdb_sample.py') - -class PrettyPrintTests(DebuggerTests): - def test_getting_backtrace(self): - gdb_output = self.get_stack_trace('id(42)') - self.assertTrue(BREAKPOINT_FN in gdb_output) - - def assertGdbRepr(self, val, exp_repr=None): - # Ensure that gdb's rendering of the value in a debugged process - # matches repr(value) in this process: - gdb_repr, gdb_output = self.get_gdb_repr('id(' + ascii(val) + ')') - if not exp_repr: - exp_repr = repr(val) - self.assertEqual(gdb_repr, exp_repr, - ('%r did not equal expected %r; full output was:\n%s' - % (gdb_repr, exp_repr, gdb_output))) - - def test_int(self): - 'Verify the pretty-printing of various int values' - self.assertGdbRepr(42) - self.assertGdbRepr(0) - self.assertGdbRepr(-7) - self.assertGdbRepr(1000000000000) - self.assertGdbRepr(-1000000000000000) - - def test_singletons(self): - 'Verify the pretty-printing of True, False and None' - self.assertGdbRepr(True) - self.assertGdbRepr(False) - self.assertGdbRepr(None) - - def test_dicts(self): - 'Verify the pretty-printing of dictionaries' - self.assertGdbRepr({}) - self.assertGdbRepr({'foo': 'bar'}, "{'foo': 'bar'}") - # Python preserves insertion order since 3.6 - self.assertGdbRepr({'foo': 'bar', 'douglas': 42}, "{'foo': 'bar', 'douglas': 42}") - - def test_lists(self): - 'Verify the pretty-printing of lists' - self.assertGdbRepr([]) - self.assertGdbRepr(list(range(5))) - - def test_bytes(self): - 'Verify the pretty-printing of bytes' - self.assertGdbRepr(b'') - self.assertGdbRepr(b'And now for something hopefully the same') - self.assertGdbRepr(b'string with embedded NUL here \0 and then some more text') - self.assertGdbRepr(b'this is a tab:\t' - b' this is a slash-N:\n' - b' this is a slash-R:\r' - ) - - self.assertGdbRepr(b'this is byte 255:\xff and byte 128:\x80') - - self.assertGdbRepr(bytes([b for b in range(255)])) - - def test_strings(self): - 'Verify the pretty-printing of unicode strings' - encoding = locale.getpreferredencoding() - def check_repr(text): - try: - text.encode(encoding) - printable = True - except UnicodeEncodeError: - self.assertGdbRepr(text, ascii(text)) - else: - self.assertGdbRepr(text) - - self.assertGdbRepr('') - self.assertGdbRepr('And now for something hopefully the same') - self.assertGdbRepr('string with embedded NUL here \0 and then some more text') - - # Test printing a single character: - # U+2620 SKULL AND CROSSBONES - check_repr('\u2620') - - # Test printing a Japanese unicode string - # (I believe this reads "mojibake", using 3 characters from the CJK - # Unified Ideographs area, followed by U+3051 HIRAGANA LETTER KE) - check_repr('\u6587\u5b57\u5316\u3051') - - # Test a character outside the BMP: - # U+1D121 MUSICAL SYMBOL C CLEF - # This is: - # UTF-8: 0xF0 0x9D 0x84 0xA1 - # UTF-16: 0xD834 0xDD21 - check_repr(chr(0x1D121)) - - def test_tuples(self): - 'Verify the pretty-printing of tuples' - self.assertGdbRepr(tuple(), '()') - self.assertGdbRepr((1,), '(1,)') - self.assertGdbRepr(('foo', 'bar', 'baz')) - - def test_sets(self): - 'Verify the pretty-printing of sets' - if (gdb_major_version, gdb_minor_version) < (7, 3): - self.skipTest("pretty-printing of sets needs gdb 7.3 or later") - self.assertGdbRepr(set(), "set()") - self.assertGdbRepr(set(['a']), "{'a'}") - # PYTHONHASHSEED is need to get the exact frozenset item order - if not sys.flags.ignore_environment: - self.assertGdbRepr(set(['a', 'b']), "{'a', 'b'}") - self.assertGdbRepr(set([4, 5, 6]), "{4, 5, 6}") - - # Ensure that we handle sets containing the "dummy" key value, - # which happens on deletion: - gdb_repr, gdb_output = self.get_gdb_repr('''s = set(['a','b']) -s.remove('a') -id(s)''') - self.assertEqual(gdb_repr, "{'b'}") - - def test_frozensets(self): - 'Verify the pretty-printing of frozensets' - if (gdb_major_version, gdb_minor_version) < (7, 3): - self.skipTest("pretty-printing of frozensets needs gdb 7.3 or later") - self.assertGdbRepr(frozenset(), "frozenset()") - self.assertGdbRepr(frozenset(['a']), "frozenset({'a'})") - # PYTHONHASHSEED is need to get the exact frozenset item order - if not sys.flags.ignore_environment: - self.assertGdbRepr(frozenset(['a', 'b']), "frozenset({'a', 'b'})") - self.assertGdbRepr(frozenset([4, 5, 6]), "frozenset({4, 5, 6})") - - def test_exceptions(self): - # Test a RuntimeError - gdb_repr, gdb_output = self.get_gdb_repr(''' -try: - raise RuntimeError("I am an error") -except RuntimeError as e: - id(e) -''') - self.assertEqual(gdb_repr, - "RuntimeError('I am an error',)") - - - # Test division by zero: - gdb_repr, gdb_output = self.get_gdb_repr(''' -try: - a = 1 / 0 -except ZeroDivisionError as e: - id(e) -''') - self.assertEqual(gdb_repr, - "ZeroDivisionError('division by zero',)") - - def test_modern_class(self): - 'Verify the pretty-printing of new-style class instances' - gdb_repr, gdb_output = self.get_gdb_repr(''' -class Foo: - pass -foo = Foo() -foo.an_int = 42 -id(foo)''') - m = re.match(r'', gdb_repr) - self.assertTrue(m, - msg='Unexpected new-style class rendering %r' % gdb_repr) - - def test_subclassing_list(self): - 'Verify the pretty-printing of an instance of a list subclass' - gdb_repr, gdb_output = self.get_gdb_repr(''' -class Foo(list): - pass -foo = Foo() -foo += [1, 2, 3] -foo.an_int = 42 -id(foo)''') - m = re.match(r'', gdb_repr) - - self.assertTrue(m, - msg='Unexpected new-style class rendering %r' % gdb_repr) - - def test_subclassing_tuple(self): - 'Verify the pretty-printing of an instance of a tuple subclass' - # This should exercise the negative tp_dictoffset code in the - # new-style class support - gdb_repr, gdb_output = self.get_gdb_repr(''' -class Foo(tuple): - pass -foo = Foo((1, 2, 3)) -foo.an_int = 42 -id(foo)''') - m = re.match(r'', gdb_repr) - - self.assertTrue(m, - msg='Unexpected new-style class rendering %r' % gdb_repr) - - def assertSane(self, source, corruption, exprepr=None): - '''Run Python under gdb, corrupting variables in the inferior process - immediately before taking a backtrace. - - Verify that the variable's representation is the expected failsafe - representation''' - if corruption: - cmds_after_breakpoint=[corruption, 'backtrace'] - else: - cmds_after_breakpoint=['backtrace'] - - gdb_repr, gdb_output = \ - self.get_gdb_repr(source, - cmds_after_breakpoint=cmds_after_breakpoint) - if exprepr: - if gdb_repr == exprepr: - # gdb managed to print the value in spite of the corruption; - # this is good (see http://bugs.python.org/issue8330) - return - - # Match anything for the type name; 0xDEADBEEF could point to - # something arbitrary (see http://bugs.python.org/issue8330) - pattern = '<.* at remote 0x-?[0-9a-f]+>' - - m = re.match(pattern, gdb_repr) - if not m: - self.fail('Unexpected gdb representation: %r\n%s' % \ - (gdb_repr, gdb_output)) - - def test_NULL_ptr(self): - 'Ensure that a NULL PyObject* is handled gracefully' - gdb_repr, gdb_output = ( - self.get_gdb_repr('id(42)', - cmds_after_breakpoint=['set variable v=0', - 'backtrace']) - ) - - self.assertEqual(gdb_repr, '0x0') - - def test_NULL_ob_type(self): - 'Ensure that a PyObject* with NULL ob_type is handled gracefully' - self.assertSane('id(42)', - 'set v->ob_type=0') - - def test_corrupt_ob_type(self): - 'Ensure that a PyObject* with a corrupt ob_type is handled gracefully' - self.assertSane('id(42)', - 'set v->ob_type=0xDEADBEEF', - exprepr='42') - - def test_corrupt_tp_flags(self): - 'Ensure that a PyObject* with a type with corrupt tp_flags is handled' - self.assertSane('id(42)', - 'set v->ob_type->tp_flags=0x0', - exprepr='42') - - def test_corrupt_tp_name(self): - 'Ensure that a PyObject* with a type with corrupt tp_name is handled' - self.assertSane('id(42)', - 'set v->ob_type->tp_name=0xDEADBEEF', - exprepr='42') - - def test_builtins_help(self): - 'Ensure that the new-style class _Helper in site.py can be handled' - - if sys.flags.no_site: - self.skipTest("need site module, but -S option was used") - - # (this was the issue causing tracebacks in - # http://bugs.python.org/issue8032#msg100537 ) - gdb_repr, gdb_output = self.get_gdb_repr('id(__builtins__.help)', import_site=True) - - m = re.match(r'<_Helper at remote 0x-?[0-9a-f]+>', gdb_repr) - self.assertTrue(m, - msg='Unexpected rendering %r' % gdb_repr) - - def test_selfreferential_list(self): - '''Ensure that a reference loop involving a list doesn't lead proxyval - into an infinite loop:''' - gdb_repr, gdb_output = \ - self.get_gdb_repr("a = [3, 4, 5] ; a.append(a) ; id(a)") - self.assertEqual(gdb_repr, '[3, 4, 5, [...]]') - - gdb_repr, gdb_output = \ - self.get_gdb_repr("a = [3, 4, 5] ; b = [a] ; a.append(b) ; id(a)") - self.assertEqual(gdb_repr, '[3, 4, 5, [[...]]]') - - def test_selfreferential_dict(self): - '''Ensure that a reference loop involving a dict doesn't lead proxyval - into an infinite loop:''' - gdb_repr, gdb_output = \ - self.get_gdb_repr("a = {} ; b = {'bar':a} ; a['foo'] = b ; id(a)") - - self.assertEqual(gdb_repr, "{'foo': {'bar': {...}}}") - - def test_selfreferential_old_style_instance(self): - gdb_repr, gdb_output = \ - self.get_gdb_repr(''' -class Foo: - pass -foo = Foo() -foo.an_attr = foo -id(foo)''') - self.assertTrue(re.match(r'\) at remote 0x-?[0-9a-f]+>', - gdb_repr), - 'Unexpected gdb representation: %r\n%s' % \ - (gdb_repr, gdb_output)) - - def test_selfreferential_new_style_instance(self): - gdb_repr, gdb_output = \ - self.get_gdb_repr(''' -class Foo(object): - pass -foo = Foo() -foo.an_attr = foo -id(foo)''') - self.assertTrue(re.match(r'\) at remote 0x-?[0-9a-f]+>', - gdb_repr), - 'Unexpected gdb representation: %r\n%s' % \ - (gdb_repr, gdb_output)) - - gdb_repr, gdb_output = \ - self.get_gdb_repr(''' -class Foo(object): - pass -a = Foo() -b = Foo() -a.an_attr = b -b.an_attr = a -id(a)''') - self.assertTrue(re.match(r'\) at remote 0x-?[0-9a-f]+>\) at remote 0x-?[0-9a-f]+>', - gdb_repr), - 'Unexpected gdb representation: %r\n%s' % \ - (gdb_repr, gdb_output)) - - def test_truncation(self): - 'Verify that very long output is truncated' - gdb_repr, gdb_output = self.get_gdb_repr('id(list(range(1000)))') - self.assertEqual(gdb_repr, - "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, " - "14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, " - "27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, " - "40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, " - "53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, " - "66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, " - "79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, " - "92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, " - "104, 105, 106, 107, 108, 109, 110, 111, 112, 113, " - "114, 115, 116, 117, 118, 119, 120, 121, 122, 123, " - "124, 125, 126, 127, 128, 129, 130, 131, 132, 133, " - "134, 135, 136, 137, 138, 139, 140, 141, 142, 143, " - "144, 145, 146, 147, 148, 149, 150, 151, 152, 153, " - "154, 155, 156, 157, 158, 159, 160, 161, 162, 163, " - "164, 165, 166, 167, 168, 169, 170, 171, 172, 173, " - "174, 175, 176, 177, 178, 179, 180, 181, 182, 183, " - "184, 185, 186, 187, 188, 189, 190, 191, 192, 193, " - "194, 195, 196, 197, 198, 199, 200, 201, 202, 203, " - "204, 205, 206, 207, 208, 209, 210, 211, 212, 213, " - "214, 215, 216, 217, 218, 219, 220, 221, 222, 223, " - "224, 225, 226...(truncated)") - self.assertEqual(len(gdb_repr), - 1024 + len('...(truncated)')) - - def test_builtin_method(self): - gdb_repr, gdb_output = self.get_gdb_repr('import sys; id(sys.stdout.readlines)') - self.assertTrue(re.match(r'', - gdb_repr), - 'Unexpected gdb representation: %r\n%s' % \ - (gdb_repr, gdb_output)) - - def test_frames(self): - gdb_output = self.get_stack_trace(''' -def foo(a, b, c): - pass - -foo(3, 4, 5) -id(foo.__code__)''', - breakpoint='builtin_id', - cmds_after_breakpoint=['print (PyFrameObject*)(((PyCodeObject*)v)->co_zombieframe)'] - ) - self.assertTrue(re.match(r'.*\s+\$1 =\s+Frame 0x-?[0-9a-f]+, for file , line 3, in foo \(\)\s+.*', - gdb_output, - re.DOTALL), - 'Unexpected gdb representation: %r\n%s' % (gdb_output, gdb_output)) - -@unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") -class PyListTests(DebuggerTests): - def assertListing(self, expected, actual): - self.assertEndsWith(actual, expected) - - def test_basic_command(self): - 'Verify that the "py-list" command works' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-list']) - - self.assertListing(' 5 \n' - ' 6 def bar(a, b, c):\n' - ' 7 baz(a, b, c)\n' - ' 8 \n' - ' 9 def baz(*args):\n' - ' >10 id(42)\n' - ' 11 \n' - ' 12 foo(1, 2, 3)\n', - bt) - - def test_one_abs_arg(self): - 'Verify the "py-list" command with one absolute argument' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-list 9']) - - self.assertListing(' 9 def baz(*args):\n' - ' >10 id(42)\n' - ' 11 \n' - ' 12 foo(1, 2, 3)\n', - bt) - - def test_two_abs_args(self): - 'Verify the "py-list" command with two absolute arguments' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-list 1,3']) - - self.assertListing(' 1 # Sample script for use by test_gdb.py\n' - ' 2 \n' - ' 3 def foo(a, b, c):\n', - bt) - -class StackNavigationTests(DebuggerTests): - @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - def test_pyup_command(self): - 'Verify that the "py-up" command works' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-up', 'py-up']) - self.assertMultilineMatches(bt, - r'''^.* -#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) - baz\(a, b, c\) -$''') - - @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") - def test_down_at_bottom(self): - 'Verify handling of "py-down" at the bottom of the stack' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-down']) - self.assertEndsWith(bt, - 'Unable to find a newer python frame\n') - - @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") - def test_up_at_top(self): - 'Verify handling of "py-up" at the top of the stack' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-up'] * 5) - self.assertEndsWith(bt, - 'Unable to find an older python frame\n') - - @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - def test_up_then_down(self): - 'Verify "py-up" followed by "py-down"' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-up', 'py-up', 'py-down']) - self.assertMultilineMatches(bt, - r'''^.* -#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) - baz\(a, b, c\) -#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 10, in baz \(args=\(1, 2, 3\)\) - id\(42\) -$''') - -class PyBtTests(DebuggerTests): - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - def test_bt(self): - 'Verify that the "py-bt" command works' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-bt']) - self.assertMultilineMatches(bt, - r'''^.* -Traceback \(most recent call first\): - - File ".*gdb_sample.py", line 10, in baz - id\(42\) - File ".*gdb_sample.py", line 7, in bar - baz\(a, b, c\) - File ".*gdb_sample.py", line 4, in foo - bar\(a, b, c\) - File ".*gdb_sample.py", line 12, in - foo\(1, 2, 3\) -''') - - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - def test_bt_full(self): - 'Verify that the "py-bt-full" command works' - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-bt-full']) - self.assertMultilineMatches(bt, - r'''^.* -#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) - baz\(a, b, c\) -#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\) - bar\(a, b, c\) -#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 12, in \(\) - foo\(1, 2, 3\) -''') - - @unittest.skipUnless(_thread, - "Python was compiled without thread support") - def test_threads(self): - 'Verify that "py-bt" indicates threads that are waiting for the GIL' - cmd = ''' -from threading import Thread - -class TestThread(Thread): - # These threads would run forever, but we'll interrupt things with the - # debugger - def run(self): - i = 0 - while 1: - i += 1 - -t = {} -for i in range(4): - t[i] = TestThread() - t[i].start() - -# Trigger a breakpoint on the main thread -id(42) - -''' - # Verify with "py-bt": - gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['thread apply all py-bt']) - self.assertIn('Waiting for the GIL', gdb_output) - - # Verify with "py-bt-full": - gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['thread apply all py-bt-full']) - self.assertIn('Waiting for the GIL', gdb_output) - - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - # Some older versions of gdb will fail with - # "Cannot find new threads: generic error" - # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround - @unittest.skipUnless(_thread, - "Python was compiled without thread support") - def test_gc(self): - 'Verify that "py-bt" indicates if a thread is garbage-collecting' - cmd = ('from gc import collect\n' - 'id(42)\n' - 'def foo():\n' - ' collect()\n' - 'def bar():\n' - ' foo()\n' - 'bar()\n') - # Verify with "py-bt": - gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt'], - ) - self.assertIn('Garbage-collecting', gdb_output) - - # Verify with "py-bt-full": - gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt-full'], - ) - self.assertIn('Garbage-collecting', gdb_output) - - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - # Some older versions of gdb will fail with - # "Cannot find new threads: generic error" - # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround - @unittest.skipUnless(_thread, - "Python was compiled without thread support") - def test_pycfunction(self): - 'Verify that "py-bt" displays invocations of PyCFunction instances' - # Tested function must not be defined with METH_NOARGS or METH_O, - # otherwise call_function() doesn't call PyCFunction_Call() - cmd = ('from time import gmtime\n' - 'def foo():\n' - ' gmtime(1)\n' - 'def bar():\n' - ' foo()\n' - 'bar()\n') - # Verify with "py-bt": - gdb_output = self.get_stack_trace(cmd, - breakpoint='time_gmtime', - cmds_after_breakpoint=['bt', 'py-bt'], - ) - self.assertIn('\n.*") - -class PyLocalsTests(DebuggerTests): - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - def test_basic_command(self): - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-up', 'py-locals']) - self.assertMultilineMatches(bt, - r".*\nargs = \(1, 2, 3\)\n.*") - - @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") - @unittest.skipIf(python_is_optimized(), - "Python was compiled with optimizations") - def test_locals_after_up(self): - bt = self.get_stack_trace(script=self.get_sample_script(), - cmds_after_breakpoint=['py-up', 'py-up', 'py-locals']) - self.assertMultilineMatches(bt, - r".*\na = 1\nb = 2\nc = 3\n.*") - -def test_main(): - if support.verbose: - print("GDB version %s.%s:" % (gdb_major_version, gdb_minor_version)) - for line in gdb_version.splitlines(): - print(" " * 4 + line) - run_unittest(PrettyPrintTests, - PyListTests, - StackNavigationTests, - PyBtTests, - PyPrintTests, - PyLocalsTests - ) - -if __name__ == "__main__": - test_main() diff --git a/toxygen/tests/test_gdb.urls b/toxygen/tests/test_gdb.urls deleted file mode 100644 index 5f2cb10..0000000 --- a/toxygen/tests/test_gdb.urls +++ /dev/null @@ -1 +0,0 @@ -https://github.com/akheron/cpython/raw/master/Lib/test/test_gdb.py diff --git a/toxygen/tests/tests_socks.py b/toxygen/tests/tests_socks.py deleted file mode 100644 index 1551557..0000000 --- a/toxygen/tests/tests_socks.py +++ /dev/null @@ -1,1885 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -# -# @file tests.py -# @author Wei-Ning Huang (AZ) -# -# Copyright (C) 2013 - 2014 Wei-Ning Huang (AZ) -# All Rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# - -"""Originaly from https://github.com/oxij/PyTox c-toxcore-02 branch -which itself was forked from https://github.com/aitjcize/PyTox/ - -Modified to work with -""" - -import ctypes -import faulthandler -import hashlib -import logging -import os -import random -import re -import sys -import threading -import traceback -import unittest -from ctypes import * - -faulthandler.enable() - -import warnings - -warnings.filterwarnings('ignore') - -try: - from io import BytesIO - - import certifi - import pycurl -except ImportError: - pycurl = None - -try: - import coloredlogs - os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' -except ImportError as e: - logging.log(logging.DEBUG, f"coloredlogs not available: {e}") - coloredlogs = None - -try: - import color_runner -except ImportError as e: - logging.log(logging.DEBUG, f"color_runner not available: {e}") - color_runner = None - -import toxygen_wrapper -import toxygen_wrapper.toxcore_enums_and_consts as enums -from toxygen_wrapper.tox import Tox -from toxygen_wrapper.toxcore_enums_and_consts import (TOX_ADDRESS_SIZE, TOX_CONNECTION, - TOX_FILE_CONTROL, - TOX_MESSAGE_TYPE, - TOX_SECRET_KEY_SIZE, - TOX_USER_STATUS) - -try: - import support_testing as ts -except ImportError: - import toxygen_wrapper.tests.support_testing as ts - -try: - from tests.toxygen_tests import test_sound_notification - bIS_NOT_TOXYGEN = False -except ImportError: - bIS_NOT_TOXYGEN = True - -# from qtpy import QtCore -import time - -sleep = time.sleep - -global LOG -LOG = logging.getLogger('TestS') -# just print to stdout so there is no complications from logging. -def LOG_ERROR(l): print('EROR+ '+l) -def LOG_WARN(l): print('WARN+ '+l) -def LOG_INFO(l): print('INFO+ '+l) -def LOG_DEBUG(l): print('DEBUG+ '+l) -def LOG_TRACE(l): pass # print('TRAC+ '+l) - -ADDR_SIZE = 38 * 2 -CLIENT_ID_SIZE = 32 * 2 -THRESHOLD = 25 - -global oTOX_OPTIONS -oTOX_OPTIONS = {} - -bIS_LOCAL = 'new' in sys.argv or 'main' in sys.argv or 'newlocal' in sys.argv - -# Patch unittest for Python version <= 2.6 -if not hasattr(unittest, 'skip'): - def unittest_skip(reason): - def _wrap1(func): - def _wrap2(self, *args, **kwargs): - pass - return _wrap2 - return _wrap1 - unittest.skip = unittest_skip - -if not hasattr(unittest, 'expectedFailureIf'): - def unittest_expectedFailureIf(condition, reason): - def _wrap1(test_item): - def _wrap2(self, *args, **kwargs): - if condition: - test_item.__unittest_expecting_failure__ = True - pass - return _wrap2 - return _wrap1 - - unittest.expectedFailureIf = unittest_expectedFailureIf - -def expectedFailure(test_item): - test_item.__unittest_expecting_failure__ = True - return test_item - -class ToxOptions(): - def __init__(self): - self.ipv6_enabled = True - self.udp_enabled = True - self.proxy_type = 0 - self.proxy_host = '' - self.proxy_port = 0 - self.start_port = 0 - self.end_port = 0 - self.tcp_port = 0 - self.savedata_type = 0 # 1=toxsave, 2=secretkey - self.savedata_data = b'' - self.savedata_length = 0 - self.local_discovery_enabled = False - self.dht_announcements_enabled = True - self.hole_punching_enabled = False - self.experimental_thread_safety = False - -class App(): - def __init__(self): - self.mode = 0 -oAPP = App() - -class AliceTox(Tox): - - def __init__(self, opts, app=None): - - super(AliceTox, self).__init__(opts, app=app) - self._address = self.self_get_address() - self.name = 'alice' - self._opts = opts - self._app = app - -class BobTox(Tox): - - def __init__(self, opts, app=None): - super(BobTox, self).__init__(opts, app=app) - self._address = self.self_get_address() - self.name = 'bob' - self._opts = opts - self._app = app - -class BaseThread(threading.Thread): - - def __init__(self, name=None, target=None): - if name: - super().__init__(name=name, target=target) - else: - super().__init__(target=target) - self._stop_thread = False - self.name = name - - def stop_thread(self, timeout=-1): - self._stop_thread = True - if timeout < 0: - timeout = ts.iTHREAD_TIMEOUT - i = 0 - while i < ts.iTHREAD_JOINS: - self.join(timeout) - if not self.is_alive(): break - i = i + 1 - else: - LOG.warning(f"{self.name} BLOCKED") - -class ToxIterateThread(BaseThread): - - def __init__(self, tox): - super().__init__(name='ToxIterateThread') - self._tox = tox - - def run(self): - while not self._stop_thread: - self._tox.iterate() - sleep(self._tox.iteration_interval() / 1000) - -global bob, alice -bob = alice = None - -def prepare(self): - global bob, alice - def bobs_on_self_connection_status(iTox, connection_state, *args): - status = connection_state - self.bob.dht_connected = status - self.bob.mycon_time = time.time() - try: - if status != TOX_CONNECTION['NONE']: - LOG_DEBUG(f"bobs_on_self_connection_status TRUE {status}" \ - +f" last={int(self.bob.mycon_time)}" ) - self.bob.mycon_status = True - else: - LOG_DEBUG(f"bobs_on_self_connection_status FALSE {status}" \ - +f" last={int(self.bob.mycon_time)}" ) - self.bob.mycon_status = False - except Exception as e: - LOG_ERROR(f"bobs_on_self_connection_status {e}") - else: - if self.bob.self_get_connection_status() != status: - LOG_WARN(f"bobs_on_self_connection_status DISAGREE {status}") - - def alices_on_self_connection_status(iTox, connection_state, *args): - #FixMe connection_num - status = connection_state - self.alice.dht_connected = status - self.alice.mycon_time = time.time() - try: - if status != TOX_CONNECTION['NONE']: - LOG_DEBUG(f"alices_on_self_connection_status TRUE {status}" \ - +f" last={int(self.alice.mycon_time)}" ) - self.alice.mycon_status = True - else: - LOG_WARN(f"alices_on_self_connection_status FALSE {status}" \ - +f" last={int(self.alice.mycon_time)}" ) - self.alice.mycon_status = False - except Exception as e: - LOG_ERROR(f"alices_on_self_connection_status error={e}") - else: - if self.alice.self_get_connection_status() != status: - LOG_WARN(f"alices_on_self_connection_status != {status}") - self.alice.dht_connected = status - - opts = oToxygenToxOptions(oTOX_OARGS) - alice = AliceTox(opts, app=oAPP) - alice.oArgs = opts - alice.dht_connected = -1 - alice.mycon_status = False - alice.mycon_time = 1 - alice.callback_self_connection_status(alices_on_self_connection_status) - - bob = BobTox(opts, app=oAPP) - bob.oArgs = opts - bob.dht_connected = -1 - bob.mycon_status = False - bob.mycon_time = 1 - bob.callback_self_connection_status(bobs_on_self_connection_status) - if not bIS_LOCAL and not ts.bAreWeConnected(): - LOG.warning(f"doOnce not local and NOT CONNECTED") - return [bob, alice] - -class ToxSuite(unittest.TestCase): - failureException = RuntimeError - - @classmethod - def setUpClass(cls): - global oTOX_OARGS - assert oTOX_OPTIONS - assert oTOX_OARGS - - if not hasattr(cls, 'alice') and not hasattr(cls, 'bob'): - l = prepare(cls) - assert l - cls.bob, cls.alice = l - if not hasattr(cls.bob, '_main_loop'): - cls.bob._main_loop = ToxIterateThread(cls.bob) - cls.bob._main_loop.start() - LOG.debug(f"cls.bob._main_loop: ") # {threading.enumerate()} - if not hasattr(cls.alice, '_main_loop'): - cls.alice._main_loop = ToxIterateThread(cls.alice) - cls.alice._main_loop.start() - LOG.debug(f"cls.alice._main_loop: ") # {threading.enumerate()} - - cls.lUdp = ts.generate_nodes( - oArgs=oTOX_OARGS, - nodes_count=2*ts.iNODES, - ipv='ipv4', - udp_not_tcp=True) - - cls.lTcp = ts.generate_nodes( - oArgs=oTOX_OARGS, - nodes_count=2*ts.iNODES, - ipv='ipv4', - udp_not_tcp=False) - - @classmethod - def tearDownClass(cls): - cls.bob._main_loop.stop_thread() - cls.alice._main_loop.stop_thread() - if False: - cls.alice.kill() - cls.bob.kill() - del cls.bob - del cls.alice - - def setUp(self): - """ - """ - if hasattr(self, 'baid') and self.baid >= 0 and \ - self.baid in self.bob.self_get_friend_list(): - LOG.warn(f"setUp ALICE IS ALREADY IN BOBS FRIEND LIST") - elif self.bob.self_get_friend_list_size() >= 1: - LOG.warn(f"setUp BOB STILL HAS A FRIEND LIST") - - if hasattr(self, 'abid') and self.abid >= 0 and \ - self.abid in self.alice.self_get_friend_list(): - LOG.warn(f"setUp BOB IS ALREADY IN ALICES FRIEND LIST") - elif self.alice.self_get_friend_list_size() >= 1: - LOG.warn(f"setUp ALICE STILL HAS A FRIEND LIST") - - def tearDown(self): - """ - """ - if hasattr(self, 'baid') and self.baid >= 0 and \ - self.baid in self.bob.self_get_friend_list(): - LOG.warn(f"tearDown ALICE IS STILL IN BOBS FRIEND LIST") - elif self.bob.self_get_friend_list_size() >= 1: - LOG.warn(f"tearDown BOBS STILL HAS A FRIEND LIST") - - if hasattr(self, 'abid') and self.abid >= 0 and \ - self.abid in self.alice.self_get_friend_list(): - LOG.warn(f"tearDown BOB IS STILL IN ALICES FRIEND LIST") - elif self.bob.self_get_friend_list_size() >= 1: - LOG.warn(f"tearDown ALICE STILL HAS A FRIEND LIST") - - def run(self, result=None): - """ Stop after first error """ - if not result.errors: - super(ToxSuite, self).run(result) - - def get_connection_status(self): - if self.bob.mycon_time == -1 or self.alice.mycon_time == -1: - pass - # drop through - elif self.bob.dht_connected == TOX_CONNECTION['NONE']: - return False - elif self.alice.dht_connected == TOX_CONNECTION['NONE']: - return False - - # if not self.connected - if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: - return False - if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: - return False - return True - - def loop(self, n): - """ - t:iterate - t:iteration_interval - """ - interval = self.bob.iteration_interval() - for i in range(n): - self.alice.iterate() - self.bob.iterate() - sleep(interval / 1000.0) - - def call_bootstrap(self, num=None, lToxes=None, i=0): - if num == None: num=ts.iNODES -# LOG.debug(f"call_bootstrap network={oTOX_OARGS.network}") - if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: - ts.bootstrap_local(self.lUdp, [self.alice, self.bob]) - elif not ts.bAreWeConnected(): - LOG.warning('we are NOT CONNECTED') - else: - random.shuffle(self.lUdp) - if oTOX_OARGS.proxy_port > 0: - lElts = self.lUdp[:1] - else: - lElts = self.lUdp[:num+i] - LOG.debug(f"call_bootstrap ts.bootstrap_udp {len(lElts)}") - if lToxes is None: lToxes = [self.alice, self.bob] - ts.bootstrap_udp(lElts, lToxes) - random.shuffle(self.lTcp) - lElts = self.lTcp[:num+i] - LOG.debug(f"call_bootstrap ts.bootstrap_tcp {len(lElts)}") - ts.bootstrap_tcp(lElts, lToxes) - - def loop_until_connected(self, num=None): - """ - t:on_self_connection_status - t:self_get_connection_status - """ - i = 0 - bRet = None - while i <= THRESHOLD : - if (self.alice.mycon_status and self.bob.mycon_status): - bRet = True - break - if i % 5 == 0: - j = i//5 - self.call_bootstrap(num, lToxes=None, i=j) - s = '' - if i == 0: s = '\n' - LOG.info(s+"loop_until_connected " \ - +" #" + str(i) \ - +" BOB=" +repr(self.bob.self_get_connection_status()) \ - +" ALICE=" +repr(self.alice.self_get_connection_status()) - +f" BOBS={self.bob.mycon_status}" \ - +f" ALICES={self.alice.mycon_status}" \ - +f" last={int(self.bob.mycon_time)}" ) - if (self.alice.mycon_status and self.bob.mycon_status): - bRet = True - break - if (self.alice.self_get_connection_status() and - self.bob.self_get_connection_status()): - LOG_WARN(f"loop_until_connected disagree status() DISAGREE" \ - +f' self.bob.mycon_status={self.bob.mycon_status}' \ - +f' alice.mycon_status={self.alice.mycon_status}' \ - +f" last={int(self.bob.mycon_time)}" ) - bRet = True - break - i += 1 - self.loop(100) - else: - bRet = False - - if bRet or \ - ( self.bob.self_get_connection_status() != TOX_CONNECTION['NONE'] and \ - self.alice.self_get_connection_status() != TOX_CONNECTION['NONE'] ): - LOG.info(f"loop_until_connected returning True {i}" \ - +f" BOB={self.bob.self_get_connection_status()}" \ - +f" ALICE={self.alice.self_get_connection_status()}" \ - +f" last={int(self.bob.mycon_time)}" ) - return True - else: - LOG.warning(f"loop_until_connected returning False {i}" \ - +f" BOB={self.bob.self_get_connection_status()}" \ - +f" ALICE={self.alice.self_get_connection_status()}" \ - +f" last={int(self.bob.mycon_time)}" ) - return False - - def wait_obj_attr(self, obj, attr): - return wait_otox_attrs(self, obj, [attr]) - - def wait_objs_attr(self, objs, attr): - i = 0 - while i <= THRESHOLD: - if i % 5 == 0: - num = None - j = i//5 - self.call_bootstrap(num, objs, i=j) - LOG.debug("wait_objs_attr " +repr(objs) \ - +" for " +repr(attr) \ - +" " +str(i)) - if all([getattr(obj, attr) for obj in objs]): - return True - self.loop(100) - i += 1 - else: - LOG.error(f"wait_obj_attr i >= {THRESHOLD}") - - return all([getattr(obj, attr) for obj in objs]) - - def wait_otox_attrs(self, obj, attrs): - i = 0 - while i <= THRESHOLD: - if i % 5 == 0: - num = None - j = 0 - if obj.mycon_time == 1: - num = 4 - j = i//5 - self.call_bootstrap(num, [obj], i=j) - LOG.debug(f"wait_otox_attrs {obj.name} for {attrs} {i}" \ - +f" last={int(obj.mycon_time)}") - if all([getattr(obj, attr) is not None for attr in attrs]): - return True - self.loop(100) - i += 1 - else: - LOG.warning(f"wait_otox_attrs i >= {THRESHOLD}") - - return all([getattr(obj, attr) for attr in attrs]) - - def wait_ensure_exec(self, method, args): - i = 0 - oRet = None - while i <= THRESHOLD: - if i % 5 == 0: - j = i//5 - self.call_bootstrap(num=None, lToxes=None, i=j) - LOG.debug("wait_ensure_exec " \ - +" " +str(method) - +" " +str(i)) - try: - oRet = method(*args) - if oRet: - LOG.info(f"wait_ensure_exec oRet {oRet}") - return True - except ArgumentError as e: - # ArgumentError('This client is currently NOT CONNECTED to the friend.') - # dunno - LOG.warning(f"wait_ensure_exec ArgumentError {e}") - return False - except Exception as e: - LOG.warning(f"wait_ensure_exec EXCEPTION {e}") - return False - sleep(3) - i += 1 - else: - LOG.error(f"wait_ensure_exec i >= {1*THRESHOLD}") - return False - - return oRet - - def bob_add_alice_as_friend_norequest(self): - if hasattr(self, 'baid') and self.baid >= 0 and \ - self.baid in self.bob.self_get_friend_list(): - LOG.warn('Alice is already in bobs friend list') - return True - if self.bob.self_get_friend_list_size() >= 1: - LOG.warn(f'Bob has a friend list {self.bob.self_get_friend_list()}') - return True - - MSG = 'Hi, this is Bob.' - iRet = self.bob.friend_add_norequest(self.alice._address) - self.baid = self.bob.friend_by_public_key(self.alice._address) - assert self.baid >= 0, self.baid - assert self.bob.friend_exists(self.baid), "bob.friend_exists" - assert not self.bob.friend_exists(self.baid + 1) - assert self.baid in self.bob.self_get_friend_list() - assert self.bob.self_get_friend_list_size() >= 1 - return iRet >= 0 - - def alice_add_bob_as_friend_norequest(self): - if hasattr(self, 'abid') and self.abid >= 0 and \ - self.abid in self.alice.self_get_friend_list(): - LOG.warn('Alice is already in Bobs friend list') - return True - if self.alice.self_get_friend_list_size() >= 1: - LOG.warn(f'Alice has a friend list {self.alice.self_get_friend_list()}') - - MSG = 'Hi Bob, this is Alice.' - iRet = self.alice.friend_add_norequest(self.bob._address) - self.abid = self.alice.friend_by_public_key(self.bob._address) - assert self.abid >= 0, self.abid - assert self.abid in self.alice.self_get_friend_list() - assert self.alice.friend_exists(self.abid), "alice.friend_exists" - assert not self.alice.friend_exists(self.abid + 1) - assert self.alice.self_get_friend_list_size() >= 1 - return iRet >= 0 - - def both_add_as_friend_norequest(self): - assert self.bob_add_alice_as_friend_norequest() - if not hasattr(self, 'baid') or self.baid < 0: - raise AssertionError("both_add_as_friend_norequest bob, 'baid'") - - assert self.alice_add_bob_as_friend_norequest() - if not hasattr(self, 'abid') or self.abid < 0: - raise AssertionError("both_add_as_friend_norequest alice, 'abid'") - - #: Test last online - assert self.alice.friend_get_last_online(self.abid) is not None - assert self.bob.friend_get_last_online(self.baid) is not None - return True - - def bob_add_alice_as_friend(self): - """ - t:friend_add - t:on_friend_request - t:friend_by_public_key - """ - MSG = 'Alice, this is Bob.' - sSlot = 'friend_request' - - def alices_on_friend_request(iTox, - public_key, - message_data, - message_data_size, - *largs): - LOG_DEBUG(f"alices_on_friend_request: " +repr(message_data)) - try: - assert str(message_data, 'UTF-8') == MSG - LOG_INFO(f"alices_on_friend_request: friend_added = True ") - except Exception as e: - LOG_WARN(f"alices_on_friend_request: Exception {e}") - # return - setattr(self.bob, sSlot, True) - - setattr(self.bob, sSlot, None) - inum = -1 - self.alice.callback_friend_request(alices_on_friend_request) - try: - inum = self.bob.friend_add(self.alice._address, bytes(MSG, 'UTF-8')) - if not inum >= 0: - LOG.warning('bob.friend_add !>= 0 ' +repr(inum)) - if not self.wait_otox_attrs(self.bob, [sSlot]): - return False - except Exception as e: - LOG.error(f"bob.friend_add EXCEPTION {e}") - return False - finally: - self.bob.callback_friend_message(None) - - self.baid = self.bob.friend_by_public_key(self.alice._address) - assert self.baid >= 0, self.baid - assert self.bob.friend_exists(self.baid) - assert not self.bob.friend_exists(self.baid + 1) - assert self.baid in self.bob.self_get_friend_list() - assert self.bob.self_get_friend_list_size() >= 1 - return True - - def alice_add_bob_as_friend(self): - """ - t:friend_add - t:on_friend_request - t:friend_by_public_key - """ - MSG = 'Bob, this is Alice.' - sSlot = 'friend_request' - - def bobs_on_friend_request(iTox, - public_key, - message_data, - message_data_size, - *largs): - LOG_DEBUG(f"bobs_on_friend_request: " +repr(message_data)) - try: - assert str(message_data, 'UTF-8') == MSG - LOG_INFO(f"bobs_on_friend_request: friend_added = True ") - except Exception as e: - LOG_WARN(f"bobs_on_friend_request: Exception {e}") - # return - else: - setattr(self.alice, sSlot, True) - - setattr(self.alice, sSlot, None) - inum = -1 - self.bob.callback_friend_request(bobs_on_friend_request) - try: - inum = self.alice.friend_add(self.bob._address, bytes(MSG, 'UTF-8')) - if not inum >= 0: - LOG.warning('alice.friend_add !>= 0 ' +repr(inum)) - if not self.wait_obj_attr(self.alice, sSlot): - return False - except Exception as e: - LOG.error(f"alice.friend_add EXCEPTION {e}") - return False - finally: - self.bob.callback_friend_message(None) - self.abid = self.alice.friend_by_public_key(self.bob._address) - assert self.abid >= 0, self.abid - assert self.alice.friend_exists(self.abid) - assert not self.alice.friend_exists(self.abid + 1) - assert self.abid in self.alice.self_get_friend_list() - assert self.alice.self_get_friend_list_size() >= 1 - return True - - def both_add_as_friend(self): - assert self.bob_add_alice_as_friend() - assert self.alice_add_bob_as_friend() - - #: Test last online - assert self.alice.friend_get_last_online(self.abid) is not None - assert self.bob.friend_get_last_online(self.baid) is not None - - def bob_add_alice_as_friend_and_status(self): - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - else: - assert self.bob_add_alice_as_friend() - - #: Wait until both are online - self.bob.friend_conn_status = False - def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): - LOG_INFO(f"bobs_on_friend_connection_status {friend_id} ?>=0" +repr(iStatus)) - if iStatus > 0: - self.bob.friend_conn_status = True - - self.bob.friend_status = None - def bobs_on_friend_status(iTox, friend_id, iStatus, *largs): - LOG_INFO(f"bobs_on_friend_status {friend_id} ?>=0" +repr(iStatus)) - if iStatus > 0: - self.bob.friend_status = True - - self.alice.friend_conn_status = None - def alices_on_friend_connection_status(iTox, friend_id, iStatus, *largs): - LOG_INFO(f"alices_on_friend_connection_status {friend_id} ?>=0 " +repr(iStatus)) - if iStatus > 0: - self.alice.friend_conn_status = True - - self.alice.friend_status = False - def alices_on_friend_status(iTox, friend_id, iStatus, *largs): - LOG_INFO(f"alices_on_friend_status {friend_id} ?>=0 " +repr(iStatus)) - if iStatus > 0: - self.alice.friend_status = True - - self.alice.callback_friend_connection_status(alices_on_friend_connection_status) - self.alice.callback_friend_status(alices_on_friend_status) - try: - LOG.info("bob_add_alice_as_friend_and_status waiting for alice connections") - if not self.wait_otox_attrs(self.alice, - ['friend_conn_status', - 'friend_status']): - return False - - self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) - self.bob.callback_friend_status(bobs_on_friend_status) - - LOG.info("bob_add_alice_as_friend_and_status waiting for bob connections") - if not self.wait_otox_attrs(self.bob, - ['friend_conn_status', - 'friend_status']): - return False - except Exception as e: - LOG.error(f"bob_add_alice_as_friend_and_status ERROR {e}") - return False - finally: - self.alice.callback_friend_connection_status(None) - self.bob.callback_friend_connection_status(None) - self.alice.callback_friend_status(None) - self.bob.callback_friend_status(None) - return True - - def friend_delete(self, fname, baid): - #: Test delete friend - assert getattr(self, fname).friend_exists(baid) - getattr(self, fname).friend_delete(baid) - self.loop(50) - assert not self.bob.friend_exists(baid) - - def warn_if_no_cb(self, alice, sSlot): - if not hasattr(alice, sSlot+'_cb') or \ - not getattr(alice, sSlot+'_cb'): - LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") - - def warn_if_cb(self, alice, sSlot): - if hasattr(self.bob, sSlot+'_cb') and \ - getattr(self.bob, sSlot+'_cb'): - LOG.warning(f"self.bob.{sSlot}_cb EXIST") - - # tests are executed in order - def test_notice_log(self): # works - notice = '/var/lib/tor/.SelekTOR/3xx/cache/9050/notice.log' - if True or os.path.exists(notice): - iRet = os.system(f"sudo sed -e '1,/.notice. Bootstrapped 100%/d' {notice}" + \ - "| grep 'Tried for 120 seconds to get a connection to :0.'") - if iRet == 0: - raise SystemExit("seconds to get a connection to :0") - else: - LOG.debug(f"checked {notice}") - - def test_tests_logging(self): # works - with self.assertLogs('foo', level='INFO') as cm: - logging.getLogger('foo').info('first message') - logging.getLogger('foo.bar').error('second message') - logging.getLogger('foo.bar.baz').debug('third message') - self.assertEqual(cm.output, ['INFO:foo:first message', - 'ERROR:foo.bar:second message']) - - def test_tests_start(self): # works - LOG.info("test_tests_start " ) - port = ts.tox_bootstrapd_port() - - assert len(self.bob._address) == 2*TOX_ADDRESS_SIZE, len(self.bob._address) - assert len(self.alice._address) == 2*TOX_ADDRESS_SIZE, \ - len(self.alice._address) - - def test_bootstrap_local_netstat(self): # works - """ - t:bootstrap - """ - if oTOX_OARGS.network not in ['new', 'newlocal', 'local']: - return - - port = ts.tox_bootstrapd_port() - if not port: - return - iStatus = os.system(f"""netstat -nle4 | grep :{port}""") - if iStatus == 0: - LOG.info(f"bootstrap_local_netstat port {port} iStatus={iStatus}") - else: - LOG.warning(f"bootstrap_local_netstat NOT {port} iStatus={iStatus}") - - @unittest.skipIf(not bIS_LOCAL, "local test") - def test_bootstrap_local(self): # works - """ - t:bootstrap - """ - # get port from /etc/tox-bootstrapd.conf 33445 - self.call_bootstrap() - # ts.bootstrap_local(self, self.lUdp) - i = 0 - iStatus = -1 - while i < 10: - i = i + 1 - iStatus = self.bob.self_get_connection_status() - if iStatus != TOX_CONNECTION['NONE']: - break - sleep(3) - else: - pass - - o1 = self.alice.self_get_dht_id() - assert len(o1) == 64 - o2 = self.bob.self_get_dht_id() - assert len(o2) == 64 - -# if o1 != o2: LOG.warning(f"bootstrap_local DHT NOT same {o1} {o2} iStatus={iStatus}") - - iStatus = self.bob.self_get_connection_status() - if iStatus != TOX_CONNECTION['NONE']: - LOG.info(f"bootstrap_local connected iStatus={iStatus}") - return True - iStatus = self.alice.self_get_connection_status() - if iStatus != TOX_CONNECTION['NONE']: - LOG.info(f"bootstrap_local connected iStatus={iStatus}") - return True - LOG.warning(f"bootstrap_local NOT CONNECTED iStatus={iStatus}") - return False - - def test_bootstrap_iNmapInfo(self): # works - if os.environ['USER'] != 'root': - return - if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: - lElts = self.lUdp - elif oTOX_OARGS.proxy_port > 0: - lElts = self.lTcp - else: - lElts = self.lUdp - lRetval = [] - random.shuffle(lElts) - # assert - ts.bootstrap_iNmapInfo(lElts, oTOX_OARGS, bIS_LOCAL, iNODES=8) - - def test_self_get_secret_key(self): # works - """ - t:self_get_secret_key - """ - # test_self_get_secret_key - CRYPTO_SECRET_KEY_SIZE = 32 - secret_key = create_string_buffer(CRYPTO_SECRET_KEY_SIZE) - oRet0 = self.alice.self_get_secret_key(secret_key) - assert oRet0, repr(oRet0) - LOG.info('test_self_get_secret_key ' +repr(oRet0)) - assert len(str(oRet0)) - del secret_key - - def test_self_get_public_keys(self): # works - """ - t:self_get_secret_key - t:self_get_public_key - """ - - LOG.info('test_self_get_public_keys self.alice.self_get_secret_key') - oRet0 = self.alice.self_get_secret_key() - assert len(oRet0) - LOG.info('test_self_get_public_keys ' +repr(oRet0)) - oRet1 = self.alice.self_get_public_key() - assert len(oRet1) - LOG.info('test_self_get_public_keys ' +repr(oRet1)) - assert oRet0 != oRet1, repr(oRet0) +' != ' +repr(oRet1) - - def test_self_name(self): # works - """ - t:self_set_name - t:self_get_name - t:self_get_name_size - """ - self.alice.self_set_name('Alice') - assert self.alice.self_get_name() == 'Alice' - assert self.alice.self_get_name_size() == len('Alice') - self.bob.self_set_name('Bob') - assert self.bob.self_get_name() == 'Bob' - assert self.bob.self_get_name_size() == len('Bob') - - @unittest.skip('loud') - @unittest.skipIf(bIS_NOT_TOXYGEN or oTOX_OARGS.mode == 0, 'not testing in toxygen') - def test_sound_notification(self): # works - """ - Plays sound notification - :param type of notification - """ - from tests.toxygen_tests import test_sound_notification - test_sound_notification(self) - - def test_address(self): # works - """ - t:self_get_address - t:self_get_nospam - t:self_set_nospam - t:self_get_keys - """ - assert len(self.alice.self_get_address()) == ADDR_SIZE - assert len(self.bob.self_get_address()) == ADDR_SIZE - - self.alice.self_set_nospam(0x12345678) - assert self.alice.self_get_nospam() == 0x12345678 - self.loop(50) - - if hasattr(self.alice, 'self_get_keys'): - pk, sk = self.alice.self_get_keys() - assert pk == self.alice.self_get_address()[:CLIENT_ID_SIZE] - - def test_status_message(self): # works - MSG = 'Happy' - self.alice.self_set_status_message(MSG) - self.loop(100) - assert self.alice.self_get_status_message() == MSG, \ - self.alice.self_get_status_message() +' is not ' +MSG - assert self.alice.self_get_status_message_size() == len(MSG) - - def test_loop_until_connected(self): # works - assert self.loop_until_connected() - - def test_self_get_udp_port(self): # works - """ - t:self_get_udp_port - """ - if hasattr(oTOX_OPTIONS, 'udp_port') and oTOX_OPTIONS.udp_port: - o = self.alice.self_get_udp_port() - LOG.info('self_get_udp_port alice ' +repr(o)) - assert o > 0 - o = self.bob.self_get_udp_port() - LOG.info('self_get_udp_port bob ' +repr(o)) - assert o > 0 - - def test_self_get_tcp_port(self): # works - """ - t:self_get_tcp_port - """ - if hasattr(oTOX_OPTIONS, 'tcp_port') and oTOX_OPTIONS.tcp_port: - # errors if tcp_port <= 0 - o = self.alice.self_get_tcp_port() - LOG.info('self_get_tcp_port ' +repr(o)) - o = self.bob.self_get_tcp_port() - LOG.info('self_get_tcp_port ' +repr(o)) - - def test_get_dht_id(self): # works - """ - t:self_get_dht_id - """ - o1 = self.alice.self_get_dht_id() - assert len(o1) == 64 - o2 = self.bob.self_get_dht_id() - assert len(o2) == 64 - - def test_bob_assert_connection_status(self): # works - if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: - RuntimeError("ERROR: NOT CONNECTED " \ - +repr(self.bob.self_get_connection_status())) - - def test_alice_assert_connection_status(self): # works - if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: - RuntimeError("ERROR: NOT CONNECTED " \ - +repr(self.alice.self_get_connection_status())) - - def test_bob_assert_mycon_status(self): # works - if self.bob.mycon_status == False: - RuntimeError("ERROR: NOT CONNECTED " \ - +repr(self.bob.mycon_status)) - - def test_alice_assert_mycon_status(self): # works - if self.alice.mycon_status == False: - RuntimeError("ERROR: NOT CONNECTED " \ - +repr(self.alice.mycon_status)) - - def test_bob_add_alice_as_friend_norequest(self): # works - assert len(self.bob.self_get_friend_list()) == 0 - assert self.bob_add_alice_as_friend_norequest() - #: Test last online - assert self.bob.friend_get_last_online(self.baid) is not None - self.bob.friend_delete(self.baid) - - def test_alice_add_bob_as_friend_norequest(self): # works - assert len(self.alice.self_get_friend_list()) == 0 - assert self.alice_add_bob_as_friend_norequest() - assert len(self.alice.self_get_friend_list()) != 0 - #: Test last online - assert self.alice.friend_get_last_online(self.abid) is not None - self.alice.friend_delete(self.abid) - - def test_both_add_as_friend_norequest(self): # works - assert len(self.bob.self_get_friend_list()) == 0 - assert len(self.alice.self_get_friend_list()) == 0 - self.both_add_as_friend_norequest() - - self.bob.friend_delete(self.baid) - self.alice.friend_delete(self.abid) - assert len(self.bob.self_get_friend_list()) == 0 - assert len(self.alice.self_get_friend_list()) == 0 - - def test_bob_add_alice_as_friend_and_status(self): - self.bob_add_alice_as_friend_and_status() - self.bob.friend_delete(self.baid) - - @unittest.skip('malloc_consolidate(): invalid chunk size') -# @unittest.skipIf(bIS_LOCAL, "local test") -# @expectedFailure # (bIS_LOCAL, "local test") - def test_bob_add_alice_as_friend(self): # fails - assert len(self.bob.self_get_friend_list()) == 0 - try: - assert self.bob_add_alice_as_friend() - #: Test last online - assert self.bob.friend_get_last_online(self.baid) is not None - except AssertionError as e: - #WTF? - self.bob.friend_delete(self.baid) - raise RuntimeError(f"Failed test {e}") - finally: - self.bob.friend_delete(self.baid) - assert len(self.bob.self_get_friend_list()) == 0 - - @unittest.skip('malloc_consolidate(): invalid chunk size') -# @unittest.skipIf(bIS_LOCAL, "local test") -# @expectedFailure - def test_alice_add_bob_as_friend(self): # fails - assert len(self.bob.self_get_friend_list()) == 0 - try: - assert self.alice_add_bob_as_friend() - #: Test last online - assert self.alice.friend_get_last_online(self.abid) is not None - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - except Exception as e: - LOG.error(f"test_alice_add_bob_as_friend EXCEPTION {e}") - raise - finally: - self.alice.friend_delete(self.abid) - assert len(self.alice.self_get_friend_list()) == 0 - -# @unittest.skipIf(bIS_LOCAL, "local test") - @expectedFailure - def test_both_add_as_friend(self): # works - try: - self.both_add_as_friend() - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - except Exception as e: - LOG.error(f"test_both_add_as_friend EXCEPTION {e}") - raise - finally: - self.bob.friend_delete(self.baid) - self.alice.friend_delete(self.abid) - assert len(self.bob.self_get_friend_list()) == 0 - assert len(self.alice.self_get_friend_list()) == 0 - - @unittest.skip('unfinished') - def test_bob_add_alice_as_friend_and_status(self): - assert self.bob_add_alice_as_friend_and_status() - self.bob.friend_delete(self.baid) - -#? @unittest.skip('fails') - @expectedFailure - def test_on_friend_status_message(self): # fails - """ - t:self_set_status_message - t:self_get_status_message - t:self_get_status_message_size - t:friend_set_status_message - t:friend_get_status_message - t:friend_get_status_message_size - t:on_friend_status_message - """ - MSG = 'Happy' - sSlot = 'friend_status_message' - - def bob_on_friend_status_message(iTox, friend_id, new_status_message, new_status_size, *largs): - try: - assert str(new_status_message, 'UTF-8') == MSG - assert friend_id == self.baid - except Exception as e: - LOG_ERROR(f"BOB_ON_friend_status_message EXCEPTION {e}") - else: - LOG_INFO(f"BOB_ON_friend_status_message {friend_id}" \ - +repr(new_status_message)) - setattr(self.bob, sSlot, True) - - setattr(self.bob, sSlot, None) - try: - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - else: - assert self.bob_add_alice_as_friend() - - self.bob.callback_friend_status_message(bob_on_friend_status_message) - self.warn_if_no_cb(self.bob, sSlot) - self.alice.self_set_status_message(MSG) - assert self.wait_otox_attrs(self.bob, [sSlot]) - - assert self.bob.friend_get_status_message(self.baid) == MSG - assert self.bob.friend_get_status_message_size(self.baid) == len(MSG) - - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - except Exception as e: - LOG.error(f"test_on_friend_status_message EXCEPTION {e}") - raise - finally: - self.alice.callback_friend_status(None) - self.bob.friend_delete(self.baid) - - @expectedFailure - def test_friend(self): # works - """ - t:friend_delete - t:friend_exists - t:friend_get_public_key - t:self_get_friend_list - t:self_get_friend_list_size - t:self_set_name - t:friend_get_name - t:friend_get_name_size - t:on_friend_name - """ - - assert len(self.bob.self_get_friend_list()) == 0 - assert len(self.alice.self_get_friend_list()) == 0 - #: Test friend request - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - assert self.alice_add_bob_as_friend_norequest() - else: - # no not connected error - assert self.bob_add_alice_as_friend() - assert self.alice_add_bob_as_friend() - try: - assert self.bob.friend_get_public_key(self.baid) == \ - self.alice.self_get_address()[:CLIENT_ID_SIZE] - - #: Test friend_get_public_key - assert self.alice.friend_get_public_key(self.abid) == \ - self.bob.self_get_address()[:CLIENT_ID_SIZE] - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - except Exception as e: - LOG.error(f"test_friend EXCEPTION {e}") - raise - finally: - self.bob.friend_delete(self.baid) - self.alice.friend_delete(self.abid) - -# @unittest.skip('fails') -# @unittest.skipIf(not bIS_LOCAL and not ts.bAreWeConnected(), 'NOT CONNECTED') - @expectedFailure - def test_user_status(self): - """ - t:self_get_status - t:self_set_status - t:friend_get_status - t:friend_get_status - t:on_friend_status - """ - sSlot = 'friend_status' - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - else: - assert self.bob_add_alice_as_friend() - - sSTATUS = TOX_USER_STATUS['NONE'] - setattr(self.bob, sSlot, None) - def bobs_on_friend_set_status(iTox, friend_id, new_status, *largs): - LOG_INFO(f"bobs_on_friend_set_status {friend_id} {new_status}") - try: - assert friend_id == self.baid - assert new_status in [TOX_USER_STATUS['BUSY'], TOX_USER_STATUS['AWAY']] - except Exception as e: - LOG_WARN(f"bobs_on_friend_set_status EXCEPTION {e}") - setattr(self.bob, sSlot, True) - - try: - if not self.get_connection_status(): - LOG.warning(f"test_user_status NOT CONNECTED self.get_connection_status") - self.loop_until_connected() - - self.bob.callback_friend_status(bobs_on_friend_set_status) - self.warn_if_no_cb(self.bob, sSlot) - sSTATUS = TOX_USER_STATUS['BUSY'] - self.alice.self_set_status(sSTATUS) - sSTATUS = TOX_USER_STATUS['AWAY'] - self.alice.self_set_status(sSTATUS) - assert self.wait_otox_attrs(self.bob, [sSlot]) - # wait_obj_attr count >= 15 for friend_status - - self.alice.self_set_status(TOX_USER_STATUS['NONE']) - assert self.alice.self_get_status() == TOX_USER_STATUS['NONE'] - assert self.bob.friend_get_status(self.baid) == TOX_USER_STATUS['NONE'] - - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - - except Exception as e: - LOG.error(f"test_user_status EXCEPTION {e}") - raise - finally: - self.bob.callback_friend_status(None) - self.warn_if_cb(self.bob, sSlot) - self.bob.friend_delete(self.baid) - - @unittest.skip('crashes') - def test_connection_status(self): - """ - t:friend_get_connection_status - t:on_friend_connection_status - """ - LOG.info("test_connection_status ") - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - else: - assert self.bob_add_alice_as_friend() - - sSlot = 'friend_connection_status' - setattr(self.bob, sSlot, None) - def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): - setattr(self.bob, sSlot, True) - LOG_INFO(f"bobs_on_friend_connection_status " +repr(iStatus)) - try: - assert friend_id == self.baid - except Exception as e: - LOG.error(f"bobs_on_friend_connection_status ERROR {e}") - - opts = oToxygenToxOptions(oTOX_OARGS) - try: - setattr(self.bob, sSlot, True) - self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) - - LOG.info("test_connection_status killing alice") - self.alice.kill() #! bang - LOG.info("test_connection_status making alice") - self.alice = Tox(opts, app=oAPP) - LOG.info("test_connection_status maked alice") - - assert self.wait_otox_attrs(self.bob, [sSlot]) - except AssertionError as e: - raise - except Exception as e: - LOG.error(f"bobs_on_friend_connection_status {e}") - raise - finally: - self.bob.callback_friend_connection_status(None) - - #? assert self.bob.friend_get_connection_status(self.aid) is False - self.bob.friend_delete(self.baid) - -#? @unittest.skip('fails') - def test_friend_name(self): # fails - """ - t:self_set_name - t:friend_get_name - t:friend_get_name_size - t:on_friend_name - """ - - sSlot= 'friend_name' - #: Test friend request - - LOG.info("test_friend_name") - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - else: - assert self.bob_add_alice_as_friend() - - if not self.get_connection_status(): - LOG.warning(f"test_friend_message NOT CONNECTED") - self.loop_until_connected() - - #: Test friend name - NEWNAME = 'Jenny' - - def bobs_on_friend_name(iTox, fid, newname, iNameSize, *largs): - LOG_INFO(f"bobs_on_friend_name {sSlot} {fid}") - try: - assert fid == self.baid - assert str(newname, 'UTF-8') == NEWNAME - except Exception as e: - LOG.error(f"bobs_on_friend_name EXCEPTION {e}") - setattr(self.bob, sSlot, True) - - setattr(self.bob, sSlot, None) - self.bob.callback_friend_name(bobs_on_friend_name) - self.warn_if_no_cb(self.bob, sSlot) - try: - self.alice.self_set_name(NEWNAME) - assert self.wait_otox_attrs(self.bob, [sSlot]) - - assert self.bob.friend_get_name(self.baid) == NEWNAME - assert self.bob.friend_get_name_size(self.baid) == len(NEWNAME) - - except AssertionError as e: - raise RuntimeError(f"test_friend Failed test {e}") - - except Exception as e: - LOG.error(f"test_friend EXCEPTION {e}") - raise - - finally: - self.bob.callback_friend_name(None) - if hasattr(self.bob, sSlot + '_cb') and \ - getattr(self.bob, sSlot + '_cb'): - LOG.warning(sSlot + ' EXISTS') - - self.bob.friend_delete(self.baid) - - # wait_ensure_exec ArgumentError This client is currently not connected to the friend. - def test_friend_message(self): # fails - """ - t:on_friend_action - t:on_friend_message - t:friend_send_message - """ - - #: Test message - MSG = 'Hi, Bob!' - sSlot = 'friend_message' - if oTOX_OARGS.bIS_LOCAL: - assert self.both_add_as_friend_norequest() - else: - assert self.both_add_as_friend() - - if not self.get_connection_status(): - LOG.warning(f"test_friend_message NOT CONNECTED") - self.loop_until_connected() - - iRet = self.bob.friend_get_connection_status(self.baid) - if iRet == TOX_CONNECTION['NONE']: - LOG.error("bob.friend_get_connection_status") - raise RuntimeError("bob.friend_get_connection_status") - iRet = self.alice.friend_get_connection_status(self.abid) - if iRet == TOX_CONNECTION['NONE']: - LOG.error("alice.friend_get_connection_status") - raise RuntimeError("alice.friend_get_connection_status") - - def alices_on_friend_message(iTox, fid, msg_type, message, iSize, *largs): - LOG_DEBUG(f"alices_on_friend_message {fid} {message}") - try: - assert fid == self.alice.abid - assert msg_type == TOX_MESSAGE_TYPE['NORMAL'] - assert str(message, 'UTF-8') == MSG - except Exception as e: - LOG_ERROR(f"alices_on_friend_message EXCEPTION {e}") - else: - LOG_INFO(f"alices_on_friend_message {message}") - setattr(self.alice, sSlot, True) - - setattr(self.alice, sSlot, None) - try: - self.alice.callback_friend_message(alices_on_friend_message) - self.warn_if_no_cb(self.alice, sSlot) - - # dunno - both This client is currently NOT CONNECTED to the friend. - if True: - iMesId = self.bob.friend_send_message( - self.baid, - TOX_MESSAGE_TYPE['NORMAL'], - bytes(MSG, 'UTF-8')) - # ArgumentError('This client is currently NOT CONNECTED to the friend.') - else: - iMesId = self.wait_ensure_exec(self.bob.friend_send_message, - [self.baid, - TOX_MESSAGE_TYPE['NORMAL'], - bytes(MSG, 'UTF-8')]) - assert iMesId >= 0 - assert self.wait_otox_attrs(self.alice, [sSlot]) - except ArgumentError as e: - # ArgumentError('This client is currently NOT CONNECTED to the friend.') - # dunno - LOG.error(f"test_friend_message {e}") - raise - except AssertionError as e: - LOG.warning(f"test_friend_message {e}") - raise RuntimeError(f"Failed test test_friend_message {e}") - except Exception as e: - LOG.error(f"test_friend_message {e}") - raise - finally: - self.alice.callback_friend_message(None) - self.warn_if_cb(self.alice, sSlot) - self.bob.friend_delete(self.baid) - self.alice.friend_delete(self.abid) - -#? @unittest.skip('fails') - def test_friend_action(self): - """ - t:on_friend_action - t:on_friend_message - t:friend_send_message - """ - - if oTOX_OARGS.bIS_LOCAL: - assert self.both_add_as_friend_norequest() - else: - assert self.both_add_as_friend() - - if not self.get_connection_status(): - LOG.warning(f"test_friend_message NOT CONNECTED") - self.loop_until_connected() - - iRet = self.bob.friend_get_connection_status(self.baid) - if iRet == TOX_CONNECTION['NONE']: - LOG.error("bob.friend_get_connection_status") - raise RuntimeError("bob.friend_get_connection_status") - iRet = self.alice.friend_get_connection_status(self.abid) - if iRet == TOX_CONNECTION['NONE']: - LOG.error("alice.friend_get_connection_status") - raise RuntimeError("alice.friend_get_connection_status") - - BID = self.baid - #: Test action - ACTION = 'Kick' - sSlot = 'friend_read_action' - setattr(self.bob, sSlot, None) - sSlot = 'friend_read_receipt' - setattr(self.bob, sSlot, None) - def alices_on_friend_action(iTox, fid, msg_type, action, *largs): - sSlot = 'friend_read_action' - LOG_DEBUG(f"alices_on_friend_action") - try: - assert fid == self.bob.baid - assert msg_type == TOX_MESSAGE_TYPE['ACTION'] - assert action == ACTION - except Exception as e: - LOG_ERROR(f"alices_on_friend_action EXCEPTION {e}") - else: - LOG_INFO(f"alices_on_friend_action {message}") - setattr(self.bob, sSlot, True) - - sSlot = 'friend_read_action' - setattr(self.alice, sSlot, None) - sSlot = 'friend_read_receipt' - setattr(self.alice, sSlot, None) - def alices_on_read_reciept(iTox, fid, msg_id, *largs): - LOG_DEBUG(f"alices_on_read_reciept") - sSlot = 'friend_read_receipt' - try: - assert fid == BID - except Exception as e: - LOG_ERROR(f"alices_on_read_reciept {e}") - else: - LOG_INFO(f"alices_on_read_reciept {fid}") - setattr(self.alice, sSlot, True) - - sSlot = 'friend_read_receipt' - try: - sSlot = 'friend_read_action' - setattr(self.bob, sSlot, False) - sSlot = 'friend_read_receipt' - setattr(self.alice, sSlot, False) - - self.alice.callback_friend_read_receipt(alices_on_read_reciept) #was alices_on_friend_action - self.warn_if_no_cb(self.alice, sSlot) - assert self.wait_ensure_exec(self.bob.friend_send_message, - [self.baid, - TOX_MESSAGE_TYPE['ACTION'], - bytes(ACTION, 'UTF-8')]) - assert self.wait_otox_attrs(self.alice, [sSlot]) - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - except ArgumentError as e: - # ArgumentError('This client is currently NOT CONNECTED to the friend.') - # dunno - LOG.warning(f"test_friend_action {e}") - except Exception as e: - LOG.error(f"test_friend_action {e}") - raise - finally: - self.alice.callback_friend_read_receipt(None) - self.bob.friend_delete(self.baid) - self.alice.friend_delete(self.abid) - - @unittest.skip('fails') - def test_alice_typing_status(self): - """ - t:on_friend_read_receipt - t:on_friend_typing - t:self_set_typing - t:friend_get_typing - t:friend_get_last_online - """ - - sSlot = 'friend_typing' - # works - LOG.info("test_typing_status bob adding alice") - if oTOX_OARGS.bIS_LOCAL: - assert self.both_add_as_friend_norequest() - else: - assert self.both_add_as_friend() - - BID = self.baid - - #: Test typing status - def bob_on_friend_typing(iTox, fid, is_typing, *largs): - try: - assert fid == BID - assert is_typing is True - assert self.bob.friend_get_typing(fid) is True - except Exception as e: - LOG.error(f"BOB_ON_friend_typing {e}") - raise - else: - LOG_INFO(f"BOB_ON_friend_typing" + str(fid)) - setattr(self.bob, sSlot, True) - - setattr(self.bob, sSlot, None) - try: - if not self.get_connection_status(): - LOG.warning(f"test_friend_message NOT CONNECTED") - self.loop_until_connected() - - self.bob.callback_friend_typing(bob_on_friend_typing) - self.alice.self_set_typing(self.abid, True) - assert self.wait_otox_attrs(self.bob, [sSlot]) - if not hasattr(self.bob, sSlot+'_cb') or \ - not getattr(self.bob, sSlot+'_cb'): - LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") - except AssertionError as e: - raise RuntimeError(f"Failed test {e}") - except Exception as e: - LOG.error(f"test_alice_typing_status error={e}") - raise - finally: - self.bob.callback_friend_typing(None) - self.bob.friend_delete(self.baid) - self.alice.friend_delete(self.abid) - - @unittest.skip('unfinished') - def test_file_transfer(self): # unfinished - """ - t:file_send - t:file_send_chunk - t:file_control - t:file_seek - t:file_get_file_id - t:on_file_recv - t:on_file_recv_control - t:on_file_recv_chunk - t:on_file_chunk_request - """ - - if oTOX_OARGS.bIS_LOCAL: - assert self.bob_add_alice_as_friend_norequest() - else: - assert self.bob_add_alice_as_friend() - - BID = self.baid - - FRIEND_NUMBER = self.baid - FILE_NUMBER = 1 - FILE = os.urandom(1024 * 1024) - FILE_NAME = b"/tmp/test.bin" - if not os.path.exists(FILE_NAME): - with open(FILE_NAME, 'wb') as oFd: - oFd.write(FILE) - FILE_SIZE = len(FILE) - OFFSET = 567 - - m = hashlib.md5() - m.update(FILE[OFFSET:]) - FILE_DIGEST = m.hexdigest() - - CONTEXT = { 'FILE': bytes(), 'RECEIVED': 0, 'START': False, 'SENT': 0 } - - def alice_on_file_recv(iTox, fid, file_number, kind, size, filename): - LOG_DEBUG(f"ALICE_ON_file_recv fid={fid} {file_number}") - try: - assert size == FILE_SIZE - assert filename == FILE_NAME - retv = self.alice.file_seek(fid, file_number, OFFSET) - assert retv is True - self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['RESUME']) - except Exception as e: - LOG_ERROR(f"ALICE_ON_file_recv {e}") - else: - LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) - - def alice_on_file_recv_control(iTox, fid, file_number, control, *largs): - # TOX_FILE_CONTROL = { 'RESUME': 0, 'PAUSE': 1, 'CANCEL': 2,} - LOG_DEBUG(f"ALICE_ON_file_recv_control fid={fid} {file_number} {control}") - try: - assert FILE_NUMBER == file_number - # FixMe _FINISHED? - if False and control == TOX_FILE_CONTROL['RESUME']: - # assert CONTEXT['RECEIVED'] == FILE_SIZE - # m = hashlib.md5() - # m.update(CONTEXT['FILE']) - # assert m.hexdigest() == FILE_DIGEST - self.alice.completed = True - except Exception as e: - LOG_ERROR(f"ALICE_ON_file_recv {e}") - else: - LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) - - self.alice.completed = False - def alice_on_file_recv_chunk(iTox, fid, file_number, position, iNumBytes, *largs): - LOG_DEBUG(f"ALICE_ON_file_recv_chunk {fid} {file_number}") - # FixMe - use file_number and iNumBytes to get data? - data = '' - try: - if data is None: - assert CONTEXT['RECEIVED'] == (FILE_SIZE - OFFSET) - m = hashlib.md5() - m.update(CONTEXT['FILE']) - assert m.hexdigest() == FILE_DIGEST - self.alice.completed = True - self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['CANCEL']) - return - - CONTEXT['FILE'] += data - CONTEXT['RECEIVED'] += len(data) - # if CONTEXT['RECEIVED'] < FILE_SIZE: - # assert self.file_data_remaining( - # fid, file_number, 1) == FILE_SIZE - CONTEXT['RECEIVED'] - except Exception as e: - LOG_ERROR(f"ALICE_ON_file_recv_chunk {e}") - else: - LOG_INFO(f"ALICE_ON_file_recv_chunk {fid}") - - # AliceTox.on_file_send_request = on_file_send_request - # AliceTox.on_file_control = on_file_control - # AliceTox.on_file_data = on_file_data - - LOG.info(f"test_file_transfer: baid={self.baid}") - try: - self.alice.callback_file_recv(alice_on_file_recv) - self.alice.callback_file_recv_control(alice_on_file_recv_control) - self.alice.callback_file_recv_chunk(alice_on_file_recv_chunk) - - self.bob.completed = False - def bob_on_file_recv_control2(iTox, fid, file_number, control): - LOG_DEBUG(f"BOB_ON_file_recv_control2 {fid} {file_number} control={control}") - if control == TOX_FILE_CONTROL['RESUME']: - CONTEXT['START'] = True - elif control == TOX_FILE_CONTROL['CANCEL']: - self.bob.completed = True - pass - - def bob_on_file_chunk_request(iTox, fid, file_number, position, length, *largs): - LOG_DEBUG(f"BOB_ON_file_chunk_request {fid} {file_number}") - if length == 0: - return - data = FILE[position:(position + length)] - self.bob.file_send_chunk(fid, file_number, position, data) - - sSlot = 'file_recv_control' - self.bob.callback_file_recv_control(bob_on_file_recv_control2) - self.bob.callback_file_chunk_request(bob_on_file_chunk_request) - - # was FILE_ID = FILE_NAME - FILE_ID = 32*'1' # - FILE_NAME = b'test.in' - - if not self.get_connection_status(): - LOG.warning(f"test_file_transfer NOT CONNECTED") - self.loop_until_connected() - - i = 0 - iKind = 0 - while i < 2: - i += 1 - try: - FN = self.bob.file_send(self.baid, iKind, FILE_SIZE, FILE_ID, FILE_NAME) - LOG.info(f"test_file_transfer bob.file_send {FN}") - except ArgumentError as e: - LOG.debug(f"test_file_transfer bob.file_send {e} {i}") - # ctypes.ArgumentError: This client is currently not connected to the friend. - raise - else: - break - self.loop(100) - sleep(1) - else: - LOG.error(f"test_file_transfer bob.file_send 2") - raise RuntimeError(f"test_file_transfer bob.file_send {THRESHOLD // 2}") - - # UINT32_MAX - FID = self.bob.file_get_file_id(self.baid, FN) - hexFID = "".join([hex(ord(c))[2:].zfill(2) for c in FILE_NAME]) - assert FID.startswith(hexFID.upper()) - - if not self.wait_obj_attrs(self.bob, ['completed']): - LOG.warning(f"test_file_transfer Bob not completed") - return False - if not self.wait_obj_attrs(self.alice, ['completed']): - LOG.warning(f"test_file_transfer Alice not completed") - return False - return True - - except (ArgumentError, ValueError,) as e: - # ValueError: non-hexadecimal number found in fromhex() arg at position 0 - LOG_ERROR(f"test_file_transfer: {e}") - raise - - except Exception as e: - LOG_ERROR(f"test_file_transfer:: {e}") - LOG_DEBUG('\n' + traceback.format_exc()) - raise - - finally: - self.bob.friend_delete(self.baid) - self.alice.callback_file_recv(None) - self.alice.callback_file_recv_control(None) - self.alice.callback_file_recv_chunk(None) - self.bob.callback_file_recv_control(None) - self.bob.callback_file_chunk_request(None) - - LOG_INFO(f"test_file_transfer:: self.wait_objs_attr completed") - - @unittest.skip('crashes') - def test_tox_savedata(self): # works sorta - # but "{addr} != {self.alice.self_get_address()}" - """ - t:get_savedata_size - t:get_savedata - """ - # Fatal Python error: Aborted - # "/var/local/src/toxygen_wrapper/wrapper/tox.py", line 180 in kill - return - - assert self.alice.get_savedata_size() > 0 - data = self.alice.get_savedata() - assert data is not None - addr = self.alice.self_get_address() - # self._address - - try: - LOG.info("test_tox_savedata alice.kill") - # crashes - self.alice.kill() - except: - pass - - oArgs = oTOX_OARGS - opts = oToxygenToxOptions(oArgs) - opts.savedata_data = data - opts.savedata_length = len(data) - - self.alice = Tox(tox_options=opts) - if addr != self.alice.self_get_address(): - LOG.warning("test_tox_savedata " + - f"{addr} != {self.alice.self_get_address()}") - else: - LOG.info("passed test_tox_savedata") - -def vOargsToxPreamble(oArgs, Tox, ToxTest): - - ts.vSetupLogging(oArgs) - - methods = set([x for x in dir(Tox) if not x[0].isupper() - and not x[0] == '_']) - docs = "".join([getattr(ToxTest, x).__doc__ for x in dir(ToxTest) - if getattr(ToxTest, x).__doc__ is not None]) - - tested = set(re.findall(r't:(.*?)\n', docs)) - not_tested = methods.difference(tested) - - logging.info('Test Coverage: %.2f%%' % (len(tested) * 100.0 / len(methods))) - if len(not_tested): - logging.info('Not tested:\n %s' % "\n ".join(sorted(list(not_tested)))) - -### - -def iMain(oArgs): - failfast=True - - vOargsToxPreamble(oArgs, Tox, ToxSuite) - # https://stackoverflow.com/questions/35930811/how-to-sort-unittest-testcases-properly/35930812#35930812 - cases = ts.suiteFactory(*ts.caseFactory([ToxSuite])) - if color_runner: - runner = color_runner.runner.TextTestRunner(verbosity=2, failfast=failfast) - else: - runner = unittest.TextTestRunner(verbosity=2, failfast=failfast, warnings='ignore') - runner.run(cases) - -def oToxygenToxOptions(oArgs): - data = None - tox_options = toxygen_wrapper.tox.Tox.options_new() - if oArgs.proxy_type: - tox_options.contents.proxy_type = int(oArgs.proxy_type) - tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8') - tox_options.contents.proxy_port = int(oArgs.proxy_port) - tox_options.contents.udp_enabled = False - else: - tox_options.contents.udp_enabled = oArgs.udp_enabled - if not os.path.exists('/proc/sys/net/ipv6'): - oArgs.ipv6_enabled = False - else: - tox_options.contents.ipv6_enabled = oArgs.ipv6_enabled - - tox_options.contents.tcp_port = int(oArgs.tcp_port) - tox_options.contents.dht_announcements_enabled = oArgs.dht_announcements_enabled - tox_options.contents.hole_punching_enabled = oArgs.hole_punching_enabled - - # overrides - tox_options.contents.local_discovery_enabled = False - tox_options.contents.experimental_thread_safety = False - # REQUIRED!! - if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'): - LOG.warning('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled)) - tox_options.contents.ipv6_enabled = False - else: - tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled) - - if data: # load existing profile - tox_options.contents.savedata_type = enums.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 = enums.TOX_SAVEDATA_TYPE['NONE'] - tox_options.contents.savedata_data = None - tox_options.contents.savedata_length = 0 - - #? tox_options.contents.log_callback = LOG - if tox_options._options_pointer: - # LOG.debug("Adding logging to tox_options._options_pointer ") - ts.vAddLoggerCallback(tox_options, ts.on_log) - else: - LOG.warning("No tox_options._options_pointer " +repr(tox_options._options_pointer)) - - return tox_options - -def oArgparse(lArgv): - parser = ts.oMainArgparser() - parser.add_argument('profile', type=str, nargs='?', default=None, - help='Path to Tox profile') - oArgs = parser.parse_args(lArgv) - - for key in ts.lBOOLEANS: - if key not in oArgs: continue - val = getattr(oArgs, key) - setattr(oArgs, key, bool(val)) - - if hasattr(oArgs, 'sleep'): - if oArgs.sleep == 'qt': - pass # broken or gevent.sleep(idle_period) - elif oArgs.sleep == 'gevent': - pass # broken or gevent.sleep(idle_period) - else: - oArgs.sleep = 'time' - - return oArgs - -def main(lArgs=None): - global oTOX_OARGS - if lArgs is None: lArgs = [] - oArgs = oArgparse(lArgs) - global bIS_LOCAL - bIS_LOCAL = oArgs.network in ['newlocal', 'localnew', 'local'] - oTOX_OARGS = oArgs - setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) - bIS_LOCAL = True - setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) - # oTOX_OPTIONS = ToxOptions() - global oTOX_OPTIONS - oTOX_OPTIONS = oToxygenToxOptions(oArgs) - if coloredlogs: - # https://pypi.org/project/coloredlogs/ - coloredlogs.install(level=oArgs.loglevel, - logger=LOG, - # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d] - fmt='%(name)s %(levelname)s %(message)s' - ) - else: - logging.basicConfig(level=oArgs.loglevel) # logging.INFO - - return iMain(oArgs) - -if __name__ == '__main__': - sys.exit(main(sys.argv[1:])) - -# Ran 33 tests in 51.733s diff --git a/toxygen/third_party/__init__.py b/toxygen/third_party/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/third_party/qweechat/data/icons/README b/toxygen/third_party/qweechat/data/icons/README deleted file mode 100644 index 0694819..0000000 --- a/toxygen/third_party/qweechat/data/icons/README +++ /dev/null @@ -1,41 +0,0 @@ -Copyright and license for images -================================ - - -Files: weechat.png, bullet_green_8x8.png, bullet_yellow_8x8.png - - Copyright (C) 2011-2022 Sébastien Helleu - Released under GPLv3. - - - -Files: application-exit.png, dialog-close.png, dialog-ok-apply.png, - dialog-password.png, dialog-warning.png, document-save.png, - edit-find.png, help-about.png, network-connect.png, - network-disconnect.png, preferences-other.png - - Files come from Debian package "oxygen-icon-theme": - - The Oxygen Icon Theme - Copyright (C) 2007 Nuno Pinheiro - Copyright (C) 2007 David Vignoni - Copyright (C) 2007 David Miller - Copyright (C) 2007 Johann Ollivier Lapeyre - Copyright (C) 2007 Kenneth Wimer - Copyright (C) 2007 Riccardo Iaconelli - and others - - License: - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 3 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library. If not, see . diff --git a/toxygen/third_party/qweechat/data/icons/application-exit.png b/toxygen/third_party/qweechat/data/icons/application-exit.png deleted file mode 100644 index dd76354..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/application-exit.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/bullet_green_8x8.png b/toxygen/third_party/qweechat/data/icons/bullet_green_8x8.png deleted file mode 100644 index ea80953..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/bullet_green_8x8.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/bullet_yellow_8x8.png b/toxygen/third_party/qweechat/data/icons/bullet_yellow_8x8.png deleted file mode 100644 index 58ad5cf..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/bullet_yellow_8x8.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-close.png b/toxygen/third_party/qweechat/data/icons/dialog-close.png deleted file mode 100644 index 2c2f99e..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/dialog-close.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-ok-apply.png b/toxygen/third_party/qweechat/data/icons/dialog-ok-apply.png deleted file mode 100644 index f1d290c..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/dialog-ok-apply.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-password.png b/toxygen/third_party/qweechat/data/icons/dialog-password.png deleted file mode 100644 index 2151029..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/dialog-password.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-warning.png b/toxygen/third_party/qweechat/data/icons/dialog-warning.png deleted file mode 100644 index 43ca31a..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/dialog-warning.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/document-save.png b/toxygen/third_party/qweechat/data/icons/document-save.png deleted file mode 100644 index 7fa489c..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/document-save.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/edit-find.png b/toxygen/third_party/qweechat/data/icons/edit-find.png deleted file mode 100644 index 9b3fe6b..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/edit-find.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/help-about.png b/toxygen/third_party/qweechat/data/icons/help-about.png deleted file mode 100644 index ee59e17..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/help-about.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/network-connect.png b/toxygen/third_party/qweechat/data/icons/network-connect.png deleted file mode 100644 index 4e32020..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/network-connect.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/network-disconnect.png b/toxygen/third_party/qweechat/data/icons/network-disconnect.png deleted file mode 100644 index 623c8e0..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/network-disconnect.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/preferences-other.png b/toxygen/third_party/qweechat/data/icons/preferences-other.png deleted file mode 100644 index 711881e..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/preferences-other.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/data/icons/weechat.png b/toxygen/third_party/qweechat/data/icons/weechat.png deleted file mode 100644 index 7eca5c8..0000000 Binary files a/toxygen/third_party/qweechat/data/icons/weechat.png and /dev/null differ diff --git a/toxygen/third_party/qweechat/weechat/__init__.py b/toxygen/third_party/qweechat/weechat/__init__.py deleted file mode 100644 index f510618..0000000 --- a/toxygen/third_party/qweechat/weechat/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2011-2022 Sébastien Helleu -# -# This file is part of QWeeChat, a Qt remote GUI for WeeChat. -# -# QWeeChat is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# QWeeChat is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with QWeeChat. If not, see . -# diff --git a/toxygen/third_party/qweechat/weechat/color.py b/toxygen/third_party/qweechat/weechat/color.py deleted file mode 100644 index 0ed52ef..0000000 --- a/toxygen/third_party/qweechat/weechat/color.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -# -# color.py - remove/replace colors in WeeChat strings -# -# Copyright (C) 2011-2022 Sébastien Helleu -# -# This file is part of QWeeChat, a Qt remote GUI for WeeChat. -# -# QWeeChat is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# QWeeChat is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with QWeeChat. If not, see . -# - -"""Remove/replace colors in WeeChat strings.""" - -import re -import logging - -RE_COLOR_ATTRS = r'[*!/_|]*' -RE_COLOR_STD = r'(?:%s\d{2})' % RE_COLOR_ATTRS -RE_COLOR_EXT = r'(?:@%s\d{5})' % RE_COLOR_ATTRS -RE_COLOR_ANY = r'(?:%s|%s)' % (RE_COLOR_STD, RE_COLOR_EXT) -# \x19: color code, \x1A: set attribute, \x1B: remove attribute, \x1C: reset -RE_COLOR = re.compile( - r'(\x19(?:\d{2}|F%s|B\d{2}|B@\d{5}|E|\\*%s(~%s)?|@\d{5}|b.|\x1C))|\x1A.|' - r'\x1B.|\x1C' - % (RE_COLOR_ANY, RE_COLOR_ANY, RE_COLOR_ANY)) - -TERMINAL_COLORS = \ - '000000cd000000cd00cdcd000000cdcd00cd00cdcde5e5e5' \ - '4d4d4dff000000ff00ffff000000ffff00ff00ffffffffff' \ - '00000000002a0000550000800000aa0000d4002a00002a2a' \ - '002a55002a80002aaa002ad400550000552a005555005580' \ - '0055aa0055d400800000802a0080550080800080aa0080d4' \ - '00aa0000aa2a00aa5500aa8000aaaa00aad400d40000d42a' \ - '00d45500d48000d4aa00d4d42a00002a002a2a00552a0080' \ - '2a00aa2a00d42a2a002a2a2a2a2a552a2a802a2aaa2a2ad4' \ - '2a55002a552a2a55552a55802a55aa2a55d42a80002a802a' \ - '2a80552a80802a80aa2a80d42aaa002aaa2a2aaa552aaa80' \ - '2aaaaa2aaad42ad4002ad42a2ad4552ad4802ad4aa2ad4d4' \ - '55000055002a5500555500805500aa5500d4552a00552a2a' \ - '552a55552a80552aaa552ad455550055552a555555555580' \ - '5555aa5555d455800055802a5580555580805580aa5580d4' \ - '55aa0055aa2a55aa5555aa8055aaaa55aad455d40055d42a' \ - '55d45555d48055d4aa55d4d480000080002a800055800080' \ - '8000aa8000d4802a00802a2a802a55802a80802aaa802ad4' \ - '80550080552a8055558055808055aa8055d480800080802a' \ - '8080558080808080aa8080d480aa0080aa2a80aa5580aa80' \ - '80aaaa80aad480d40080d42a80d45580d48080d4aa80d4d4' \ - 'aa0000aa002aaa0055aa0080aa00aaaa00d4aa2a00aa2a2a' \ - 'aa2a55aa2a80aa2aaaaa2ad4aa5500aa552aaa5555aa5580' \ - 'aa55aaaa55d4aa8000aa802aaa8055aa8080aa80aaaa80d4' \ - 'aaaa00aaaa2aaaaa55aaaa80aaaaaaaaaad4aad400aad42a' \ - 'aad455aad480aad4aaaad4d4d40000d4002ad40055d40080' \ - 'd400aad400d4d42a00d42a2ad42a55d42a80d42aaad42ad4' \ - 'd45500d4552ad45555d45580d455aad455d4d48000d4802a' \ - 'd48055d48080d480aad480d4d4aa00d4aa2ad4aa55d4aa80' \ - 'd4aaaad4aad4d4d400d4d42ad4d455d4d480d4d4aad4d4d4' \ - '0808081212121c1c1c2626263030303a3a3a4444444e4e4e' \ - '5858586262626c6c6c7676768080808a8a8a9494949e9e9e' \ - 'a8a8a8b2b2b2bcbcbcc6c6c6d0d0d0dadadae4e4e4eeeeee' - -# WeeChat basic colors (color name, index in terminal colors) -WEECHAT_BASIC_COLORS = ( - ('default', 0), ('black', 0), ('darkgray', 8), ('red', 1), - ('lightred', 9), ('green', 2), ('lightgreen', 10), ('brown', 3), - ('yellow', 11), ('blue', 4), ('lightblue', 12), ('magenta', 5), - ('lightmagenta', 13), ('cyan', 6), ('lightcyan', 14), ('gray', 7), - ('white', 0)) - - -log = logging.getLogger(__name__) - - -class Color(): - def __init__(self, color_options, debug=False): - self.color_options = color_options - self.debug = debug - - def _rgb_color(self, index): - color = TERMINAL_COLORS[index*6:(index*6)+6] - col_r = int(color[0:2], 16) * 0.85 - col_g = int(color[2:4], 16) * 0.85 - col_b = int(color[4:6], 16) * 0.85 - return '%02x%02x%02x' % (col_r, col_g, col_b) - - def _convert_weechat_color(self, color): - try: - index = int(color) - return '\x01(Fr%s)' % self.color_options[index] - except Exception: # noqa: E722 - log.debug('Error decoding WeeChat color "%s"', color) - return '' - - def _convert_terminal_color(self, fg_bg, attrs, color): - try: - index = int(color) - return '\x01(%s%s#%s)' % (fg_bg, attrs, self._rgb_color(index)) - except Exception: # noqa: E722 - log.debug('Error decoding terminal color "%s"', color) - return '' - - def _convert_color_attr(self, fg_bg, color): - extended = False - if color[0].startswith('@'): - extended = True - color = color[1:] - attrs = '' - # keep_attrs = False - while color.startswith(('*', '!', '/', '_', '|')): - # TODO: manage the "keep attributes" flag - # if color[0] == '|': - # keep_attrs = True - attrs += color[0] - color = color[1:] - if extended: - return self._convert_terminal_color(fg_bg, attrs, color) - try: - index = int(color) - return self._convert_terminal_color(fg_bg, attrs, - WEECHAT_BASIC_COLORS[index][1]) - except Exception: # noqa: E722 - log.debug('Error decoding color "%s"', color) - return '' - - def _attrcode_to_char(self, code): - codes = { - '\x01': '*', - '\x02': '!', - '\x03': '/', - '\x04': '_', - } - return codes.get(code, '') - - def _convert_color(self, match): - color = match.group(0) - if color[0] == '\x19': - if color[1] == 'b': - # bar code, ignored - return '' - if color[1] == '\x1C': - # reset - return '\x01(Fr)\x01(Br)' - if color[1] in ('F', 'B'): - # foreground or background - return self._convert_color_attr(color[1], color[2:]) - if color[1] == '*': - # foreground with optional background - items = color[2:].split(',') - str_col = self._convert_color_attr('F', items[0]) - if len(items) > 1: - str_col += self._convert_color_attr('B', items[1]) - return str_col - if color[1] == '@': - # direct ncurses pair number, ignored - return '' - if color[1] == 'E': - # text emphasis, ignored - return '' - if color[1:].isdigit(): - return self._convert_weechat_color(int(color[1:])) - elif color[0] == '\x1A': - # set attribute - return '\x01(+%s)' % self._attrcode_to_char(color[1]) - elif color[0] == '\x1B': - # remove attribute - return '\x01(-%s)' % self._attrcode_to_char(color[1]) - elif color[0] == '\x1C': - # reset - return '\x01(Fr)\x01(Br)' - # should never be executed! - return match.group(0) - - def _convert_color_debug(self, match): - group = match.group(0) - for code in (0x01, 0x02, 0x03, 0x04, 0x19, 0x1A, 0x1B): - group = group.replace(chr(code), '' % code) - return group - - def convert(self, text): - if not text: - return '' - if self.debug: - return RE_COLOR.sub(self._convert_color_debug, text) - return RE_COLOR.sub(self._convert_color, text) - - -def remove(text): - """Remove colors in a WeeChat string.""" - if not text: - return '' - return re.sub(RE_COLOR, '', text) diff --git a/toxygen/third_party/qweechat/weechat/protocol.py b/toxygen/third_party/qweechat/weechat/protocol.py deleted file mode 100644 index 90ce7d2..0000000 --- a/toxygen/third_party/qweechat/weechat/protocol.py +++ /dev/null @@ -1,361 +0,0 @@ -# -*- coding: utf-8 -*- -# -# protocol.py - decode binary messages received from WeeChat/relay -# -# Copyright (C) 2011-2022 Sébastien Helleu -# -# This file is part of QWeeChat, a Qt remote GUI for WeeChat. -# -# QWeeChat is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# QWeeChat is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with QWeeChat. If not, see . -# - -# -# For info about protocol and format of messages, please read document -# "WeeChat Relay Protocol", available at: https://weechat.org/doc/ -# -# History: -# -# 2011-11-23, Sébastien Helleu : -# start dev -# - -"""Decode binary messages received from WeeChat/relay.""" - -import collections -import struct -import zlib - - -class WeechatDict(collections.OrderedDict): - def __str__(self): - return '{%s}' % ', '.join( - ['%s: %s' % (repr(key), repr(self[key])) for key in self]) - - -class WeechatObject: - def __init__(self, objtype, value, separator='\n'): - self.objtype = objtype - self.value = value - self.separator = separator - self.indent = ' ' if separator == '\n' else '' - self.separator1 = '\n%s' % self.indent if separator == '\n' else '' - - def _str_value(self, val): - if isinstance(val, str) and val is not None: - return '\'%s\'' % val - return str(val) - - def _str_value_hdata(self): - lines = ['%skeys: %s%s%spath: %s' % (self.separator1, - str(self.value['keys']), - self.separator, - self.indent, - str(self.value['path']))] - for i, item in enumerate(self.value['items']): - lines.append(' item %d:%s%s' % ( - (i + 1), self.separator, - self.separator.join( - ['%s%s: %s' % (self.indent * 2, key, - self._str_value(value)) - for key, value in item.items()]))) - return '\n'.join(lines) - - def _str_value_infolist(self): - lines = ['%sname: %s' % (self.separator1, self.value['name'])] - for i, item in enumerate(self.value['items']): - lines.append(' item %d:%s%s' % ( - (i + 1), self.separator, - self.separator.join( - ['%s%s: %s' % (self.indent * 2, key, - self._str_value(value)) - for key, value in item.items()]))) - return '\n'.join(lines) - - def _str_value_other(self): - return self._str_value(self.value) - - def __str__(self): - obj_cb = { - 'hda': self._str_value_hdata, - 'inl': self._str_value_infolist, - } - return '%s: %s' % (self.objtype, - obj_cb.get(self.objtype, self._str_value_other)()) - - -class WeechatObjects(list): - def __init__(self, separator='\n'): - super().__init__() - self.separator = separator - - def __str__(self): - return self.separator.join([str(obj) for obj in self]) - - -class WeechatMessage: - def __init__(self, size, size_uncompressed, compression, uncompressed, - msgid, objects): - self.size = size - self.size_uncompressed = size_uncompressed - self.compression = compression - self.uncompressed = uncompressed - self.msgid = msgid - self.objects = objects - - def __str__(self): - if self.compression != 0: - return 'size: %d/%d (%d%%), id=\'%s\', objects:\n%s' % ( - self.size, self.size_uncompressed, - 100 - ((self.size * 100) // self.size_uncompressed), - self.msgid, self.objects) - return 'size: %d, id=\'%s\', objects:\n%s' % (self.size, - self.msgid, - self.objects) - - -class Protocol: - """Decode binary message received from WeeChat/relay.""" - - def __init__(self): - self.data = '' - self._obj_cb = { - 'chr': self._obj_char, - 'int': self._obj_int, - 'lon': self._obj_long, - 'str': self._obj_str, - 'buf': self._obj_buffer, - 'ptr': self._obj_ptr, - 'tim': self._obj_time, - 'htb': self._obj_hashtable, - 'hda': self._obj_hdata, - 'inf': self._obj_info, - 'inl': self._obj_infolist, - 'arr': self._obj_array, - } - - def _obj_type(self): - """Read type in data (3 chars).""" - if len(self.data) < 3: - self.data = '' - return '' - objtype = self.data[0:3].decode() - self.data = self.data[3:] - return objtype - - def _obj_len_data(self, length_size): - """Read length (1 or 4 bytes), then value with this length.""" - if len(self.data) < length_size: - self.data = '' - return None - if length_size == 1: - length = struct.unpack('B', self.data[0:1])[0] - self.data = self.data[1:] - else: - length = self._obj_int() - if length < 0: - return None - if length > 0: - value = self.data[0:length] - self.data = self.data[length:] - else: - value = '' - return value - - def _obj_char(self): - """Read a char in data.""" - if len(self.data) < 1: - return 0 - value = struct.unpack('b', self.data[0:1])[0] - self.data = self.data[1:] - return value - - def _obj_int(self): - """Read an integer in data (4 bytes).""" - if len(self.data) < 4: - self.data = '' - return 0 - value = struct.unpack('>i', self.data[0:4])[0] - self.data = self.data[4:] - return value - - def _obj_long(self): - """Read a long integer in data (length on 1 byte + value as string).""" - value = self._obj_len_data(1) - if value is None: - return None - return int(value) - - def _obj_str(self): - """Read a string in data (length on 4 bytes + content).""" - value = self._obj_len_data(4) - if value in ("", None): - return "" - return value.decode() - - def _obj_buffer(self): - """Read a buffer in data (length on 4 bytes + data).""" - return self._obj_len_data(4) - - def _obj_ptr(self): - """Read a pointer in data (length on 1 byte + value as string).""" - value = self._obj_len_data(1) - if value is None: - return None - return '0x%s' % value - - def _obj_time(self): - """Read a time in data (length on 1 byte + value as string).""" - value = self._obj_len_data(1) - if value is None: - return None - return int(value) - - def _obj_hashtable(self): - """ - Read a hashtable in data - (type for keys + type for values + count + items). - """ - type_keys = self._obj_type() - type_values = self._obj_type() - count = self._obj_int() - hashtable = WeechatDict() - for _ in range(count): - key = self._obj_cb[type_keys]() - value = self._obj_cb[type_values]() - hashtable[key] = value - return hashtable - - def _obj_hdata(self): - """Read a hdata in data.""" - path = self._obj_str() - keys = self._obj_str() - count = self._obj_int() - list_path = path.split('/') if path else [] - list_keys = keys.split(',') if keys else [] - keys_types = [] - dict_keys = WeechatDict() - for key in list_keys: - items = key.split(':') - keys_types.append(items) - dict_keys[items[0]] = items[1] - items = [] - for _ in range(count): - item = WeechatDict() - item['__path'] = [] - pointers = [] - for _ in enumerate(list_path): - pointers.append(self._obj_ptr()) - for key, objtype in keys_types: - item[key] = self._obj_cb[objtype]() - item['__path'] = pointers - items.append(item) - return { - 'path': list_path, - 'keys': dict_keys, - 'count': count, - 'items': items, - } - - def _obj_info(self): - """Read an info in data.""" - name = self._obj_str() - value = self._obj_str() - return (name, value) - - def _obj_infolist(self): - """Read an infolist in data.""" - name = self._obj_str() - count_items = self._obj_int() - items = [] - for _ in range(count_items): - count_vars = self._obj_int() - variables = WeechatDict() - for _ in range(count_vars): - var_name = self._obj_str() - var_type = self._obj_type() - var_value = self._obj_cb[var_type]() - variables[var_name] = var_value - items.append(variables) - return { - 'name': name, - 'items': items - } - - def _obj_array(self): - """Read an array of values in data.""" - type_values = self._obj_type() - count_values = self._obj_int() - values = [] - for _ in range(count_values): - values.append(self._obj_cb[type_values]()) - return values - - def decode(self, data, separator='\n'): - """Decode binary data and return list of objects.""" - self.data = data - size = len(self.data) - size_uncompressed = size - uncompressed = None - # uncompress data (if it is compressed) - compression = struct.unpack('b', self.data[4:5])[0] - if compression: - uncompressed = zlib.decompress(self.data[5:]) - size_uncompressed = len(uncompressed) + 5 - uncompressed = b'%s%s%s' % (struct.pack('>i', size_uncompressed), - struct.pack('b', 0), uncompressed) - self.data = uncompressed - else: - uncompressed = self.data[:] - # skip length and compression flag - self.data = self.data[5:] - # read id - msgid = self._obj_str() - if msgid is None: - msgid = '' - # read objects - objects = WeechatObjects(separator=separator) - while len(self.data) > 0: - objtype = self._obj_type() - value = self._obj_cb[objtype]() - objects.append(WeechatObject(objtype, value, separator=separator)) - return WeechatMessage(size, size_uncompressed, compression, - uncompressed, msgid, objects) - - -def hex_and_ascii(data, bytes_per_line=10): - """Convert a QByteArray to hex + ascii output.""" - num_lines = ((len(data) - 1) // bytes_per_line) + 1 - if num_lines == 0: - return '' - lines = [] - for i in range(num_lines): - str_hex = [] - str_ascii = [] - for j in range(bytes_per_line): - # We can't easily iterate over individual bytes, so we are going to - # do it this way. - index = (i*bytes_per_line) + j - char = data[index:index+1] - if not char: - char = b'x' - byte = struct.unpack('B', char)[0] - str_hex.append(b'%02X' % int(byte)) - if 32 <= byte <= 127: - str_ascii.append(char) - else: - str_ascii.append(b'.') - fmt = b'%%-%ds %%s' % ((bytes_per_line * 3) - 1) - lines.append(fmt % (b' '.join(str_hex), - b''.join(str_ascii))) - return b'\n'.join(lines) diff --git a/toxygen/third_party/qweechat/weechat/testproto.py b/toxygen/third_party/qweechat/weechat/testproto.py deleted file mode 100644 index 2afabd9..0000000 --- a/toxygen/third_party/qweechat/weechat/testproto.py +++ /dev/null @@ -1,252 +0,0 @@ -# -*- coding: utf-8 -*- -# -# testproto.py - command-line program for testing WeeChat/relay protocol -# -# Copyright (C) 2013-2022 Sébastien Helleu -# -# This file is part of QWeeChat, a Qt remote GUI for WeeChat. -# -# QWeeChat is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# QWeeChat is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with QWeeChat. If not, see . -# - -"""Command-line program for testing WeeChat/relay protocol.""" - -import argparse -import os -import select -import shlex -import socket -import struct -import sys -import time -import traceback - -from qweechat.weechat import protocol - -qweechat_version = '0.1' - -NAME = 'qweechat-testproto' - - -class TestProto(object): - """Test of WeeChat/relay protocol.""" - - def __init__(self, args): - self.args = args - self.sock = None - self.has_quit = False - self.address = '{self.args.hostname}/{self.args.port} ' \ - '(IPv{0})'.format(6 if self.args.ipv6 else 4, self=self) - - def connect(self): - """ - Connect to WeeChat/relay. - Return True if OK, False if error. - """ - inet = socket.AF_INET6 if self.args.ipv6 else socket.AF_INET - try: - self.sock = socket.socket(inet, socket.SOCK_STREAM) - self.sock.connect((self.args.hostname, self.args.port)) - except Exception: - if self.sock: - self.sock.close() - print('Failed to connect to', self.address) - return False - - print(f'Connected to {self.address} socket {self.sock}') - return True - - def send(self, messages): - """ - Send a text message to WeeChat/relay. - Return True if OK, False if error. - """ - try: - for msg in messages.split(b'\n'): - if msg == b'quit': - self.has_quit = True - self.sock.sendall(msg + b'\n') - sys.stdout.write( - (b'\x1b[33m<-- ' + msg + b'\x1b[0m\n').decode()) - except Exception: # noqa: E722 - traceback.print_exc() - print('Failed to send message') - return False - return True - - def decode(self, message): - """ - Decode a binary message received from WeeChat/relay. - Return True if OK, False if error. - """ - try: - proto = protocol.Protocol() - msgd = proto.decode(message, - separator=b'\n' if self.args.debug > 0 - else ', ') - print('') - if self.args.debug >= 2 and msgd.uncompressed: - # display raw message - print('\x1b[32m--> message uncompressed ({0} bytes):\n' - '{1}\x1b[0m' - ''.format(msgd.size_uncompressed, - protocol.hex_and_ascii(msgd.uncompressed, 20))) - # display decoded message - print('\x1b[32m--> {0}\x1b[0m'.format(msgd)) - except Exception: # noqa: E722 - traceback.print_exc() - print('Error while decoding message from WeeChat') - return False - return True - - def send_stdin(self): - """ - Send commands from standard input if some data is available. - Return True if OK (it's OK if stdin has no commands), - False if error. - """ - inr = select.select([sys.stdin], [], [], 0)[0] - if inr: - data = os.read(sys.stdin.fileno(), 4096) - if data: - if not self.send(data.strip()): - self.sock.close() - return False - # open stdin to read user commands - sys.stdin = open('/dev/tty') - return True - - def mainloop(self): - """ - Main loop: read keyboard, send commands, read socket, - decode/display binary messages received from WeeChat/relay. - Return 0 if OK, 4 if send error, 5 if decode error. - """ - if self.has_quit: - return 0 - message = b'' - recvbuf = b'' - prompt = b'\x1b[36mrelay> \x1b[0m' - sys.stdout.write(prompt.decode()) - sys.stdout.flush() - try: - while not self.has_quit: - inr = select.select([sys.stdin, self.sock], [], [], 1)[0] - for _file in inr: - if _file == sys.stdin: - buf = os.read(_file.fileno(), 4096) - if buf: - message += buf - if b'\n' in message: - messages = message.split(b'\n') - msgsent = b'\n'.join(messages[:-1]) - if msgsent and not self.send(msgsent): - return 4 - message = messages[-1] - sys.stdout.write((prompt + message).decode()) - # sys.stdout.write(prompt + message) - sys.stdout.flush() - else: - buf = _file.recv(4096) - if buf: - recvbuf += buf - while len(recvbuf) >= 4: - remainder = None - length = struct.unpack('>i', recvbuf[0:4])[0] - if len(recvbuf) < length: - # partial message, just wait for the - # end of message - break - # more than one message? - if length < len(recvbuf): - # save beginning of another message - remainder = recvbuf[length:] - recvbuf = recvbuf[0:length] - if not self.decode(recvbuf): - return 5 - if remainder: - recvbuf = remainder - else: - recvbuf = b'' - sys.stdout.write((prompt + message).decode()) - sys.stdout.flush() - except Exception: # noqa: E722 - traceback.print_exc() - self.send(b'quit') - return 0 - - def __del__(self): - print('Closing connection with', self.address) - time.sleep(0.5) - self.sock.close() - - -def main(): - """Main function.""" - # parse command line arguments - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - fromfile_prefix_chars='@', - description='Command-line program for testing WeeChat/relay protocol.', - epilog=''' -Environment variable "QWEECHAT_PROTO_OPTIONS" can be set with default options. -Argument "@file.txt" can be used to read default options in a file. - -Some commands can be piped to the script, for example: - echo "init password=xxxx" | {name} localhost 5000 - {name} localhost 5000 < commands.txt - -The script returns: - 0: OK - 2: wrong arguments (command line) - 3: connection error - 4: send error (message sent to WeeChat) - 5: decode error (message received from WeeChat) -'''.format(name=NAME)) - parser.add_argument('-6', '--ipv6', action='store_true', - help='connect using IPv6') - parser.add_argument('-d', '--debug', action='count', default=0, - help='debug mode: long objects view ' - '(-dd: display raw messages)') - parser.add_argument('-v', '--version', action='version', - version=qweechat_version) - parser.add_argument('hostname', - help='hostname (or IP address) of machine running ' - 'WeeChat/relay') - parser.add_argument('port', type=int, - help='port of machine running WeeChat/relay') - if len(sys.argv) == 1: - parser.print_help() - sys.exit(0) - _args = parser.parse_args( - shlex.split(os.getenv('QWEECHAT_PROTO_OPTIONS') or '') + sys.argv[1:]) - - test = TestProto(_args) - - # connect to WeeChat/relay - if not test.connect(): - sys.exit(3) - - # send commands from standard input if some data is available - if not test.send_stdin(): - sys.exit(4) - - # main loop (wait commands, display messages received) - returncode = test.mainloop() - del test - sys.exit(returncode) - - -if __name__ == "__main__": - main() diff --git a/toxygen/tox.py b/toxygen/tox.py new file mode 100644 index 0000000..862badd --- /dev/null +++ b/toxygen/tox.py @@ -0,0 +1,1512 @@ +# -*- coding: utf-8 -*- +from ctypes import c_char_p, Structure, c_bool, byref, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 +from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8 +from toxcore_enums_and_consts import * +from toxav import ToxAV +from libtox import LibToxCore + + +class ToxOptions(Structure): + _fields_ = [ + ('ipv6_enabled', c_bool), + ('udp_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), + ('savedata_type', c_int), + ('savedata_data', c_char_p), + ('savedata_length', c_size_t) + ] + + +def string_to_bin(tox_id): + return c_char_p(bytes.fromhex(tox_id)) if tox_id is not None else None + + +def bin_to_string(raw_id, length): + res = ''.join('{:02x}'.format(ord(raw_id[i])) for i in range(length)) + return res.upper() + + +class Tox: + + libtoxcore = LibToxCore() + + def __init__(self, tox_options=None, tox_pointer=None): + """ + Creates and initialises a new Tox instance with the options passed. + + This function will bring the instance into a valid state. Running the event loop with a new instance will + operate correctly. + + :param tox_options: An options object. If this parameter is None, the default options are used. + :param tox_pointer: Tox instance pointer. If this parameter is not None, tox_options will be ignored. + """ + if tox_pointer is not None: + 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)) + 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.') + elif tox_err_new == TOX_ERR_NEW['MALLOC']: + raise MemoryError('The function was unable to allocate enough ' + 'memory to store the internal structures for the Tox object.') + elif tox_err_new == TOX_ERR_NEW['PORT_ALLOC']: + raise RuntimeError('The function was unable to bind to a port. This may mean that all ports have ' + 'already been bound, e.g. by other Tox instances, or it may mean a permission error.' + ' You may be able to gather more information from errno.') + elif tox_err_new == TOX_ERR_NEW['PROXY_BAD_TYPE']: + raise ArgumentError('proxy_type was invalid.') + elif tox_err_new == TOX_ERR_NEW['PROXY_BAD_HOST']: + raise ArgumentError('proxy_type was valid but the proxy_host passed had an invalid format or was NULL.') + elif tox_err_new == TOX_ERR_NEW['PROXY_BAD_PORT']: + raise ArgumentError('proxy_type was valid, but the proxy_port was invalid.') + elif tox_err_new == TOX_ERR_NEW['PROXY_NOT_FOUND']: + raise ArgumentError('The proxy address passed could not be resolved.') + elif tox_err_new == TOX_ERR_NEW['LOAD_ENCRYPTED']: + raise ArgumentError('The byte array to be loaded contained an encrypted save.') + elif tox_err_new == TOX_ERR_NEW['LOAD_BAD_FORMAT']: + raise ArgumentError('The data format was invalid. This can happen when loading data that was saved by' + ' an older version of Tox, or when the data has been corrupted. When loading from' + ' badly formatted data, some data may have been loaded, and the rest is discarded.' + ' Passing an invalid length parameter also causes this error.') + + self.self_connection_status_cb = None + self.friend_name_cb = None + self.friend_status_message_cb = None + self.friend_status_cb = None + self.friend_connection_status_cb = None + self.friend_request_cb = None + self.friend_read_receipt_cb = None + self.friend_typing_cb = None + self.friend_message_cb = None + self.file_recv_control_cb = None + self.file_chunk_request_cb = None + self.file_recv_cb = None + self.file_recv_chunk_cb = None + self.friend_lossy_packet_cb = None + self.friend_lossless_packet_cb = None + + self.AV = ToxAV(self._tox_pointer) + + def __del__(self): + del self.AV + Tox.libtoxcore.tox_kill(self._tox_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # Startup options + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def options_default(tox_options): + """ + Initialises a Tox_Options object with the default options. + + The result of this function is independent of the original options. All values will be overwritten, no values + will be read (so it is permissible to pass an uninitialised object). + + If options is NULL, this function has no effect. + + :param tox_options: A pointer to options object to be filled with default options. + """ + Tox.libtoxcore.tox_options_default(tox_options) + + @staticmethod + def options_new(): + """ + Allocates a new Tox_Options object and initialises it with the default options. This function can be used to + preserve long term ABI compatibility by giving the responsibility of allocation and deallocation to the Tox + library. + + Objects returned from this function must be freed using the tox_options_free function. + + :return: A pointer to new ToxOptions object with default options or raise MemoryError. + """ + tox_err_options_new = c_int() + f = Tox.libtoxcore.tox_options_new + f.restype = POINTER(ToxOptions) + result = f(byref(tox_err_options_new)) + tox_err_options_new = tox_err_options_new.value + if tox_err_options_new == TOX_ERR_OPTIONS_NEW['OK']: + return result + elif tox_err_options_new == TOX_ERR_OPTIONS_NEW['MALLOC']: + raise MemoryError('The function failed to allocate enough memory for the options struct.') + + @staticmethod + def options_free(tox_options): + """ + Releases all resources associated with an options objects. + + Passing a pointer that was not returned by tox_options_new results in undefined behaviour. + + :param tox_options: A pointer to new ToxOptions object + """ + Tox.libtoxcore.tox_options_free(tox_options) + + # ----------------------------------------------------------------------------------------------------------------- + # Creation and destruction + # ----------------------------------------------------------------------------------------------------------------- + + def get_savedata_size(self): + """ + Calculates the number of bytes required to store the tox instance with tox_get_savedata. + This function cannot fail. The result is always greater than 0. + + :return: number of bytes + """ + return Tox.libtoxcore.tox_get_savedata_size(self._tox_pointer) + + def get_savedata(self, savedata=None): + """ + Store all information associated with the tox instance to a byte array. + + :param savedata: pointer (c_char_p) to a memory region large enough to store the tox instance data. + Call tox_get_savedata_size to find the number of bytes required. If this parameter is None, this function + allocates memory for the tox instance data. + :return: pointer (c_char_p) to a memory region with the tox instance data + """ + if savedata is None: + savedata_size = self.get_savedata_size() + savedata = create_string_buffer(savedata_size) + Tox.libtoxcore.tox_get_savedata(self._tox_pointer, savedata) + return savedata[:] + + # ----------------------------------------------------------------------------------------------------------------- + # Connection lifecycle and event loop + # ----------------------------------------------------------------------------------------------------------------- + + def bootstrap(self, address, port, public_key): + """ + Sends a "get nodes" request to the given bootstrap node with IP, port, and public key to setup connections. + + This function will attempt to connect to the node using UDP. You must use this function even if + Tox_Options.udp_enabled was set to false. + + :param address: The hostname or IP address (IPv4 or IPv6) of the node. + :param port: The port on the host on which the bootstrap Tox instance is listening. + :param public_key: The long term public key of the bootstrap node (TOX_PUBLIC_KEY_SIZE bytes). + :return: True on success. + """ + 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)) + tox_err_bootstrap = tox_err_bootstrap.value + if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['OK']: + return bool(result) + elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']: + raise ArgumentError('The address could not be resolved to an IP ' + 'address, or the IP address passed was invalid.') + elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']: + raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).') + + def add_tcp_relay(self, address, port, public_key): + """ + Adds additional host:port pair as TCP relay. + + This function can be used to initiate TCP connections to different ports on the same bootstrap node, or to add + TCP relays without using them as bootstrap nodes. + + :param address: The hostname or IP address (IPv4 or IPv6) of the TCP relay. + :param port: The port on the host on which the TCP relay is listening. + :param public_key: The long term public key of the TCP relay (TOX_PUBLIC_KEY_SIZE bytes). + :return: True on success. + """ + 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)) + tox_err_bootstrap = tox_err_bootstrap.value + if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['OK']: + return bool(result) + elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']: + raise ArgumentError('The address could not be resolved to an IP ' + 'address, or the IP address passed was invalid.') + elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']: + raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).') + + def self_get_connection_status(self): + """ + Return whether we are connected to the DHT. The return value is equal to the last value received through the + `self_connection_status` callback. + + :return: TOX_CONNECTION + """ + return Tox.libtoxcore.tox_self_get_connection_status(self._tox_pointer) + + def callback_self_connection_status(self, callback, user_data): + """ + Set the callback for the `self_connection_status` event. Pass None to unset. + + This event is triggered whenever there is a change in the DHT connection state. When disconnected, a client may + choose to call tox_bootstrap again, to reconnect to the DHT. Note that this state may frequently change for + short amounts of time. Clients should therefore not immediately bootstrap on receiving a disconnect. + + :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) + + def iteration_interval(self): + """ + Return the time in milliseconds before tox_iterate() should be called again for optimal performance. + :return: time in milliseconds + """ + return Tox.libtoxcore.tox_iteration_interval(self._tox_pointer) + + def iterate(self): + """ + The main loop that needs to be run in intervals of tox_iteration_interval() milliseconds. + """ + Tox.libtoxcore.tox_iterate(self._tox_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # Internal client information (Tox address/id) + # ----------------------------------------------------------------------------------------------------------------- + + def self_get_address(self, address=None): + """ + Writes the Tox friend address of the client to a byte array. The address is not in human-readable format. If a + client wants to display the address, formatting is required. + + :param address: pointer (c_char_p) to a memory region of at least TOX_ADDRESS_SIZE bytes. If this parameter is + None, this function allocates memory for address. + :return: Tox friend address + """ + if address is None: + address = create_string_buffer(TOX_ADDRESS_SIZE) + Tox.libtoxcore.tox_self_get_address(self._tox_pointer, address) + return bin_to_string(address, TOX_ADDRESS_SIZE) + + def self_set_nospam(self, nospam): + """ + Set the 4-byte nospam part of the address. + + :param nospam: Any 32 bit unsigned integer. + """ + Tox.libtoxcore.tox_self_set_nospam(self._tox_pointer, c_uint32(nospam)) + + def self_get_nospam(self): + """ + Get the 4-byte nospam part of the address. + + :return: nospam part of the address + """ + return Tox.libtoxcore.tox_self_get_nospam(self._tox_pointer) + + def self_get_public_key(self, public_key=None): + """ + Copy the Tox Public Key (long term) from the Tox object. + + :param public_key: A memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this parameter is NULL, this + function allocates memory for Tox Public Key. + :return: Tox Public Key + """ + if public_key is None: + public_key = create_string_buffer(TOX_PUBLIC_KEY_SIZE) + Tox.libtoxcore.tox_self_get_public_key(self._tox_pointer, public_key) + return bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE) + + def self_get_secret_key(self, secret_key=None): + """ + Copy the Tox Secret Key from the Tox object. + + :param secret_key: pointer (c_char_p) to a memory region of at least TOX_SECRET_KEY_SIZE bytes. If this + parameter is NULL, this function allocates memory for Tox Secret Key. + :return: Tox Secret Key + """ + if secret_key is None: + secret_key = create_string_buffer(TOX_SECRET_KEY_SIZE) + Tox.libtoxcore.tox_self_get_secret_key(self._tox_pointer, secret_key) + return bin_to_string(secret_key, TOX_SECRET_KEY_SIZE) + + # ----------------------------------------------------------------------------------------------------------------- + # User-visible client information (nickname/status) + # ----------------------------------------------------------------------------------------------------------------- + + def self_set_name(self, name): + """ + Set the nickname for the Tox client. + + Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is 0, the name parameter is ignored + (it can be None), and the nickname is set back to empty. + :param name: New nickname. + :return: True on success. + """ + tox_err_set_info = c_int() + 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 + if tox_err_set_info == TOX_ERR_SET_INFO['OK']: + return bool(result) + elif tox_err_set_info == TOX_ERR_SET_INFO['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_set_info == TOX_ERR_SET_INFO['TOO_LONG']: + raise ArgumentError('Information length exceeded maximum permissible size.') + + def self_get_name_size(self): + """ + Return the length of the current nickname as passed to tox_self_set_name. + + If no nickname was set before calling this function, the name is empty, and this function returns 0. + + :return: length of the current nickname + """ + return Tox.libtoxcore.tox_self_get_name_size(self._tox_pointer) + + def self_get_name(self, name=None): + """ + Write the nickname set by tox_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_self_get_name_size to find out how much memory to allocate for the result. + + :param name: pointer (c_char_p) to a memory region location large enough to hold the nickname. If this parameter + is NULL, the function allocates memory for the nickname. + :return: nickname + """ + if name is None: + name = create_string_buffer(self.self_get_name_size()) + Tox.libtoxcore.tox_self_get_name(self._tox_pointer, name) + return str(name.value, 'utf-8') + + def self_set_status_message(self, status_message): + """ + Set the client's status message. + + Status message length cannot exceed TOX_MAX_STATUS_MESSAGE_LENGTH. If length is 0, the status parameter is + ignored, and the user status is set back to empty. + + :param status_message: new status message + :return: True on success. + """ + tox_err_set_info = c_int() + 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 + if tox_err_set_info == TOX_ERR_SET_INFO['OK']: + return bool(result) + elif tox_err_set_info == TOX_ERR_SET_INFO['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_set_info == TOX_ERR_SET_INFO['TOO_LONG']: + raise ArgumentError('Information length exceeded maximum permissible size.') + + def self_get_status_message_size(self): + """ + Return the length of the current status message as passed to tox_self_set_status_message. + + If no status message was set before calling this function, the status is empty, and this function returns 0. + + :return: length of the current status message + """ + return Tox.libtoxcore.tox_self_get_status_message_size(self._tox_pointer) + + def self_get_status_message(self, status_message=None): + """ + Write the status message set by tox_self_set_status_message to a byte array. + + If no status message was set before calling this function, the status is empty, and this function has no effect. + + Call tox_self_get_status_message_size to find out how much memory to allocate for the result. + + :param status_message: pointer (c_char_p) to a valid memory location large enough to hold the status message. + If this parameter is None, the function allocates memory for the status message. + :return: status message + """ + if status_message is None: + status_message = create_string_buffer(self.self_get_status_message_size()) + Tox.libtoxcore.tox_self_get_status_message(self._tox_pointer, status_message) + return str(status_message.value, 'utf-8') + + def self_set_status(self, status): + """ + Set the client's user status. + + :param status: One of the user statuses listed in the enumeration TOX_USER_STATUS. + """ + Tox.libtoxcore.tox_self_set_status(self._tox_pointer, c_int(status)) + + def self_get_status(self): + """ + Returns the client's user status. + + :return: client's user status + """ + return Tox.libtoxcore.tox_self_get_status(self._tox_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # Friend list management + # ----------------------------------------------------------------------------------------------------------------- + + def friend_add(self, address, message): + """ + Add a friend to the friend list and send a friend request. + + A friend request message must be at least 1 byte long and at most TOX_MAX_FRIEND_REQUEST_LENGTH. + + Friend numbers are unique identifiers used in all functions that operate on friends. Once added, a friend number + is stable for the lifetime of the Tox object. After saving the state and reloading it, the friend numbers may + not be the same as before. Deleting a friend creates a gap in the friend number set, which is filled by the next + adding of a friend. Any pattern in friend numbers should not be relied on. + + If more than INT32_MAX friends are added, this function causes undefined behaviour. + + :param address: The address of the friend (returned by tox_self_get_address of the friend you wish to add) it + must be TOX_ADDRESS_SIZE bytes. + :param message: The message that will be sent along with the friend request. + :return: the friend number on success, UINT32_MAX on failure. + """ + tox_err_friend_add = c_int() + result = Tox.libtoxcore.tox_friend_add(self._tox_pointer, string_to_bin(address), c_char_p(message), + c_size_t(len(message)), byref(tox_err_friend_add)) + tox_err_friend_add = tox_err_friend_add.value + if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OK']: + return result + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['TOO_LONG']: + raise ArgumentError('The length of the friend request message exceeded TOX_MAX_FRIEND_REQUEST_LENGTH.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NO_MESSAGE']: + raise ArgumentError('The friend request message was empty. This, and the TOO_LONG code will never be' + ' returned from tox_friend_add_norequest.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['OWN_KEY']: + raise ArgumentError('The friend address belongs to the sending client.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['ALREADY_SENT']: + raise ArgumentError('A friend request has already been sent, or the address belongs to a friend that is' + ' already on the friend list.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['BAD_CHECKSUM']: + raise ArgumentError('The friend address checksum failed.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['SET_NEW_NOSPAM']: + raise ArgumentError('The friend was already there, but the nospam value was different.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']: + raise MemoryError('A memory allocation failed when trying to increase the friend list size.') + + def friend_add_norequest(self, public_key): + """ + Add a friend without sending a friend request. + + This function is used to add a friend in response to a friend request. If the client receives a friend request, + it can be reasonably sure that the other client added this client as a friend, eliminating the need for a friend + request. + + This function is also useful in a situation where both instances are controlled by the same entity, so that this + entity can perform the mutual friend adding. In this case, there is no need for a friend request, either. + + :param public_key: A byte array of length TOX_PUBLIC_KEY_SIZE containing the Public Key (not the Address) of the + friend to add. + :return: the friend number on success, UINT32_MAX on failure. + """ + tox_err_friend_add = c_int() + result = Tox.libtoxcore.tox_friend_add_norequest(self._tox_pointer, string_to_bin(public_key), + byref(tox_err_friend_add)) + tox_err_friend_add = tox_err_friend_add.value + if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OK']: + return result + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['TOO_LONG']: + raise ArgumentError('The length of the friend request message exceeded TOX_MAX_FRIEND_REQUEST_LENGTH.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NO_MESSAGE']: + raise ArgumentError('The friend request message was empty. This, and the TOO_LONG code will never be' + ' returned from tox_friend_add_norequest.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['OWN_KEY']: + raise ArgumentError('The friend address belongs to the sending client.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['ALREADY_SENT']: + raise ArgumentError('A friend request has already been sent, or the address belongs to a friend that is' + ' already on the friend list.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['BAD_CHECKSUM']: + raise ArgumentError('The friend address checksum failed.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['SET_NEW_NOSPAM']: + raise ArgumentError('The friend was already there, but the nospam value was different.') + elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']: + raise MemoryError('A memory allocation failed when trying to increase the friend list size.') + + def friend_delete(self, friend_number): + """ + Remove a friend from the friend list. + + This does not notify the friend of their deletion. After calling this function, this client will appear offline + to the friend and no communication can occur between the two. + + :param friend_number: Friend number for the friend to be deleted. + :return: True on success. + """ + tox_err_friend_delete = c_int() + result = Tox.libtoxcore.tox_friend_delete(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_friend_delete)) + tox_err_friend_delete = tox_err_friend_delete.value + if tox_err_friend_delete == TOX_ERR_FRIEND_DELETE['OK']: + return bool(result) + elif tox_err_friend_delete == TOX_ERR_FRIEND_DELETE['FRIEND_NOT_FOUND']: + raise ArgumentError('There was no friend with the given friend number. No friends were deleted.') + + # ----------------------------------------------------------------------------------------------------------------- + # Friend list queries + # ----------------------------------------------------------------------------------------------------------------- + + def friend_by_public_key(self, public_key): + """ + Return the friend number associated with that Public Key. + + :param public_key: A byte array containing the Public Key. + :return: friend number + """ + tox_err_friend_by_public_key = c_int() + result = Tox.libtoxcore.tox_friend_by_public_key(self._tox_pointer, string_to_bin(public_key), + byref(tox_err_friend_by_public_key)) + tox_err_friend_by_public_key = tox_err_friend_by_public_key.value + if tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['OK']: + return result + elif tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NOT_FOUND']: + raise ArgumentError('No friend with the given Public Key exists on the friend list.') + + def friend_exists(self, friend_number): + """ + Checks if a friend with the given friend number exists and returns true if it does. + """ + return bool(Tox.libtoxcore.tox_friend_exists(self._tox_pointer, c_uint32(friend_number))) + + def self_get_friend_list_size(self): + """ + Return the number of friends on the friend list. + + This function can be used to determine how much memory to allocate for tox_self_get_friend_list. + + :return: number of friends + """ + return Tox.libtoxcore.tox_self_get_friend_list_size(self._tox_pointer) + + def self_get_friend_list(self, friend_list=None): + """ + Copy a list of valid friend numbers into an array. + + Call tox_self_get_friend_list_size to determine the number of elements to allocate. + + :param friend_list: pointer (c_char_p) to a memory region with enough space to hold the friend list. If this + parameter is None, this function allocates memory for the friend list. + :return: friend list + """ + friend_list_size = self.self_get_friend_list_size() + if friend_list is None: + friend_list = create_string_buffer(sizeof(c_uint32) * friend_list_size) + friend_list = POINTER(c_uint32)(friend_list) + Tox.libtoxcore.tox_self_get_friend_list(self._tox_pointer, friend_list) + return friend_list[0:friend_list_size] + + def friend_get_public_key(self, friend_number, public_key=None): + """ + Copies the Public Key associated with a given friend number to a byte array. + + :param friend_number: The friend number you want the Public Key of. + :param public_key: pointer (c_char_p) to a memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this + parameter is None, this function allocates memory for Tox Public Key. + :return: Tox Public Key + """ + if public_key is None: + public_key = create_string_buffer(TOX_PUBLIC_KEY_SIZE) + tox_err_friend_get_public_key = c_int() + Tox.libtoxcore.tox_friend_get_public_key(self._tox_pointer, c_uint32(friend_number), public_key, + byref(tox_err_friend_get_public_key)) + tox_err_friend_get_public_key = tox_err_friend_get_public_key.value + if tox_err_friend_get_public_key == TOX_ERR_FRIEND_GET_PUBLIC_KEY['OK']: + return bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE) + elif tox_err_friend_get_public_key == TOX_ERR_FRIEND_GET_PUBLIC_KEY['FRIEND_NOT_FOUND']: + raise ArgumentError('No friend with the given number exists on the friend list.') + + def friend_get_last_online(self, friend_number): + """ + Return a unix-time timestamp of the last time the friend associated with a given friend number was seen online. + This function will return UINT64_MAX on error. + + :param friend_number: The friend number you want to query. + :return: unix-time timestamp + """ + tox_err_last_online = c_int() + result = Tox.libtoxcore.tox_friend_get_last_online(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_last_online)) + tox_err_last_online = tox_err_last_online.value + if tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['OK']: + return result + elif tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['FRIEND_NOT_FOUND']: + raise ArgumentError('No friend with the given number exists on the friend list.') + + # ----------------------------------------------------------------------------------------------------------------- + # Friend-specific state queries (can also be received through callbacks) + # ----------------------------------------------------------------------------------------------------------------- + + def friend_get_name_size(self, friend_number): + """ + Return the length of the friend's name. If the friend number is invalid, the return value is unspecified. + + The return value is equal to the `length` argument received by the last `friend_name` callback. + """ + tox_err_friend_query = c_int() + result = Tox.libtoxcore.tox_friend_get_name_size(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return result + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number did not designate a valid friend.') + + def friend_get_name(self, friend_number, name=None): + """ + Write the name of the friend designated by the given friend number to a byte array. + + Call tox_friend_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 `friend_name` callback. + + :param friend_number: number of friend + :param name: pointer (c_char_p) to a valid memory region large enough to store the friend's name. + :return: name of the friend + """ + if name is None: + name = create_string_buffer(self.friend_get_name_size(friend_number)) + tox_err_friend_query = c_int() + Tox.libtoxcore.tox_friend_get_name(self._tox_pointer, c_uint32(friend_number), name, + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return str(name.value, 'utf-8') + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + 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): + """ + Set the callback for the `friend_name` event. Pass None to unset. + + This event is triggered when a friend changes their name. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend whose name changed, + 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) + + def friend_get_status_message_size(self, friend_number): + """ + Return the length of the friend's status message. If the friend number is invalid, the return value is SIZE_MAX. + + :return: length of the friend's status message + """ + tox_err_friend_query = c_int() + result = Tox.libtoxcore.tox_friend_get_status_message_size(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return result + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number did not designate a valid friend.') + + def friend_get_status_message(self, friend_number, status_message=None): + """ + Write the status message of the friend designated by the given friend number to a byte array. + + Call tox_friend_get_status_message_size to determine the allocation size for the `status_name` parameter. + + The data written to `status_message` is equal to the data received by the last `friend_status_message` callback. + + :param friend_number: + :param status_message: pointer (c_char_p) to a valid memory region large enough to store the friend's status + message. + :return: status message of the friend + """ + if status_message is None: + status_message = create_string_buffer(self.friend_get_status_message_size(friend_number)) + tox_err_friend_query = c_int() + Tox.libtoxcore.tox_friend_get_status_message(self._tox_pointer, c_uint32(friend_number), status_message, + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return str(status_message.value, 'utf-8') + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + 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): + """ + Set the callback for the `friend_status_message` event. Pass NULL to unset. + + This event is triggered when a friend changes their status message. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend whose status message changed, + A byte array (c_char_p) containing the same data as tox_friend_get_status_message would write to its + `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)) + + def friend_get_status(self, friend_number): + """ + Return the friend's user status (away/busy/...). If the friend number is invalid, the return value is + unspecified. + + The status returned is equal to the last status received through the `friend_status` callback. + + :return: TOX_USER_STATUS + """ + tox_err_friend_query = c_int() + result = Tox.libtoxcore.tox_friend_get_status(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return result + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + 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): + """ + Set the callback for the `friend_status` event. Pass None to unset. + + This event is triggered when a friend changes their user status. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend whose user status changed, + The new user status (TOX_USER_STATUS), + 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_status_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_friend_status(self._tox_pointer, self.friend_status_cb, c_void_p(user_data)) + + def friend_get_connection_status(self, friend_number): + """ + Check whether a friend is currently connected to this client. + + The result of this function is equal to the last value received by the `friend_connection_status` callback. + + :param friend_number: The friend number for which to query the connection status. + :return: the friend's connection status (TOX_CONNECTION) as it was received through the + `friend_connection_status` event. + """ + tox_err_friend_query = c_int() + result = Tox.libtoxcore.tox_friend_get_connection_status(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return result + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + 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): + """ + Set the callback for the `friend_connection_status` event. Pass NULL to unset. + + This event is triggered when a friend goes offline after having been online, or when a friend goes online. + + This callback is not called when adding friends. It is assumed that when adding friends, their connection status + is initially offline. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + 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)) + + def friend_get_typing(self, friend_number): + """ + Check whether a friend is currently typing a message. + + :param friend_number: The friend number for which to query the typing status. + :return: true if the friend is typing. + """ + tox_err_friend_query = c_int() + result = Tox.libtoxcore.tox_friend_get_typing(self._tox_pointer, c_uint32(friend_number), + byref(tox_err_friend_query)) + tox_err_friend_query = tox_err_friend_query.value + if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: + return bool(result) + elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: + raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' + ' the `_self_` variants of these functions, which have no effect when a parameter is' + ' NULL, these functions return an error in that case.') + 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): + """ + Set the callback for the `friend_typing` event. Pass NULL to unset. + + This event is triggered when a friend starts or stops typing. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + 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)) + + # ----------------------------------------------------------------------------------------------------------------- + # Sending private messages + # ----------------------------------------------------------------------------------------------------------------- + + def self_set_typing(self, friend_number, typing): + """ + Set the client's typing status for a friend. + + The client is responsible for turning it on or off. + + :param friend_number: The friend to which the client is typing a message. + :param typing: The typing status. True means the client is typing. + :return: True on success. + """ + tox_err_set_typing = c_int() + result = Tox.libtoxcore.tox_self_set_typing(self._tox_pointer, c_uint32(friend_number), + c_bool(typing), byref(tox_err_set_typing)) + tox_err_set_typing = tox_err_set_typing.value + if tox_err_set_typing == TOX_ERR_SET_TYPING['OK']: + return bool(result) + elif tox_err_set_typing == TOX_ERR_SET_TYPING['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + + def friend_send_message(self, friend_number, message_type, message): + """ + Send a text chat message to an online friend. + + This function creates a chat 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. + + The return value of this function is the message ID. If a read receipt is received, the triggered + `friend_read_receipt` event will be passed this message ID. + + Message IDs are unique per friend. The first message ID is 0. Message IDs are incremented by 1 each time a + message is sent. If UINT32_MAX messages were sent, the next message ID is 0. + + :param friend_number: The friend number of the friend to send the message to. + :param message_type: Message type (TOX_MESSAGE_TYPE). + :param message: A non-None message text. + :return: message ID + """ + tox_err_friend_send_message = c_int() + result = Tox.libtoxcore.tox_friend_send_message(self._tox_pointer, c_uint32(friend_number), + c_int(message_type), c_char_p(message), c_size_t(len(message)), + byref(tox_err_friend_send_message)) + tox_err_friend_send_message = tox_err_friend_send_message.value + if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['OK']: + return result + elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['SENDQ']: + raise MemoryError('An allocation error occurred while increasing the send queue size.') + elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['TOO_LONG']: + raise ArgumentError('Message length exceeded TOX_MAX_MESSAGE_LENGTH.') + 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): + """ + Set the callback for the `friend_read_receipt` event. Pass None to unset. + + This event is triggered when the friend receives the message sent with tox_friend_send_message with the + corresponding message ID. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend who received the message, + The message ID (c_uint32) as returned from tox_friend_send_message corresponding to the message sent, + 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_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)) + + # ----------------------------------------------------------------------------------------------------------------- + # Receiving private messages and friend requests + # ----------------------------------------------------------------------------------------------------------------- + + def callback_friend_request(self, callback, user_data): + """ + Set the callback for the `friend_request` event. Pass None to unset. + + This event is triggered when a friend request is received. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + The Public Key (c_uint8 array) of the user who sent the friend request, + The message (c_char_p) they sent along with the request, + 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, 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)) + + def callback_friend_message(self, callback, user_data): + """ + Set the callback for the `friend_message` event. Pass None to unset. + + This event is triggered when a message from a friend is received. + + :param callback: Python function. Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend who sent the message, + Message type (TOX_MESSAGE_TYPE), + 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)) + + # ----------------------------------------------------------------------------------------------------------------- + # File transmission: common between sending and receiving + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def hash(data, hash=None): + """ + Generates a cryptographic hash of the given data. + + This function may be used by clients for any purpose, but is provided primarily for validating cached avatars. + This use is highly recommended to avoid unnecessary avatar updates. + + If hash is NULL or data is NULL while length is not 0 the function returns false, otherwise it returns true. + + This function is a wrapper to internal message-digest functions. + + :param hash: A valid memory location the hash data. It must be at least TOX_HASH_LENGTH bytes in size. + :param data: Data to be hashed or NULL. + :return: true if hash was not NULL. + """ + if hash is None: + hash = create_string_buffer(TOX_HASH_LENGTH) + Tox.libtoxcore.tox_hash(hash, c_char_p(data), len(data)) + return bin_to_string(hash, TOX_HASH_LENGTH) + + def file_control(self, friend_number, file_number, control): + """ + Sends a file control command to a friend for a given file transfer. + + :param friend_number: The friend number of the friend the file is being transferred to or received from. + :param file_number: The friend-specific identifier for the file transfer. + :param control: The control (TOX_FILE_CONTROL) command to send. + :return: True on success. + """ + tox_err_file_control = c_int() + result = Tox.libtoxcore.tox_file_control(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), + c_int(control), byref(tox_err_file_control)) + tox_err_file_control = tox_err_file_control.value + if tox_err_file_control == TOX_ERR_FILE_CONTROL['OK']: + return bool(result) + elif tox_err_file_control == TOX_ERR_FILE_CONTROL['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif tox_err_file_control == TOX_ERR_FILE_CONTROL['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_FOUND']: + raise ArgumentError('No file transfer with the given file number was found for the given friend.') + elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_PAUSED']: + raise RuntimeError('A RESUME control was sent, but the file transfer is running normally.') + elif tox_err_file_control == TOX_ERR_FILE_CONTROL['DENIED']: + raise RuntimeError('A RESUME control was sent, but the file transfer was paused by the other party. Only ' + 'the party that paused the transfer can resume it.') + elif tox_err_file_control == TOX_ERR_FILE_CONTROL['ALREADY_PAUSED']: + raise RuntimeError('A PAUSE control was sent, but the file transfer was already paused.') + 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): + """ + Set the callback for the `file_recv_control` event. Pass NULL to unset. + + This event is triggered when a file control command is received from a friend. + + :param callback: Python function. + When receiving TOX_FILE_CONTROL_CANCEL, the client should release the resources associated with the file number + and consider the transfer failed. + + Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend who is sending the file. + 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) + + def file_seek(self, friend_number, file_number, position): + """ + Sends a file seek control command to a friend for a given file transfer. + + This function can only be called to resume a file transfer right before TOX_FILE_CONTROL_RESUME is sent. + + :param friend_number: The friend number of the friend the file is being received from. + :param file_number: The friend-specific identifier for the file transfer. + :param position: The position that the file should be seeked to. + :return: True on success. + """ + tox_err_file_seek = c_int() + result = Tox.libtoxcore.tox_file_control(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), + c_uint64(position), byref(tox_err_file_seek)) + tox_err_file_seek = tox_err_file_seek.value + if tox_err_file_seek == TOX_ERR_FILE_SEEK['OK']: + return bool(result) + elif tox_err_file_seek == TOX_ERR_FILE_SEEK['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif tox_err_file_seek == TOX_ERR_FILE_SEEK['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_file_seek == TOX_ERR_FILE_SEEK['NOT_FOUND']: + raise ArgumentError('No file transfer with the given file number was found for the given friend.') + elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SEEK_DENIED']: + raise IOError('File was not in a state where it could be seeked.') + elif tox_err_file_seek == TOX_ERR_FILE_SEEK['INVALID_POSITION']: + raise ArgumentError('Seek position was invalid') + elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SENDQ']: + raise RuntimeError('Packet queue is full.') + + def file_get_file_id(self, friend_number, file_number, file_id=None): + """ + Copy the file id associated to the file transfer to a byte array. + + :param friend_number: The friend number of the friend the file is being transferred to or received from. + :param file_number: The friend-specific identifier for the file transfer. + :param file_id: A pointer (c_char_p) to memory region of at least TOX_FILE_ID_LENGTH bytes. If this parameter is + None, this function has no effect. + :return: file id. + """ + if file_id is None: + file_id = create_string_buffer(TOX_FILE_ID_LENGTH) + tox_err_file_get = c_int() + Tox.libtoxcore.tox_file_get_file_id(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), file_id, + byref(tox_err_file_get)) + tox_err_file_get = tox_err_file_get.value + if tox_err_file_get == TOX_ERR_FILE_GET['OK']: + return bin_to_string(file_id, TOX_FILE_ID_LENGTH) + elif tox_err_file_get == TOX_ERR_FILE_GET['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_file_get == TOX_ERR_FILE_GET['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif tox_err_file_get == TOX_ERR_FILE_GET['NOT_FOUND']: + raise ArgumentError('No file transfer with the given file number was found for the given friend.') + + # ----------------------------------------------------------------------------------------------------------------- + # File transmission: sending + # ----------------------------------------------------------------------------------------------------------------- + + def file_send(self, friend_number, kind, file_size, file_id, filename): + """ + Send a file transmission request. + + Maximum filename length is TOX_MAX_FILENAME_LENGTH bytes. The filename should generally just be a file name, not + a path with directory names. + + If a non-UINT64_MAX file size is provided, it can be used by both sides to determine the sending progress. File + size can be set to UINT64_MAX for streaming data of unknown size. + + File transmission occurs in chunks, which are requested through the `file_chunk_request` event. + + When a friend goes offline, all file transfers associated with the friend are purged from core. + + If the file contents change during a transfer, the behaviour is unspecified in general. What will actually + happen depends on the mode in which the file was modified and how the client determines the file size. + + - If the file size was increased + - and sending mode was streaming (file_size = UINT64_MAX), the behaviour will be as expected. + - and sending mode was file (file_size != UINT64_MAX), the file_chunk_request callback will receive length = + 0 when Core thinks the file transfer has finished. If the client remembers the file size as it was when + sending the request, it will terminate the transfer normally. If the client re-reads the size, it will think + the friend cancelled the transfer. + - If the file size was decreased + - and sending mode was streaming, the behaviour is as expected. + - and sending mode was file, the callback will return 0 at the new (earlier) end-of-file, signalling to the + friend that the transfer was cancelled. + - If the file contents were modified + - at a position before the current read, the two files (local and remote) will differ after the transfer + terminates. + - at a position after the current read, the file transfer will succeed as expected. + - In either case, both sides will regard the transfer as complete and successful. + + :param friend_number: The friend number of the friend the file send request should be sent to. + :param kind: The meaning of the file to be sent. + :param file_size: Size in bytes of the file the client wants to send, UINT64_MAX if unknown or streaming. + :param file_id: A file identifier of length TOX_FILE_ID_LENGTH that can be used to uniquely identify file + transfers across core restarts. If NULL, a random one will be generated by core. It can then be obtained by + using tox_file_get_file_id(). + :param filename: Name of the file. Does not need to be the actual name. This name will be sent along with the + file send request. + :return: A file number used as an identifier in subsequent callbacks. This number is per friend. File numbers + are reused after a transfer terminates. On failure, this function returns UINT32_MAX. Any pattern in file + numbers should not be relied on. + """ + tox_err_file_send = c_int() + result = self.libtoxcore.tox_file_send(self._tox_pointer, c_uint32(friend_number), c_uint32(kind), + c_uint64(file_size), + string_to_bin(file_id), + c_char_p(filename), + c_size_t(len(filename)), byref(tox_err_file_send)) + tox_err_file_send = tox_err_file_send.value + if tox_err_file_send == TOX_ERR_FILE_SEND['OK']: + return result + elif tox_err_file_send == TOX_ERR_FILE_SEND['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_file_send == TOX_ERR_FILE_SEND['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif tox_err_file_send == TOX_ERR_FILE_SEND['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_file_send == TOX_ERR_FILE_SEND['NAME_TOO_LONG']: + raise ArgumentError('Filename length exceeded TOX_MAX_FILENAME_LENGTH bytes.') + elif tox_err_file_send == TOX_ERR_FILE_SEND['TOO_MANY']: + raise RuntimeError('Too many ongoing transfers. The maximum number of concurrent file transfers is 256 per' + 'friend per direction (sending and receiving).') + + def file_send_chunk(self, friend_number, file_number, position, data): + """ + Send a chunk of file data to a friend. + + This function is called in response to the `file_chunk_request` callback. The length parameter should be equal + to the one received though the callback. If it is zero, the transfer is assumed complete. For files with known + size, Core will know that the transfer is complete after the last byte has been received, so it is not necessary + (though not harmful) to send a zero-length chunk to terminate. For streams, core will know that the transfer is + finished if a chunk with length less than the length requested in the callback is sent. + + :param friend_number: The friend number of the receiving friend for this file. + :param file_number: The file transfer identifier returned by tox_file_send. + :param position: The file or stream position from which to continue reading. + :param data: Chunk of file data + :return: true on success. + """ + tox_err_file_send_chunk = c_int() + result = self.libtoxcore.tox_file_send_chunk(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), + c_uint64(position), c_char_p(data), c_size_t(len(data)), + byref(tox_err_file_send_chunk)) + tox_err_file_send_chunk = tox_err_file_send_chunk.value + if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['OK']: + return bool(result) + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NULL']: + raise ArgumentError('The length parameter was non-zero, but data was NULL.') + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['FRIEND_NOT_FOUND']: + ArgumentError('The friend_number passed did not designate a valid friend.') + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NOT_FOUND']: + raise ArgumentError('No file transfer with the given file number was found for the given friend.') + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NOT_TRANSFERRING']: + raise ArgumentError('File transfer was found but isn\'t in a transferring state: (paused, done, broken, ' + 'etc...) (happens only when not called from the request chunk callback).') + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['INVALID_LENGTH']: + raise ArgumentError('Attempted to send more or less data than requested. The requested data size is ' + 'adjusted according to maximum transmission unit and the expected end of the file. ' + 'Trying to send less or more than requested will return this error.') + elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['SENDQ']: + raise RuntimeError('Packet queue is full.') + 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): + """ + Set the callback for the `file_chunk_request` event. Pass None to unset. + + This event is triggered when Core is ready to send more file data. + + :param callback: Python function. + If the length parameter is 0, the file transfer is finished, and the client's resources associated with the file + number should be released. After a call with zero length, the file number can be reused for future file + transfers. + + If the requested position is not equal to the client's idea of the current file or stream position, it will need + to seek. In case of read-once streams, the client should keep the last read chunk so that a seek back can be + supported. A seek-back only ever needs to read from the last requested chunk. This happens when a chunk was + requested, but the send failed. A seek-back request can occur an arbitrary number of times for any given chunk. + + In response to receiving this callback, the client should call the function `tox_file_send_chunk` with the + requested chunk. If the number of bytes sent through that function is zero, the file transfer is assumed + complete. A client must send the full length of data requested with this callback. + + Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the receiving friend for this file. + The file transfer identifier (c_uint32) returned by tox_file_send. + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # File transmission: receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_file_recv(self, callback, user_data): + """ + Set the callback for the `file_recv` event. Pass None to unset. + + This event is triggered when a file transfer request is received. + + :param callback: Python function. + The client should acquire resources to be associated with the file transfer. Incoming file transfers start in + the PAUSED state. After this callback returns, a transfer can be rejected by sending a TOX_FILE_CONTROL_CANCEL + control command before any other control commands. It can be accepted by sending TOX_FILE_CONTROL_RESUME. + + Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend who is sending the file transfer request. + The friend-specific file number (c_uint32) the data received is associated with. + The meaning of the file (c_uint32) to be sent. + Size in bytes (c_uint64) of the file the client wants to send, UINT64_MAX if unknown or streaming. + Name of the file (c_char_p). Does not need to be the actual name. This name will be sent along with the file + 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) + + def callback_file_recv_chunk(self, callback, user_data): + """ + Set the callback for the `file_recv_chunk` event. Pass NULL to unset. + + This event is first triggered when a file transfer request is received, and subsequently when a chunk of file + data for an accepted request was received. + + :param callback: Python function. + When length is 0, the transfer is finished and the client should release the resources it acquired for the + transfer. After a call with length = 0, the file number can be reused for new file transfers. + + If position is equal to file_size (received in the file_receive callback) when the transfer finishes, the file + was received completely. Otherwise, if file_size was UINT64_MAX, streaming ended successfully when length is 0. + + Should take pointer (c_void_p) to Tox object, + The friend number (c_uint32) of the friend who is sending the file. + The friend-specific file number (c_uint32) the data received is associated with. + The file position (c_uint64) of the first byte in data. + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # Low-level custom packet sending and receiving + # ----------------------------------------------------------------------------------------------------------------- + + def friend_send_lossy_packet(self, friend_number, data): + """ + Send a custom lossy packet to a friend. + The first byte of data must be in the range 200-254. Maximum length of a + custom packet is TOX_MAX_CUSTOM_PACKET_SIZE. + + 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, it is recommended that you use lossless custom packets instead. + + :param friend_number: The friend number of the friend this lossy packet + :param data: python string containing the packet data + :return: True on success. + """ + tox_err_friend_custom_packet = c_int() + result = self.libtoxcore.tox_friend_send_lossy_packet(self._tox_pointer, c_uint32(friend_number), + c_char_p(data), c_size_t(len(data)), + byref(tox_err_friend_custom_packet)) + tox_err_friend_custom_packet = tox_err_friend_custom_packet.value + if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['OK']: + return bool(result) + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['INVALID']: + raise ArgumentError('The first byte of data was not in the specified range for the packet type.' + 'This range is 200-254 for lossy, and 160-191 for lossless packets.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['EMPTY']: + raise ArgumentError('Attempted to send an empty packet.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']: + raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']: + raise RuntimeError('Packet queue is full.') + + def friend_send_lossless_packet(self, friend_number, data): + """ + Send a custom lossless packet to a friend. + The first byte of data must be in the range 160-191. Maximum length of a + custom packet is TOX_MAX_CUSTOM_PACKET_SIZE. + + Lossless packet behaviour is comparable to TCP (reliability, arrive in order) + but with packets instead of a stream. + + :param friend_number: The friend number of the friend this lossless packet + :param data: python string containing the packet data + :return: True on success. + """ + tox_err_friend_custom_packet = c_int() + result = self.libtoxcore.tox_friend_send_lossless_packet(self._tox_pointer, c_uint32(friend_number), + c_char_p(data), c_size_t(len(data)), + byref(tox_err_friend_custom_packet)) + tox_err_friend_custom_packet = tox_err_friend_custom_packet.value + if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['OK']: + return bool(result) + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_CONNECTED']: + raise ArgumentError('This client is currently not connected to the friend.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['INVALID']: + raise ArgumentError('The first byte of data was not in the specified range for the packet type.' + 'This range is 200-254 for lossy, and 160-191 for lossless packets.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['EMPTY']: + raise ArgumentError('Attempted to send an empty packet.') + elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']: + raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.') + 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): + """ + Set the callback for the `friend_lossy_packet` event. Pass NULL to unset. + + :param callback: Python function. + Should take pointer (c_void_p) to Tox object, + friend_number (c_uint32) - The friend number of the friend who sent a lossy packet, + 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) + + def callback_friend_lossless_packet(self, callback, user_data): + """ + Set the callback for the `friend_lossless_packet` event. Pass NULL to unset. + + :param callback: Python function. + Should take pointer (c_void_p) to Tox object, + friend_number (c_uint32) - The friend number of the friend who sent a lossless packet, + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # Low-level network information + # ----------------------------------------------------------------------------------------------------------------- + + def self_get_dht_id(self, dht_id=None): + """ + Writes the temporary DHT public key of this instance to a byte array. + + This can be used in combination with an externally accessible IP address and the bound port (from + tox_self_get_udp_port) to run a temporary bootstrap node. + + Be aware that every time a new instance is created, the DHT public key changes, meaning this cannot be used to + run a permanent bootstrap node. + + :param dht_id: pointer (c_char_p) to a memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this parameter is + None, this function allocates memory for dht_id. + :return: dht_id + """ + if dht_id is None: + dht_id = create_string_buffer(TOX_PUBLIC_KEY_SIZE) + Tox.libtoxcore.tox_self_get_dht_id(self._tox_pointer, dht_id) + return bin_to_string(dht_id, TOX_PUBLIC_KEY_SIZE) + + def self_get_udp_port(self): + """ + Return the UDP port this Tox instance is bound to. + """ + tox_err_get_port = c_int() + result = Tox.libtoxcore.tox_self_get_udp_port(self._tox_pointer, byref(tox_err_get_port)) + tox_err_get_port = tox_err_get_port.value + if tox_err_get_port == TOX_ERR_GET_PORT['OK']: + return result + elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: + raise RuntimeError('The instance was not bound to any port.') + + def self_get_tcp_port(self): + """ + Return the TCP port this Tox instance is bound to. This is only relevant if the instance is acting as a TCP + relay. + """ + tox_err_get_port = c_int() + result = Tox.libtoxcore.tox_self_get_tcp_port(self._tox_pointer, byref(tox_err_get_port)) + tox_err_get_port = tox_err_get_port.value + if tox_err_get_port == TOX_ERR_GET_PORT['OK']: + return result + elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: + raise RuntimeError('The instance was not bound to any port.') diff --git a/toxygen/tox_dns.py b/toxygen/tox_dns.py new file mode 100644 index 0000000..ec8582f --- /dev/null +++ b/toxygen/tox_dns.py @@ -0,0 +1,61 @@ +import json +import urllib.request +from util import log +import settings +try: + from PySide import QtNetwork, QtCore +except: + from PyQt4 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(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/toxav.py b/toxygen/toxav.py new file mode 100644 index 0000000..0ab891c --- /dev/null +++ b/toxygen/toxav.py @@ -0,0 +1,362 @@ +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 * + + +class ToxAV: + """ + The ToxAV instance type. Each ToxAV instance can be bound to only one Tox instance, and Tox instance can have only + one ToxAV instance. One must make sure to close ToxAV instance prior closing Tox instance otherwise undefined + behaviour occurs. Upon closing of ToxAV instance, all active calls will be forcibly terminated without notifying + peers. + """ + + # ----------------------------------------------------------------------------------------------------------------- + # Creation and destruction + # ----------------------------------------------------------------------------------------------------------------- + + def __init__(self, tox_pointer): + """ + Start new A/V session. There can only be only one session per Tox instance. + + :param tox_pointer: pointer to Tox instance + """ + 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)) + 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.') + elif toxav_err_new == TOXAV_ERR_NEW['MALLOC']: + raise MemoryError('Memory allocation failure while trying to allocate structures required for the A/V ' + 'session.') + elif toxav_err_new == TOXAV_ERR_NEW['MULTIPLE']: + raise RuntimeError('Attempted to create a second session for the same Tox instance.') + + self.call_state_cb = None + self.audio_receive_frame_cb = None + self.video_receive_frame_cb = None + self.call_cb = None + + def __del__(self): + """ + Releases all resources associated with the A/V session. + + If any calls were ongoing, these will be forcibly terminated without notifying peers. After calling this + function, no other functions may be called and the av pointer becomes invalid. + """ + self.libtoxav.toxav_kill(self._toxav_pointer) + + def get_tox_pointer(self): + """ + Returns the Tox instance the A/V object was created for. + + :return: pointer to the Tox instance + """ + self.libtoxav.toxav_get_tox.restype = POINTER(c_void_p) + return self.libtoxav.toxav_get_tox(self._toxav_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # A/V event loop + # ----------------------------------------------------------------------------------------------------------------- + + def iteration_interval(self): + """ + Returns the interval in milliseconds when the next toxav_iterate call should be. If no call is active at the + moment, this function returns 200. + + :return: interval in milliseconds + """ + return self.libtoxav.toxav_iteration_interval(self._toxav_pointer) + + def iterate(self): + """ + Main loop for the session. This function needs to be called in intervals of toxav_iteration_interval() + milliseconds. It is best called in the separate thread from tox_iterate. + """ + self.libtoxav.toxav_iterate(self._toxav_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # Call setup + # ----------------------------------------------------------------------------------------------------------------- + + def call(self, friend_number, audio_bit_rate, video_bit_rate): + """ + Call a friend. This will start ringing the friend. + + It is the client's responsibility to stop ringing after a certain timeout, if such behaviour is desired. If the + client does not stop ringing, the library will not stop until the friend is disconnected. Audio and video + receiving are both enabled by default. + + :param friend_number: The friend number of the friend that should be called. + :param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + :param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + :return: True on success. + """ + toxav_err_call = c_int() + result = self.libtoxav.toxav_call(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate), + c_uint32(video_bit_rate), byref(toxav_err_call)) + toxav_err_call = toxav_err_call.value + if toxav_err_call == TOXAV_ERR_CALL['OK']: + return bool(result) + elif toxav_err_call == TOXAV_ERR_CALL['MALLOC']: + raise MemoryError('A resource allocation error occurred while trying to create the structures required for ' + 'the call.') + elif toxav_err_call == TOXAV_ERR_CALL['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_NOT_CONNECTED']: + raise ArgumentError('The friend was valid, but not currently connected.') + elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_ALREADY_IN_CALL']: + raise ArgumentError('Attempted to call a friend while already in an audio or video call with them.') + elif toxav_err_call == TOXAV_ERR_CALL['INVALID_BIT_RATE']: + raise ArgumentError('Audio or video bit rate is invalid.') + + def callback_call(self, callback, user_data): + """ + Set the callback for the `call` event. Pass None to unset. + + :param callback: The function for the call callback. + + Should take pointer (c_void_p) to ToxAV object, + The friend number (c_uint32) from which the call is incoming. + True (c_bool) if friend is sending audio. + True (c_bool) if friend is sending video. + 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_bool, c_void_p) + self.call_cb = c_callback(callback) + self.libtoxav.toxav_callback_call(self._toxav_pointer, self.call_cb, user_data) + + def answer(self, friend_number, audio_bit_rate, video_bit_rate): + """ + Accept an incoming call. + + If answering fails for any reason, the call will still be pending and it is possible to try and answer it later. + Audio and video receiving are both enabled by default. + + :param friend_number: The friend number of the friend that is calling. + :param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + :param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + :return: True on success. + """ + toxav_err_answer = c_int() + result = self.libtoxav.toxav_answer(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate), + c_uint32(video_bit_rate), byref(toxav_err_answer)) + toxav_err_answer = toxav_err_answer.value + if toxav_err_answer == TOXAV_ERR_ANSWER['OK']: + return bool(result) + elif toxav_err_answer == TOXAV_ERR_ANSWER['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['CODEC_INITIALIZATION']: + raise RuntimeError('Failed to initialize codecs for call session. Note that codec initiation will fail if ' + 'there is no receive callback registered for either audio or video.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['FRIEND_NOT_CALLING']: + raise ArgumentError('The friend was valid, but they are not currently trying to initiate a call. This is ' + 'also returned if this client is already in a call with the friend.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['INVALID_BIT_RATE']: + raise ArgumentError('Audio or video bit rate is invalid.') + + # ----------------------------------------------------------------------------------------------------------------- + # Call state graph + # ----------------------------------------------------------------------------------------------------------------- + + def callback_call_state(self, callback, user_data): + """ + Set the callback for the `call_state` event. Pass None to unset. + + :param callback: Python function. + The function for the call_state callback. + + Should take pointer (c_void_p) to ToxAV object, + The friend number (c_uint32) for which the call state changed. + The bitmask of the new call state which is guaranteed to be different than the previous state. The state is set + to 0 when the call is paused. The bitmask represents all the activities currently performed by the friend. + 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_void_p) + self.call_state_cb = c_callback(callback) + self.libtoxav.toxav_callback_call_state(self._toxav_pointer, self.call_state_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Call control + # ----------------------------------------------------------------------------------------------------------------- + + def call_control(self, friend_number, control): + """ + Sends a call control command to a friend. + + :param friend_number: The friend number of the friend this client is in a call with. + :param control: The control command to send. + :return: True on success. + """ + toxav_err_call_control = c_int() + result = self.libtoxav.toxav_call_control(self._toxav_pointer, c_uint32(friend_number), c_int(control), + byref(toxav_err_call_control)) + toxav_err_call_control = toxav_err_call_control.value + if toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['OK']: + return bool(result) + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_IN_CALL']: + raise RuntimeError('This client is currently not in a call with the friend. Before the call is answered, ' + 'only CANCEL is a valid control.') + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['INVALID_TRANSITION']: + raise RuntimeError('Happens if user tried to pause an already paused call or if trying to resume a call ' + 'that is not paused.') + + # ----------------------------------------------------------------------------------------------------------------- + # TODO Controlling bit rates + # ----------------------------------------------------------------------------------------------------------------- + + # ----------------------------------------------------------------------------------------------------------------- + # A/V sending + # ----------------------------------------------------------------------------------------------------------------- + + def audio_send_frame(self, friend_number, pcm, sample_count, channels, sampling_rate): + """ + Send an audio frame to a friend. + + The expected format of the PCM data is: [s1c1][s1c2][...][s2c1][s2c2][...]... + Meaning: sample 1 for channel 1, sample 1 for channel 2, ... + For mono audio, this has no meaning, every sample is subsequent. For stereo, this means the expected format is + LRLRLR... with samples for left and right alternating. + + :param friend_number: The friend number of the friend to which to send an audio frame. + :param pcm: An array of audio samples. The size of this array must be sample_count * channels. + :param sample_count: Number of samples in this frame. Valid numbers here are + ((sample rate) * (audio length) / 1000), where audio length can be 2.5, 5, 10, 20, 40 or 60 milliseconds. + :param channels: Number of audio channels. Sulpported values are 1 and 2. + :param sampling_rate: Audio sampling rate used in this frame. Valid sampling rates are 8000, 12000, 16000, + 24000, or 48000. + """ + toxav_err_send_frame = c_int() + result = self.libtoxav.toxav_audio_send_frame(self._toxav_pointer, c_uint32(friend_number), + cast(pcm, c_void_p), + c_size_t(sample_count), c_uint8(channels), + c_uint32(sampling_rate), byref(toxav_err_send_frame)) + toxav_err_send_frame = toxav_err_send_frame.value + if toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['OK']: + return bool(result) + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['NULL']: + raise ArgumentError('The samples data pointer was NULL.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']: + raise RuntimeError('This client is currently not in a call with the friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['INVALID']: + raise ArgumentError('One of the frame parameters was invalid. E.g. the resolution may be too small or too ' + 'large, or the audio sampling rate may be unsupported.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']: + raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said' + 'payload.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['RTP_FAILED']: + RuntimeError('Failed to push frame through rtp interface.') + + def video_send_frame(self, friend_number, width, height, y, u, v): + """ + Send a video frame to a friend. + + Y - plane should be of size: height * width + U - plane should be of size: (height/2) * (width/2) + V - plane should be of size: (height/2) * (width/2) + + :param friend_number: The friend number of the friend to which to send a video frame. + :param width: Width of the frame in pixels. + :param height: Height of the frame in pixels. + :param y: Y (Luminance) plane data. + :param u: U (Chroma) plane data. + :param v: V (Chroma) plane data. + """ + toxav_err_send_frame = c_int() + result = self.libtoxav.toxav_video_send_frame(self._toxav_pointer, c_uint32(friend_number), c_uint16(width), + c_uint16(height), c_char_p(y), c_char_p(u), c_char_p(v), + byref(toxav_err_send_frame)) + toxav_err_send_frame = toxav_err_send_frame.value + if toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['OK']: + return bool(result) + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['NULL']: + raise ArgumentError('One of Y, U, or V was NULL.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']: + raise RuntimeError('This client is currently not in a call with the friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['INVALID']: + raise ArgumentError('One of the frame parameters was invalid. E.g. the resolution may be too small or too ' + 'large, or the audio sampling rate may be unsupported.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']: + raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said' + 'payload.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['RTP_FAILED']: + RuntimeError('Failed to push frame through rtp interface.') + + # ----------------------------------------------------------------------------------------------------------------- + # A/V receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_audio_receive_frame(self, callback, user_data): + """ + Set the callback for the `audio_receive_frame` event. Pass None to unset. + + :param callback: Python function. + Function for the audio_receive_frame callback. The callback can be called multiple times per single + iteration depending on the amount of queued frames in the buffer. The received format is the same as in send + function. + + Should take pointer (c_void_p) to ToxAV object, + The friend number (c_uint32) of the friend who sent an audio frame. + An array (c_uint8) of audio samples (sample_count * channels elements). + The number (c_size_t) of audio samples per channel in the PCM array. + Number (c_uint8) of audio channels. + Sampling rate (c_uint32) used in this frame. + 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_uint8, c_uint32, c_void_p) + self.audio_receive_frame_cb = c_callback(callback) + self.libtoxav.toxav_callback_audio_receive_frame(self._toxav_pointer, self.audio_receive_frame_cb, user_data) + + def callback_video_receive_frame(self, callback, user_data): + """ + Set the callback for the `video_receive_frame` event. Pass None to unset. + + :param callback: Python function. + The function type for the video_receive_frame callback. + + Should take + toxAV pointer (c_void_p) to ToxAV object, + friend_number The friend number (c_uint32) of the friend who sent a video frame. + width Width (c_uint16) of the frame in pixels. + height Height (c_uint16) of the frame in pixels. + y + u + v Plane data (POINTER(c_uint8)). + The size of plane data is derived from width and height where + Y = MAX(width, abs(ystride)) * height, + U = MAX(width/2, abs(ustride)) * (height/2) and + V = MAX(width/2, abs(vstride)) * (height/2). + ystride + ustride + vstride Strides data (c_int32). Strides represent padding for each plane that may or may not be present. You must + handle strides in your image processing code. Strides are negative if the image is bottom-up + hence why you MUST abs() it when calculating plane buffer size. + user_data 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_uint16, c_uint16, POINTER(c_uint8), POINTER(c_uint8), + POINTER(c_uint8), c_int32, c_int32, c_int32, c_void_p) + self.video_receive_frame_cb = c_callback(callback) + self.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, self.video_receive_frame_cb, user_data) diff --git a/toxygen/toxav_enums.py b/toxygen/toxav_enums.py new file mode 100644 index 0000000..3f3977a --- /dev/null +++ b/toxygen/toxav_enums.py @@ -0,0 +1,131 @@ +TOXAV_ERR_NEW = { + # The function returned successfully. + 'OK': 0, + # One of the arguments to the function was NULL when it was not expected. + 'NULL': 1, + # Memory allocation failure while trying to allocate structures required for the A/V session. + 'MALLOC': 2, + # Attempted to create a second session for the same Tox instance. + 'MULTIPLE': 3, +} + +TOXAV_ERR_CALL = { + # The function returned successfully. + 'OK': 0, + # A resource allocation error occurred while trying to create the structures required for the call. + 'MALLOC': 1, + # Synchronization error occurred. + 'SYNC': 2, + # The friend number did not designate a valid friend. + 'FRIEND_NOT_FOUND': 3, + # The friend was valid, but not currently connected. + 'FRIEND_NOT_CONNECTED': 4, + # Attempted to call a friend while already in an audio or video call with them. + 'FRIEND_ALREADY_IN_CALL': 5, + # Audio or video bit rate is invalid. + 'INVALID_BIT_RATE': 6, +} + +TOXAV_ERR_ANSWER = { + # The function returned successfully. + 'OK': 0, + # Synchronization error occurred. + 'SYNC': 1, + # Failed to initialize codecs for call session. Note that codec initiation will fail if there is no receive callback + # registered for either audio or video. + 'CODEC_INITIALIZATION': 2, + # The friend number did not designate a valid friend. + 'FRIEND_NOT_FOUND': 3, + # The friend was valid, but they are not currently trying to initiate a call. This is also returned if this client + # is already in a call with the friend. + 'FRIEND_NOT_CALLING': 4, + # Audio or video bit rate is invalid. + 'INVALID_BIT_RATE': 5, +} + +TOXAV_FRIEND_CALL_STATE = { + # Set by the AV core if an error occurred on the remote end or if friend timed out. This is the final state after + # which no more state transitions can occur for the call. This call state will never be triggered in combination + # with other call states. + 'ERROR': 1, + # The call has finished. This is the final state after which no more state transitions can occur for the call. This + # call state will never be triggered in combination with other call states. + 'FINISHED': 2, + # The flag that marks that friend is sending audio. + 'SENDING_A': 4, + # The flag that marks that friend is sending video. + 'SENDING_V': 8, + # The flag that marks that friend is receiving audio. + 'ACCEPTING_A': 16, + # The flag that marks that friend is receiving video. + 'ACCEPTING_V': 32, +} + +TOXAV_CALL_CONTROL = { + # Resume a previously paused call. Only valid if the pause was caused by this client, if not, this control is + # ignored. Not valid before the call is accepted. + 'RESUME': 0, + # Put a call on hold. Not valid before the call is accepted. + 'PAUSE': 1, + # Reject a call if it was not answered, yet. Cancel a call after it was answered. + 'CANCEL': 2, + # Request that the friend stops sending audio. Regardless of the friend's compliance, this will cause the + # audio_receive_frame event to stop being triggered on receiving an audio frame from the friend. + 'MUTE_AUDIO': 3, + # Calling this control will notify client to start sending audio again. + 'UNMUTE_AUDIO': 4, + # Request that the friend stops sending video. Regardless of the friend's compliance, this will cause the + # video_receive_frame event to stop being triggered on receiving a video frame from the friend. + 'HIDE_VIDEO': 5, + # Calling this control will notify client to start sending video again. + 'SHOW_VIDEO': 6, +} + +TOXAV_ERR_CALL_CONTROL = { + # The function returned successfully. + 'OK': 0, + # Synchronization error occurred. + 'SYNC': 1, + # The friend_number passed did not designate a valid friend. + 'FRIEND_NOT_FOUND': 2, + # This client is currently not in a call with the friend. Before the call is answered, only CANCEL is a valid + # control. + 'FRIEND_NOT_IN_CALL': 3, + # Happens if user tried to pause an already paused call or if trying to resume a call that is not paused. + 'INVALID_TRANSITION': 4, +} + +TOXAV_ERR_BIT_RATE_SET = { + # The function returned successfully. + 'OK': 0, + # Synchronization error occurred. + 'SYNC': 1, + # The audio bit rate passed was not one of the supported values. + 'INVALID_AUDIO_BIT_RATE': 2, + # The video bit rate passed was not one of the supported values. + 'INVALID_VIDEO_BIT_RATE': 3, + # The friend_number passed did not designate a valid friend. + 'FRIEND_NOT_FOUND': 4, + # This client is currently not in a call with the friend. + 'FRIEND_NOT_IN_CALL': 5, +} + +TOXAV_ERR_SEND_FRAME = { + # The function returned successfully. + 'OK': 0, + # In case of video, one of Y, U, or V was NULL. In case of audio, the samples data pointer was NULL. + 'NULL': 1, + # The friend_number passed did not designate a valid friend. + 'FRIEND_NOT_FOUND': 2, + # This client is currently not in a call with the friend. + 'FRIEND_NOT_IN_CALL': 3, + # Synchronization error occurred. + 'SYNC': 4, + # One of the frame parameters was invalid. E.g. the resolution may be too small or too large, or the audio sampling + # rate may be unsupported. + 'INVALID': 5, + # Either friend turned off audio or video receiving or we turned off sending for the said payload. + 'PAYLOAD_TYPE_DISABLED': 6, + # Failed to push frame through rtp interface. + 'RTP_FAILED': 7, +} diff --git a/toxygen/toxcore_enums_and_consts.py b/toxygen/toxcore_enums_and_consts.py new file mode 100644 index 0000000..4d52837 --- /dev/null +++ b/toxygen/toxcore_enums_and_consts.py @@ -0,0 +1,209 @@ +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_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/toxencryptsave.py b/toxygen/toxencryptsave.py new file mode 100644 index 0000000..e420ecf --- /dev/null +++ b/toxygen/toxencryptsave.py @@ -0,0 +1,113 @@ +import libtox +import util +from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool + + +TOX_ERR_ENCRYPTION = { + # The function returned successfully. + 'OK': 0, + # Some input data, or maybe the output pointer, was null. + 'NULL': 1, + # The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The + # functions accepting keys do not produce this error. + 'KEY_DERIVATION_FAILED': 2, + # The encryption itself failed. + 'FAILED': 3 +} + +TOX_ERR_DECRYPTION = { + # The function returned successfully. + 'OK': 0, + # Some input data, or maybe the output pointer, was null. + 'NULL': 1, + # The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes + 'INVALID_LENGTH': 2, + # The input data is missing the magic number (i.e. wasn't created by this module, or is corrupted) + 'BAD_FORMAT': 3, + # The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The + # functions accepting keys do not produce this error. + 'KEY_DERIVATION_FAILED': 4, + # The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect. + 'FAILED': 5, +} + +TOX_PASS_ENCRYPTION_EXTRA_LENGTH = 80 + + +class ToxEncryptSave(util.Singleton): + + def __init__(self): + super().__init__() + self.libtoxencryptsave = libtox.LibToxEncryptSave() + 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): + func = self.libtoxencryptsave.tox_is_data_encrypted + func.restype = c_bool + result = func(c_char_p(bytes(data))) + return result + + def pass_encrypt(self, data): + """ + Encrypts the given data with the given passphrase. + + :return: output array + """ + out = create_string_buffer(len(data) + TOX_PASS_ENCRYPTION_EXTRA_LENGTH) + tox_err_encryption = c_int() + self.libtoxencryptsave.tox_pass_encrypt(c_char_p(data), + c_size_t(len(data)), + c_char_p(bytes(self._passphrase, 'utf-8')), + c_size_t(len(self._passphrase)), + out, + byref(tox_err_encryption)) + tox_err_encryption = tox_err_encryption.value + if tox_err_encryption == TOX_ERR_ENCRYPTION['OK']: + return out[:] + elif tox_err_encryption == TOX_ERR_ENCRYPTION['NULL']: + raise ArgumentError('Some input data, or maybe the output pointer, was null.') + elif tox_err_encryption == TOX_ERR_ENCRYPTION['KEY_DERIVATION_FAILED']: + raise RuntimeError('The crypto lib was unable to derive a key from the given passphrase, which is usually a' + ' lack of memory issue. The functions accepting keys do not produce this error.') + elif tox_err_encryption == TOX_ERR_ENCRYPTION['FAILED']: + raise RuntimeError('The encryption itself failed.') + + def pass_decrypt(self, data): + """ + Decrypts the given data with the given passphrase. + + :return: output array + """ + out = create_string_buffer(len(data) - TOX_PASS_ENCRYPTION_EXTRA_LENGTH) + tox_err_decryption = c_int() + self.libtoxencryptsave.tox_pass_decrypt(c_char_p(bytes(data)), + c_size_t(len(data)), + c_char_p(bytes(self._passphrase, 'utf-8')), + c_size_t(len(self._passphrase)), + out, + byref(tox_err_decryption)) + tox_err_decryption = tox_err_decryption.value + if tox_err_decryption == TOX_ERR_DECRYPTION['OK']: + return out[:] + elif tox_err_decryption == TOX_ERR_DECRYPTION['NULL']: + raise ArgumentError('Some input data, or maybe the output pointer, was null.') + elif tox_err_decryption == TOX_ERR_DECRYPTION['INVALID_LENGTH']: + raise ArgumentError('The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes') + elif tox_err_decryption == TOX_ERR_DECRYPTION['BAD_FORMAT']: + raise ArgumentError('The input data is missing the magic number (i.e. wasn\'t created by this module, or is' + ' corrupted)') + elif tox_err_decryption == TOX_ERR_DECRYPTION['KEY_DERIVATION_FAILED']: + raise RuntimeError('The crypto lib was unable to derive a key from the given passphrase, which is usually a' + ' lack of memory issue. The functions accepting keys do not produce this error.') + elif tox_err_decryption == TOX_ERR_DECRYPTION['FAILED']: + raise RuntimeError('The encrypted byte array could not be decrypted. Either the data was corrupt or the ' + 'password/key was incorrect.') diff --git a/toxygen/toxygen.pro b/toxygen/toxygen.pro index 9643c8b..aeafe07 100644 --- a/toxygen/toxygen.pro +++ b/toxygen/toxygen.pro @@ -1,2 +1,2 @@ -SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py plugins/plugin_super_class.py callbacks.py widgets.py avwidgets.py mainscreen_widgets.py passwordscreen.py -TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts translations/uk_UA.ts +SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py plugins/plugin_super_class.py callbacks.py widgets.py avwidgets.py mainscreen_widgets.py +TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts diff --git a/toxygen/translations/en_GB.ts b/toxygen/translations/en_GB.ts index 7186005..a50fd4c 100644 --- a/toxygen/translations/en_GB.ts +++ b/toxygen/translations/en_GB.ts @@ -3,22 +3,22 @@ AddContact - + Add contact Add contact - + TOX ID: TOX ID: - + Message: Message: - + TOX ID or public key of contact @@ -26,7 +26,7 @@ Callback - + File from @@ -34,32 +34,32 @@ Form - + Send request Send request - + IPv6 IPv6 - + UDP UDP - + Proxy Proxy - + IP: IP: - + Port: Port: @@ -69,118 +69,113 @@ Online contacts - + HTTP HTTP - + WARNING: using proxy with enabled UDP can produce IP leak - - - Download nodes list from tox.chat - - MainWindow - + Profile - + Settings - + About - + Add contact - + Privacy - + Interface - + Notifications - + Network - + About program - + User {} wants to add you to contact list. Message: {} - + Friend request - + Choose file Choose file - + Disallow auto accept - + Allow auto accept - + Set alias - + Clear history - + Remove friend - + Enter new alias for friend {} or leave empty to use friend's name: Enter new alias for friend {} or leave empty to use friend's name: - + Audio Audio @@ -190,24 +185,24 @@ can produce IP leak Find contact - + Friend added Friend added - + Toxygen is Tox client written on Python. Version: Toxygen is Tox client written on Python. Version: - + Friend added without sending friend request Friend added without sending friend request - + Choose folder Choose folder @@ -222,300 +217,220 @@ Version: Send file - + Send message Send message - + Start audio call with friend Start audio call with friend - + Plugins - + List of plugins - + Search - + All - + Online - + Notes - + Notes about user - + Copy link location - + Copy - + Select all - + Delete - + Paste - + Cut - + Undo - + Redo - + Save - + User {} is now known as {} - + Delete message - + Lock - + Cannot lock app - + Error. Profile password is not set. - + Name - + Status message - + Public key - + Error - + Profile with this name already exists - + Choose folder with sticker pack - + Choose folder with smiley pack - + Import plugin - + Choose folder with plugin - + Restart Toxygen - + Plugin will be loaded after restart - - - Quote selected text - - - - - Chat history - - - - - Export as text - - - - - Export as HTML - - - - - Updates - - - - - Online first - - - - - Online and by name - - - - - Online first and by name - - - - - Block friend - - - - - Not found - - - - - Text "{}" was not found - - - - - Reload plugins - - - - - Video - - - - - User {} invites you to group chat. Accept? - - - - - Group chat invite - - - - - {} users in chat - - - - - Enter new title for group {}: - - - - - Set title - - - - - Create group chat - - - - - Invite to group chat - - - - - Leave chat - - MenuWindow - + + Send audio message to friend {} + + + + + Start recording + + + + + Stop recording + + + + Send screenshot Send screenshot - + Send file Send file - + + Send audio message + + + + + Send video message + + + + Add smiley - + Send sticker @@ -523,63 +438,25 @@ Version: NetworkSettings - + Network settings Network settings - + Restart TOX core Restart Tox core - - PasswordScreen - - - Profile password - - - - - Password (at least 8 symbols) - - - - - Confirm password - - - - - Set password - - - - - Passwords do not match - - - - - There is no way to recover lost passwords - - - - - Password must be at least 8 symbols - - - PluginWindow - + List of commands for plugin {} - + No commands available @@ -587,42 +464,42 @@ Version: PluginsForm - + Plugins - + Open selected plugin - + No GUI found for this plugin - + No description available - + Disable plugin - + Enable plugin - + No plugins found - + Error @@ -630,218 +507,208 @@ Version: ProfileSettingsForm - + Export profile - + Profile settings - + Name: - + Status: - + TOX ID: - + Copy TOX ID - + New avatar - + Reset avatar - + New NoSpam New NoSpam - + Profile password - + Password (at least 8 symbols) - + Confirm password - + Set password - + Passwords do not match - + Leaving blank will reset current password - + There is no way to recover lost passwords - + Password must be at least 8 symbols - + Choose avatar - + Online - + Away - + Busy - + Mark as not default profile - + Mark as default profile - + Copy public key - - - Use new path - - - - - Do you want to move your profile to this location? - - WelcomeScreen - + Don't show again - + Tip of the day - + Press Esc if you want hide app to tray. - + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> - + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. - + + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a> + + + + Right click on screenshot button hides app to tray during screenshot. - + Use Settings -> Interface to customize interface. - + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. - + Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. - + + New in Toxygen v0.2.3:<br>TCS compliance<br>Plugins, smileys and stickers import<br>Bug fixes + + + + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu - + Use right click on inline image to save it - - - Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> - - - - - New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes - - audioSettingsForm - + Audio settings Audio settings - + Input device: Input device: - + Output device: Output device: @@ -849,32 +716,32 @@ Version: incoming_call - + Incoming video call Incoming video call - + Incoming audio call Incoming audio call - + Outgoing video call - + Outgoing audio call - + Call declined - + Call finished @@ -882,274 +749,226 @@ Version: interfaceForm - + Interface settings - + Theme: - + Language: - + Smileys - + Smiley pack: - + Mirror mode - + Messages font size: - + Restart app to apply settings - + Restart required - + Select unread messages notification color - + Compact contact list - + Import smiley pack - + Import sticker pack - + Show avatars in chat - - - Close to tray - - - - - Select font - - login - + Log in - + Create - + Profile name: - + Load profile - + Use as default - + Load existing profile - + Create new profile - + toxygen - + Profile name - + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? - + Do you want to set profile password? - - - Do you want to save profile in default folder? If no, profile will be saved in program folder - - - - - Profile saving error! Does Toxygen have permission to write to this directory? - - - - - Update for Toxygen was found. Download and install it? - - notificationsForm - + Notification settings - + Enable notifications - + Enable call's sound - + Enable sound notifications - - - Notify about all messages in groups - - - - - pass - - - Enter password - - - - - Password: - - - - - Incorrect password - - privacySettings - + Privacy settings - + Save chat history - + Allow file auto accept - + Send typing notifications - + Auto accept default path: - + Change - + Allow inlines - + Chat history - + History will be cleaned! Continue? - + Blocked users: Blocked users: - + Unblock Unblock - + Block user Block user - + Add to friend list Add to friend list - + Do you want to add this user to friend list? Do you want to add this user to friend list? @@ -1159,12 +978,12 @@ Version: Block by TOX ID: - + Block by public key: - + Save unsent messages only @@ -1172,115 +991,34 @@ Version: tray - + Open Toxygen - + Exit - + Set status - + Online - + Away - + Busy - - updateSettingsForm - - - Update settings - - - - - Select update mode: - - - - - Update Toxygen - - - - - Disabled - - - - - Manual - - - - - Auto - - - - - Error - - - - - Problems with internet connection - - - - - Updater not found - - - - - No updates found - - - - - Toxygen is up to date - - - - - videoSettingsForm - - - Video settings - - - - - Device: - - - - - Desktop - - - - - Select region - - - diff --git a/toxygen/translations/fr_FR.qm b/toxygen/translations/fr_FR.qm index 33b0cbc..e7ab531 100644 Binary files a/toxygen/translations/fr_FR.qm and b/toxygen/translations/fr_FR.qm differ diff --git a/toxygen/translations/fr_FR.ts b/toxygen/translations/fr_FR.ts index 1931a26..a547576 100644 --- a/toxygen/translations/fr_FR.ts +++ b/toxygen/translations/fr_FR.ts @@ -2,64 +2,64 @@ AddContact - - - Add contact - Ajouter un contact - - TOX ID: - ID Tox : + Add contact + Rajouter un contact - + + TOX ID: + ID TOX : + + + Message: Message : - + TOX ID or public key of contact - ID Tox ou clé publique de contact + Callback - + File from - Fichier de + Form - + Send request Envoyer une demande - + IPv6 IPv6 - + UDP UDP - + Proxy Proxy - + IP: IP : - + Port: Port : @@ -69,82 +69,75 @@ Contacts connectés - + HTTP HTTP - + WARNING: using proxy with enabled UDP can produce IP leak - ATTENTION : -Utiliser un proxy avec UDP -peut entrainer une fuite d'IP - - - - Download nodes list from tox.chat MainWindow - + Profile - Profil + Profile - + Settings - Paramètres + Paramêtres - + About À Propos - + Add contact - Ajouter un contact + Rajouter un contact - + Privacy Confidentialité - + Interface Interface - + Notifications Notifications - + Network Réseau - + About program - À propos de toxygen + À propos du programme - + User {} wants to add you to contact list. Message: {} - L'Utilisateur {} veut vous ajouter à sa liste de contacts. Message : {} + L'Utilisateur {} veut vout rajouter à sa liste de contacts. Message : {} - + Friend request - Demande de contact + Demande d'amis @@ -152,27 +145,27 @@ peut entrainer une fuite d'IP Toxygen est un client Tox écris en Python 2.7. Version : - + Choose file - Sélectionner un fichier + Choisir un fichier - + Disallow auto accept Désactiver l'auto-réception - + Allow auto accept Activer l'auto-réception - + Set alias Définir un alias - + Clear history Vider l'historique @@ -182,17 +175,17 @@ peut entrainer une fuite d'IP Copier la clé publique - + Remove friend - Retirer ce contact + Retirer un ami - + Enter new alias for friend {} or leave empty to use friend's name: - Entrez un nouvel alias pour le contact {} ou laissez vide pour garder son nom de base : + Entrez un nouvel alias pour l'ami {} ou laissez vide pour garder son nom de base : - + Audio Audio @@ -202,26 +195,26 @@ peut entrainer une fuite d'IP Trouver le contact - + Friend added - Contact ajouté + Ami rajouté - + Toxygen is Tox client written on Python. Version: Toxygen est un client Tox écrit en Python. Version : - + Friend added without sending friend request - Contact ajouté sans envoi de demande + Ami rajouté sans avoir envoyé de demande - + Choose folder - Sélectionner un dossier + Choisir le dossier @@ -234,631 +227,498 @@ Version : Envoyer le fichier - + Send message Envoyer le message - + Start audio call with friend - Démarrer un appel audio avec un ami + Lancer un appel audio avec un ami - + Plugins - Plugins + - + List of plugins - Liste de plugins + - + Search - Chercher + - + All - Tous + - + Online - En ligne + - + Notes - Notes + - + Notes about user - Notes sur l'utilisateur + - + Copy link location - Copier l'emplacement du lien + - + Copy - Copier + - + Select all - Tout sélectionner + - + Delete - Supprimer + - + Paste - Coller + - + Cut - Couper + - + Undo - Annuler + - + Redo - Refaire + - + Save - Sauvegarder + - + User {} is now known as {} - L'utilisateur {} s'appelle désormais {} + - + Delete message - Supprimer ce message + - + Lock - Verrouiller + - + Cannot lock app - Impossible de verrouiller l'application + - + Error. Profile password is not set. - Erreur. Le profil n'a pas de mot de passe. + - + Name - Nom + - + Status message - Status + - + Public key - Clé publique + - + Error - Erreur + - + Profile with this name already exists - Un profil ayant ce nom existe déjà + - + Choose folder with sticker pack - Sélectionner le dossier contenant le pack de stickers + - + Choose folder with smiley pack - Sélectionner le dossier contenant le pack de smileys + - + Import plugin - Importer un plugin + - + Choose folder with plugin - Sélectionner un dossier avec des plugins + - + Restart Toxygen - Redémarrer Toxyger + - + Plugin will be loaded after restart - Le plugin sera chargé après le redémarrage - - - - Quote selected text - Citer le texte sélectionné - - - - Chat history - Historique de la conversation - - - - Export as text - Exporter comme texte - - - - Export as HTML - Exporter comme HTML - - - - Updates - Mises à jour - - - - Online first - En ligne d'abord - - - - Online and by name - En ligne et par nom - - - - Online first and by name - En ligne d'abord puis par nom - - - - Block friend - Bloquer le contact - - - - Not found - Non trouvé - - - - Text "{}" was not found - Le texte "{}" n'a pas été trouvé - - - - Reload plugins - Recharger les plugins - - - - Video - Vidéo - - - - User {} invites you to group chat. Accept? - - - - - Group chat invite - - - - - {} users in chat - - - - - Enter new title for group {}: - - - - - Set title - - - - - Create group chat - - - - - Invite to group chat - - - - - Leave chat MenuWindow - + + Send audio message to friend {} + + + + + Start recording + + + + + Stop recording + + + + Send screenshot - Envoyer une capture d'écran + Envoyer une capture d'écran - + Send file - Envoyer un fichier + Envoyer le fichier - + + Send audio message + + + + + Send video message + + + + Add smiley - Ajouter un smiley + - + Send sticker - Ajouter un sticker + NetworkSettings - + Network settings Paramètres réseaux - + Restart TOX core - Relancer le noyau Tox - - - - PasswordScreen - - - Profile password - Mot de passe du profil - - - - Password (at least 8 symbols) - Mot de passe (8 symboles minimum) - - - - Confirm password - Confirmation - - - - Set password - Enregistrer le mot de passe - - - - Passwords do not match - Les mots de passes sont différents - - - - There is no way to recover lost passwords - Il est impossible de récuperer un mot de passe perdu - - - - Password must be at least 8 symbols - Un mot de passe doit faire 8 symboles minimum + Relancer le noyau TOX PluginWindow - + List of commands for plugin {} - Liste de commandes du plugin {} + - + No commands available - Pas de commandes disponibles + PluginsForm - + Plugins - Plugins + - + Open selected plugin - Ouvrir le plugin sélectionné + - + No GUI found for this plugin - Pas d'interface pour ce plugin + - + No description available - Pas de description + - + Disable plugin - Désactiver le plugin + - + Enable plugin - Activer le plugin + - + No plugins found - Pas de plugin trouvé + - + Error - Erreur + ProfileSettingsForm - - - Export profile - Exporter le profil - - Profile settings - Paramètres du profil + Export profile + Exporter le profile + Profile settings + Paramêtres du profil + + + Name: Nom : - + Status: Status : - + TOX ID: ID TOX : - + Copy TOX ID - Copier l'ID Tox + Copier l'ID TOX - + New avatar Nouvel avatar - + Reset avatar Réinitialiser l'avatar - + New NoSpam Nouveau NoSpam - - - Profile password - Mot de passe du profil - - Password (at least 8 symbols) - Mot de passe (8 symboles minimum) + Profile password + - Confirm password - Confirmation + Password (at least 8 symbols) + + Confirm password + + + + Set password - Sauvegarder le mot de passe + - + Passwords do not match - Les mots de passe sont différents - - - - Leaving blank will reset current password - Laisser vide réinitialisera le mot de passe actuel + - There is no way to recover lost passwords - Il est impossible de récupérer un mot de passe perdu - - - - Password must be at least 8 symbols - Le mot de passe doit faire 8 symboles minimum - - - - Choose avatar - Choisir l'avatar + Leaving blank will reset current password + - Online - En ligne + There is no way to recover lost passwords + + + + + Password must be at least 8 symbols + + + + + Choose avatar + - Away - Absent + Online + - Busy - Occupé - - - - Mark as not default profile - Ne plus en faire le profil par défaut - - - - Mark as default profile - En faire le profil par défaut + Away + + Busy + + + + + Mark as not default profile + + + + + Mark as default profile + + + + Copy public key - Copier la clé publique - - - - Use new path - Utiliser un nouveau chemin - - - - Do you want to move your profile to this location? - Déplacer le profil dans ce dossier ? + Copier la clé publique WelcomeScreen - + Don't show again - Ne plus montrer - - - - Tip of the day - Astuce du jou - - - - Press Esc if you want hide app to tray. - Appuyez sur échap pour réduire l'application. - - - - You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> - Vous pouvez utiliser Tox avec Tor. Pour plus d'informations, voir <a href="https://wiki.tox.chat/users/tox_over_tor_tot">cet article</a> - - - - Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. - Vous pouvez mettre un mot de passe dans Profil -> Paramètres -> Mot de passe pour que toxygen encrypte votre historique et vos paramètres. - - - - Right click on screenshot button hides app to tray during screenshot. - Faire un clic droit sur le bouton de capture d'écran réduit l'application avant de capturer l'écran. - - - - Use Settings -> Interface to customize interface. - Vous pouvez customizer votre interface dans Paramètres -> Interface. - - - - Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. - Toxygen permet d'envoyer des messages et fichiers en différé. Envoyez des messages ou fichiers à un contact hors ligne et il le recevra plus tard. + + Tip of the day + + + + + Press Esc if you want hide app to tray. + + + + + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> + + + + + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. + + + + + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a> + + + + + Right click on screenshot button hides app to tray during screenshot. + + + + + Use Settings -> Interface to customize interface. + + + + + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. + + + + Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. - Vous pouvez empecher le spam dans les demandes de contact avec Profil -> Paramètres -> Nouveau NoSpam. + - + + New in Toxygen v0.2.3:<br>TCS compliance<br>Plugins, smileys and stickers import<br>Bug fixes + + + + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu - Pour supprimer un seul message dans une conversation, faites un clic droit sur l'heure du message et sélectionnez "Supprimer ce message" dans le menu + - + Use right click on inline image to save it - Pour sauvegarder une image intégrée, faites un clic droit dessus - - - - Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> - Depuis la version 0.1.3 Toxygen supporte les plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">En savoir plus</a> - - - - New in Toxygen 0.3.0:<br>Video calls<br>Python3.6 support<br>Migration to PyQt5 - Nouveau dans Toxygen 0.3.0 : <br>Appels vidéo<br>Support de Python3.6<br>Migration vers PyQt5 - - - - New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes audioSettingsForm - + Audio settings Paramètres audio - + Input device: Péripherique d'entrée : - + Output device: Péripherique de sortie : @@ -866,158 +726,148 @@ Version : incoming_call - + Incoming video call Appel vidéo entrant - + Incoming audio call Appel audio entrant - + Outgoing video call - Appel vidéo sortant + - + Outgoing audio call - Appel audio sortant + - + Call declined - Appel refusé + - + Call finished - Appel terminé + interfaceForm - + Interface settings - Paramètres de l'interface + Paramêtres de l'interface - + Theme: Thème : - + Language: Langue : - + Smileys - Smileys + - + Smiley pack: - Pack de smileys : + - + Mirror mode - Mode miroir + - + Messages font size: - Taille des messages : + - + Restart app to apply settings - Redémarrer toxygen pour appliquer les paramètres + - + Restart required - Redémarrage nécessaire + - + Select unread messages notification color - Sélectionner la couleur des messages non-lus + - + Compact contact list - Liste de contacts compacte + - + Import smiley pack - Importer un pack de smileys + - + Import sticker pack - Importer un pack de stickers + - + Show avatars in chat - Montrer les avatars dans la conversation - - - - Close to tray - Réduire - - - - Select font - Sélectionner la police + login - + Log in Se connecter - + Create Créer - + Profile name: Nom du profil : - + Load profile Charger le profil - + Use as default Utiliser par défaut - + Load existing profile Charger un profil existant - + Create new profile Créer un nouveau profil - + toxygen toxygen @@ -1027,153 +877,115 @@ Version : Il semble qu'une autre instance de Toxygen utilise ce profil ! Continuer ? - + Profile name - Nom de profil + - + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? - Ce profil semble être utilisé par une autre instance de toxygen ou avoir été incorrectement fermé . Continuer ? + - + Do you want to set profile password? - Souhaitez vous protéger le profil par un mot de passe ? - - - - Do you want to save profile in default folder? If no, profile will be saved in program folder - Souhaitez vous conserver le profil dans le dossier par défaut ? Si non, il sera conservé dans le dossier du programme - - - - Profile saving error! Does Toxygen have permission to write to this directory? - Un problème est survenu lors de la sauvegarde du profil ! Toxygen as t'il le droit d'écrire dans ce dossier ? - - - - Update for Toxygen was found. Download and install it? - Une mise à jour est disponible. La télécharger et l'installer ? + notificationsForm - + Notification settings - Paramètres de notification + Paramêtres de notification - + Enable notifications Activer les notifications - + Enable call's sound Activer les sons d'appel - + Enable sound notifications Activer les sons de notifications - - - Notify about all messages in groups - - - - - pass - - - Enter password - Entrer le mot de passe - - - - Password: - Mot de passe : - - - - Incorrect password - Mot de passe incorrect - privacySettings - + Privacy settings - Paramètres de confidentialité + Paramêtres de confidentialité - + Save chat history - Sauvegarder l'historique de conversation + Sauvegarder l'historique de chat - + Allow file auto accept Autoriser les fichier automatiquement - + Send typing notifications - Informer de la frappe + Notifier la frappe - + Auto accept default path: - Chemin par défaut des fichiers acceptés automatiquement : + Chemin d'accès des fichiers acceptés automatiquement : - + Change Modifier - + Allow inlines - Activer l'affichage integré + Activer l'auto-réception - + Chat history - Historique de conversation + Historique de chat - + History will be cleaned! Continue? - L'Historique va être vidé ! Confirmer ? + L'Historique va être nettoyé ! Confirmer ? - + Blocked users: Utilisateurs bloqués : - + Unblock Débloquer - + Block user Bloquer l'utilisateur - + Add to friend list - Ajouter à la liste de contacts + Ajouter à la liste des amis - + Do you want to add this user to friend list? - Voulez vous aajouter cet utilisateur à votre liste de contacts ? + Voulez vous rajouter cet utilisateur à votre liste d'amis ? @@ -1181,127 +993,46 @@ Version : Bloquer l'ID TOX : - + Block by public key: - Bloquer par clé publique : + - + Save unsent messages only - Sauvegarder les messages non envoyés uniquement + tray - + Open Toxygen Ouvrir Toxygen - + Exit Quitter - + Set status - Changer le status - - - - Online - En ligne - - - - Away - Absent - - - - Busy - Occupé - - - - updateSettingsForm - - - Update settings - Paramètres de mise à jour - - - - Select update mode: - Sélectionner le mode de mise à jour : - - - - Update Toxygen - Mettre à jour toxygen - - - - Disabled - Désactivé - - - - Manual - Manuel - - - - Auto - Automatique - - - - Error - Erreur - - - - Problems with internet connection - Il y à des problèmes avec votre connexion internet - - - - Updater not found - Updater non trouvé - - - - No updates found - Pas de mises à jour trouvés - - - - Toxygen is up to date - Toxygen est à jour - - - - videoSettingsForm - - - Video settings - Paramètres vidéo - - - - Device: - Périphérique : - - - - Desktop - - Select region + + Online + + + + + Away + + + + + Busy diff --git a/toxygen/translations/ru_RU.qm b/toxygen/translations/ru_RU.qm index 1231884..f8a1019 100644 Binary files a/toxygen/translations/ru_RU.qm and b/toxygen/translations/ru_RU.qm differ diff --git a/toxygen/translations/ru_RU.ts b/toxygen/translations/ru_RU.ts index 8d6c63c..5664f77 100644 --- a/toxygen/translations/ru_RU.ts +++ b/toxygen/translations/ru_RU.ts @@ -3,22 +3,22 @@ AddContact - + Add contact Добавить контакт - + TOX ID: TOX ID: - + Message: Сообщение: - + TOX ID or public key of contact TOX ID или публичный ключ контакта @@ -26,7 +26,7 @@ Callback - + File from Файл от @@ -34,32 +34,32 @@ Form - + Send request Отправить запрос - + IPv6 IPv6 - + UDP UDP - + Proxy Прокси - + IP: IP: - + Port: Порт: @@ -69,12 +69,12 @@ Контакты в сети - + HTTP HTTP - + WARNING: using proxy with enabled UDP can produce IP leak @@ -82,93 +82,88 @@ can produce IP leak использование прокси с UDP может привести к утечке IP - - - Download nodes list from tox.chat - - MainWindow - + Profile Профиль - + Settings Настройки - + About О программе - + Add contact Добавить контакт - + Privacy Приватность - + Interface Интерфейс - + Notifications Уведомления - + Network Сеть - + About program О программе - + User {} wants to add you to contact list. Message: {} Пользователь {} хочет добавить Вас в список контактов. Сообщение: {} - + Friend request Запрос на добавление в друзья - + Choose file Выберите файл - + Disallow auto accept Запретить автоматическое получение файлов - + Allow auto accept Разрешить автоматическое получение файлов - + Set alias Изменить псевдоним - + Clear history Очистить историю @@ -178,17 +173,17 @@ can produce IP leak Копировать публичный ключ - + Remove friend Удалить друга - + Enter new alias for friend {} or leave empty to use friend's name: Введите новый псевдоним для друга {} или оставьте пустым для использования его имени: - + Audio Аудио @@ -198,23 +193,23 @@ can produce IP leak Найти контакт - + Friend added Друг добавлен - + Toxygen is Tox client written on Python. Version: Toxygen - клиент для мессенджера Tox, написанный на Python. Версия: - + Friend added without sending friend request Друг добавлен без отправки запроса на добавление в друзья - + Choose folder Выбрать папку @@ -229,325 +224,220 @@ Version: Отправить файл - + Send message Отправить сообщение - + Start audio call with friend Начать аудиозвонок с другом - + Plugins Плагины - + List of plugins Список плагинов - + Search Поиск - + All Все - + Online Онлайн - + Notes Заметки - + Notes about user Заметки о пользователе - + Copy link location Копировать адрес ссылки - + Copy Копировать - + Select all Выделить всё - + Delete Удалить - + Paste Вставить - + Cut Вырезать - + Undo Отменить - + Redo Повторить - + Save Сохранить - + User {} is now known as {} Пользователь {} сейчас известен как {} - + Delete message Удалить сообщение - + Lock Заблокировать - + Cannot lock app Невозможно заблокировать приложение - + Error. Profile password is not set. Ошибка. Пароль профиля не установлен. - + Name Имя - + Status message Статус - + Public key Публичный ключ - + Error Ошибка - + Profile with this name already exists Профиль с данным именем уже существует - + Choose folder with sticker pack Выберите папку в паком стикеров - + Choose folder with smiley pack Выберите папку с паком смайлов - + Import plugin Импортировать плагин - + Choose folder with plugin Выберите папку с плагином - + Restart Toxygen Перезапустите Toxygen - + Plugin will be loaded after restart Плагин будет загружен после перезапуска - - - Quote selected text - Цитировать выбранный текст - - - - Chat history - История чата - - - - Export as text - Экспортировать как текст - - - - Export as HTML - Экспортировать как HTML - - - - Updates - Обновления - - - - Online first - Сначала онлайн - - - - Online and by name - Онлайн и по имени - - - - Online first and by name - Сначала онлайн и по имени - - - - Block friend - Заблокировать друга - - - - Not found - Не найдено - - - - Text "{}" was not found - Текст "{}" не был найден - - - - Reload plugins - Перезагрузить плагины - - - - Video - Видео - - - - User {} invites you to group chat. Accept? - Пользователь {} приглашает Вас в групповой чат. Принять приглашение? - - - - Group chat invite - Приглашение в групповой чат - - - - {} users in chat - {} пользователей в чате - - - - Enter new title for group {}: - Введите название для группы {}: - - - - Set title - Изменить название - - - - Create group chat - Создать групповой чат - - - - Invite to group chat - Пригласить в групповой чат - - - - Leave chat - Покинуть чат - MenuWindow Send audio message to friend {} - Отправить аудиосообщение другу + Отправить аудиосообщение другу Start recording - Начать запись + Начать запись Stop recording - Остановить запись + Остановить запись - + Send screenshot Отправить снимок экрана - + Send file Отправить файл - + Send audio message - Отправить аудиосообщение + Отправить аудиосообщение - + Send video message - Отправить видеосообщение + Отправить видеосообщение - + Add smiley Добавить смайлик - + Send sticker Отправить стикер @@ -555,63 +445,25 @@ Version: NetworkSettings - + Network settings Настройки сети - + Restart TOX core Перезапустить ядро TOX - - PasswordScreen - - - Profile password - Пароль профиля - - - - Password (at least 8 symbols) - Пароль (минимум 8 символов) - - - - Confirm password - Подтверждение пароля - - - - Set password - Изменить пароль - - - - Passwords do not match - Пароли не совпадают - - - - There is no way to recover lost passwords - Восстановление забытых паролей не поддерживается - - - - Password must be at least 8 symbols - Пароль должен быть длиной не менее 8 символов - - PluginWindow - + List of commands for plugin {} Список команд для плагина {} - + No commands available Команды не найдены @@ -619,42 +471,42 @@ Version: PluginsForm - + Plugins Плагины - + Open selected plugin Открыть выбранный плагин - + No GUI found for this plugin GUI для данного плагина не найден - + No description available Описание недоступно - + Disable plugin Отключить плагин - + Enable plugin Включить плагин - + No plugins found Плагины не найдены - + Error Ошибка @@ -662,32 +514,32 @@ Version: ProfileSettingsForm - + Export profile Экспорт профиля - + Profile settings Настройки профиля - + Name: Имя: - + Status: Статус: - + TOX ID: TOX ID: - + Copy TOX ID Копировать TOX ID @@ -697,120 +549,110 @@ Version: Язык: - + New avatar Новый аватар - + Reset avatar Сбросить аватар - + New NoSpam Новый NoSpam - + Profile password Пароль профиля - + Password (at least 8 symbols) Пароль (минимум 8 символов) - + Confirm password Подтверждение пароля - + Set password Изменить пароль - + Passwords do not match Пароли не совпадают - + Leaving blank will reset current password Пустое поле сбросит текущий пароль - + There is no way to recover lost passwords Восстановление забытых паролей не поддерживается - + Password must be at least 8 symbols Пароль должен быть длиной не менее 8 символов - + Choose avatar Выбрать аватар - + Online Онлайн - + Away Нет на месте - + Busy Занят - + Mark as not default profile Отключить автозагрузку профиля - + Mark as default profile Сделать профилем по умолчанию - + Copy public key Копировать публичный ключ - - - Use new path - Использовать новый путь - - - - Do you want to move your profile to this location? - Вы хотите переместить ваш профиль в эту папку? - WelcomeScreen - + Don't show again Не показывать снова - + Tip of the day Подсказка дня - + Press Esc if you want hide app to tray. Нажатие Esc сворачивает приложение в трей. @@ -820,7 +662,7 @@ Version: Правый клик на кнопке скриншота сворачивает приложение в трей на время скриншота - + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> Вы можете использовать Tox через Tor. Дополнительная информация <a href="https://wiki.tox.chat/users/tox_over_tor_tot">тут</a> @@ -830,14 +672,14 @@ Version: Используйте Настройки -> Интерфейс для настройки интерфейса - + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. Установите пароль профиля: Профиль -> Настройки. Пароль позволяет шифровать историю переписки и настройки. - + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a> - С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> + С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> @@ -855,80 +697,55 @@ Version: Установите новый NoSpam, чтобы избежать спам запросов в друзья: Профиль->Настройки->Новый NoSpam - + Right click on screenshot button hides app to tray during screenshot. Правый клик на кнопке скриншота сворачивает приложение в трей на время скриншота. - + Use Settings -> Interface to customize interface. Используйте Настройки -> Интерфейс для настройки интерфейса. - + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. Toxygen поддерживает псевдооффлайн сообщения и файл трансферы. - + Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. Установите новый NoSpam, чтобы избежать спам запросов в друзья: Профиль->Настройки->Новый NoSpam. New in Toxygen v0.2.3:<br>TCS compliance<br>Plugins, smileys and stickers import<br>Bug fixes - Новое в Toxygen 0.2.3:<br>Соответствие TCS<br>Импорт плагинов, смайлов и стикеров<br>Исправления ошибок + Новое в Toxygen 0.2.3:<br>Соответствие TCS<br>Импорт плагинов, смайлов и стикеров<br>Исправления ошибок - + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu Чтобы удалить отдельное сообщение в чате сделайте правый клик на спиннер или время сообщения и выберите "Удалить" в меню - + Use right click on inline image to save it Правый клик на инлайн изображении позволит сохранить его - - - New in Toxygen v0.2.4:<br>File transfers update<br>Autoreconnection<br>Improvements<br>Bug fixes - Новое в Toxygen v0.2.4:<br>Передача файлов обновлена<br>Автопереподключение<br>Улучшения<br>Исправления ошибок - - - - New in Toxygen v0.2.6:<br>Updater<br>Better contact sorting<br>Plugins improvements - Новое в Toxygen v0.2.6:<br>Поддержка обновлений<br>Улучшенная сортировка контактов<br>Улучшения в работе плагинов - - - - Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> - С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> - - - - New in Toxygen 0.3.0:<br>Video calls<br>Python3.6 support<br>Migration to PyQt5 - Новое в Toxygen 0.3.0:<br>Видеозвонки<br>Поддержка Python3.6<br>Миграция на PyQt5 - - - - New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes - - audioSettingsForm - + Audio settings Настройки аудио - + Input device: Устройство ввода: - + Output device: Устройство вывода: @@ -936,32 +753,32 @@ Version: incoming_call - + Incoming video call Входящий видеозвонок - + Incoming audio call Входящий аудиозвонок - + Outgoing video call Исходящий видеозвонок - + Outgoing audio call Исходящий аудиозвонок - + Call declined Звонок отменен - + Call finished Звонок завершен @@ -969,125 +786,115 @@ Version: interfaceForm - + Interface settings Настройки интерфейса - + Theme: Тема: - + Language: Язык: - + Smileys Смайлики - + Smiley pack: Набор смайликов: - + Mirror mode Зеркальный режим - + Messages font size: Размер шрифта сообщений: - + Restart app to apply settings Для применения настроек необходимо перезапустить приложение - + Restart required Требуется перезапуск - + Select unread messages notification color Цвет уведомления о сообщении - + Compact contact list Компактный список контактов - + Import smiley pack Импортировать смайлы - + Import sticker pack Импортировать стикеры - + Show avatars in chat Показывать аватары в чате - - - Close to tray - Сворачивать в трей - - - - Select font - Выбрать шрифт - login - + Log in Вход - + Create Создать - + Profile name: Имя профиля: - + Load profile Загрузить профиль - + Use as default По умолчанию - + Load existing profile Загрузить профиль - + Create new profile Создать новый профиль - + toxygen toxygen @@ -1097,152 +904,114 @@ Version: Похоже, что этот профиль используется другим экземпляром Toxygen! Продолжить? - + Profile name Имя профиля - + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? Этот профиль используется другим экземпляром Toxygen или не был правильно закрыт. Продолжить? - + Do you want to set profile password? Хотите ли вы установить пароль профиля? - - - Do you want to save profile in default folder? If no, profile will be saved in program folder - Вы хотите сохранить профиль в папку по умолчанию? Если нет, профиль будет сохранен в папке с программой - - - - Profile saving error! Does Toxygen have permission to write to this directory? - Ошибка сохранения профиля! Toxygen имеет разрешение на запись в данную папку? - - - - Update for Toxygen was found. Download and install it? - Обновление для Toxygen было найдено. Загрузить и установить его? - notificationsForm - + Notification settings Настройки уведомлений - + Enable notifications Включить уведомления - + Enable call's sound Включить звук звонка - + Enable sound notifications Включить звуковые уведомления - - - Notify about all messages in groups - Уведомлять обо всех сообщениях в группах - - - - pass - - - Enter password - Введите пароль - - - - Password: - Пароль: - - - - Incorrect password - Неверный пароль - privacySettings - + Privacy settings Настройки приватности - + Save chat history Сохранять историю переписки - + Allow file auto accept Разрешить автополучение файлов - + Send typing notifications Посылать уведомления о наборе текста - + Auto accept default path: Путь автоприема файлов: - + Change Изменить - + Allow inlines Разрешать инлайны - + Chat history История чата - + History will be cleaned! Continue? История переписки будет очищена! Продолжить? - + Blocked users: Заблокированные пользователи: - + Unblock Разблокировать - + Block user Заблокировать пользователя - + Add to friend list Добавить в список друзей - + Do you want to add this user to friend list? Добавить этого пользователя в список друзей? @@ -1252,12 +1021,12 @@ Version: Блокировать по TOX ID: - + Block by public key: Блокировать по публичному ключу: - + Save unsent messages only Сохранять только неотправленные сообщения @@ -1265,115 +1034,34 @@ Version: tray - + Open Toxygen Открыть Toxygen - + Exit Выход - + Set status Изменить статус - + Online Онлайн - + Away Нет на месте - + Busy Занят - - updateSettingsForm - - - Update settings - Обновить настройки - - - - Select update mode: - Выбрать режим обновлений: - - - - Update Toxygen - Обновить Toxygen - - - - Disabled - Отключены - - - - Manual - Вручную - - - - Auto - Автоматически - - - - Error - Ошибка - - - - Problems with internet connection - Проблемы с соединением - - - - Updater not found - Апдейтер не был найден - - - - No updates found - Обновления не найдены - - - - Toxygen is up to date - Toxygen уже обновлен - - - - videoSettingsForm - - - Video settings - Настройки видео - - - - Device: - Устройство: - - - - Desktop - Рабочий стол - - - - Select region - Выберите область - - diff --git a/toxygen/translations/uk_UA.qm b/toxygen/translations/uk_UA.qm deleted file mode 100644 index a4082ef..0000000 Binary files a/toxygen/translations/uk_UA.qm and /dev/null differ diff --git a/toxygen/translations/uk_UA.ts b/toxygen/translations/uk_UA.ts deleted file mode 100644 index c36ecf0..0000000 --- a/toxygen/translations/uk_UA.ts +++ /dev/null @@ -1,1297 +0,0 @@ - - - - AddContact - - - TOX ID: - TOX ID: - - - - Add contact - Додати контакт - - - - Message: - Повідомлення: - - - - TOX ID or public key of contact - - - - - Callback - - - File from - - - - - Form - - - IP: - IP: - - - - UDP - UDP - - - - HTTP - HTTP - - - - IPv6 - IPv6 - - - - Port: - Порт: - - - - Proxy - Проксі - - - - Online contacts - Контактів онлайн - - - - Send request - Відправити запит - - - - WARNING: -using proxy with enabled UDP -can produce IP leak - - - - - Download nodes list from tox.chat - - - - - MainWindow - - - About program - Про проґраму - - - - Friend request - Запит дружби - - - - About - Про - - - - Audio - Звук - - - - Friend added - Друга додано - - - - Send file - Надіслати файл - - - - User {} wants to add you to contact list. Message: -{} - Користувач {} хоче додати вас до списку контактів. Повідомлення -{} - - - - Network - Мережа - - - - Clear history - Очистити журнал - - - - Copy public key - Копіювати публічний ключ - - - - Send message - Надіслати повідомлення - - - - Set alias - Встановити скорочення - - - - Privacy - Приватність - - - - Profile - Профіль - - - - Toxygen is Tox client written on Python. -Version: - Toxygen — це клієнт Tox написаний на Python. -Версія: - - - - Choose file - Обрати файл - - - - Enter new alias for friend {} or leave empty to use friend's name: - Введіть нове скорочення для друга {} або залишіть порожнім, щоб використовувати його псевдо: - - - - Add contact - Додати контакт - - - - Friend added without sending friend request - Друга додано без надсилання запиту дружби - - - - Interface - Зовнішній вигляд - - - - Settings - Налаштування - - - - Notifications - Сповіщення - - - - Remove friend - Вилучити друга - - - - Find contact - Знайти контакт - - - - Choose folder - Обрати теку - - - - Allow auto accept - Дозволити автоприймання - - - - Disallow auto accept - Заборонити автоприймання - - - - Start audio call with friend - Почати звуковий дзвінок - - - - Send screenshot - Надіслати знімок екрану - - - - Error - - - - - Profile with this name already exists - - - - - User {} is now known as {} - - - - - Choose folder with sticker pack - - - - - Choose folder with smiley pack - - - - - Quote selected text - - - - - Plugins - - - - - Delete message - - - - - Lock - - - - - List of plugins - - - - - Video - - - - - Updates - - - - - Search - - - - - All - - - - - Online - - - - - Online first - - - - - Name - - - - - Online and by name - - - - - Online first and by name - - - - - Import plugin - - - - - Reload plugins - - - - - Choose folder with plugin - - - - - Restart Toxygen - - - - - Plugin will be loaded after restart - - - - - Cannot lock app - - - - - Error. Profile password is not set. - - - - - Chat history - Журнал бесіди - - - - Export as text - - - - - Export as HTML - - - - - Copy - - - - - Status message - - - - - Public key - - - - - Block friend - - - - - Notes - - - - - Notes about user - - - - - Copy link location - - - - - Select all - - - - - Delete - - - - - Paste - - - - - Cut - - - - - Undo - - - - - Redo - - - - - Save - - - - - Text "{}" was not found - - - - - Not found - - - - - User {} invites you to group chat. Accept? - - - - - Group chat invite - - - - - {} users in chat - - - - - Enter new title for group {}: - - - - - Set title - - - - - Create group chat - - - - - Invite to group chat - - - - - Leave chat - - - - - MenuWindow - - - Send screenshot - Надіслати знімок екрану - - - - Send file - Надіслати файл - - - - Add smiley - - - - - Send sticker - - - - - NetworkSettings - - - Network settings - Налаштування мережі - - - - Restart TOX core - Перезапустити ядро Tox - - - - PasswordScreen - - - Profile password - - - - - Password (at least 8 symbols) - - - - - Confirm password - - - - - Set password - - - - - Passwords do not match - - - - - There is no way to recover lost passwords - - - - - Password must be at least 8 symbols - - - - - PluginWindow - - - List of commands for plugin {} - - - - - No commands available - - - - - PluginsForm - - - Plugins - - - - - Open selected plugin - - - - - No GUI found for this plugin - - - - - Error - - - - - No description available - - - - - Disable plugin - - - - - Enable plugin - - - - - No plugins found - - - - - ProfileSettingsForm - - - Name: - Псевдо: - - - - Profile settings - Налаштування профілю - - - - Reset avatar - Скинути аватар - - - - New NoSpam - Новий NoSpam - - - - Copy TOX ID - Копіювати TOX ID - - - - New avatar - Новий аватар - - - - Export profile - Експортувати профіль - - - - TOX ID: - TOX ID: - - - - Status: - Статус: - - - - Profile password - - - - - Password (at least 8 symbols) - - - - - Confirm password - - - - - Set password - - - - - Passwords do not match - - - - - Leaving blank will reset current password - - - - - There is no way to recover lost passwords - - - - - Online - - - - - Away - - - - - Busy - - - - - Copy public key - Копіювати публічний ключ - - - - Mark as not default profile - - - - - Mark as default profile - - - - - Password must be at least 8 symbols - - - - - Choose avatar - - - - - Use new path - - - - - Do you want to move your profile to this location? - - - - - WelcomeScreen - - - Don't show again - - - - - Tip of the day - - - - - Press Esc if you want hide app to tray. - - - - - Right click on screenshot button hides app to tray during screenshot. - - - - - You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> - - - - - Use Settings -> Interface to customize interface. - - - - - Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. - - - - - Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> - - - - - Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. - - - - - Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu - - - - - Use right click on inline image to save it - - - - - Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. - - - - - New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes - - - - - audioSettingsForm - - - Output device: - Пристрій виводу: - - - - Audio settings - Налаштування звуку - - - - Input device: - Пристрій вводу: - - - - incoming_call - - - Incoming video call - Вхідний відеодзвінок - - - - Incoming audio call - Вхідний аудіодзвінок - - - - Outgoing video call - - - - - Outgoing audio call - - - - - Call declined - - - - - Call finished - - - - - interfaceForm - - - Language: - Мова: - - - - Theme: - Тема: - - - - Interface settings - Налаштування зовнішнього вигляду - - - - Show avatars in chat - - - - - Smileys - - - - - Smiley pack: - - - - - Mirror mode - - - - - Messages font size: - - - - - Select unread messages notification color - - - - - Compact contact list - - - - - Import smiley pack - - - - - Import sticker pack - - - - - Close to tray - - - - - Select font - - - - - Restart app to apply settings - - - - - Restart required - - - - - login - - - Profile name: - Псевдо профілю: - - - - Load profile - Завантажити профіль - - - - Use as default - За замовчуванням - - - - Create new profile - Створити новий профіль - - - - Create - Створити - - - - Log in - Увійти - - - - Load existing profile - Завантажити існуючий - - - - toxygen - toxygen - - - - Looks like other instance of Toxygen uses this profile! Continue? - Схоже, що інша копія Toxygenʼу використовує цей профіль! Продовжити? - - - - Do you want to set profile password? - - - - - Do you want to save profile in default folder? If no, profile will be saved in program folder - - - - - Profile saving error! Does Toxygen have permission to write to this directory? - - - - - Other instance of Toxygen uses this profile or profile was not properly closed. Continue? - - - - - Update for Toxygen was found. Download and install it? - - - - - Profile name - - - - - notificationsForm - - - Enable sound notifications - Увімкнути звукові сповіщення - - - - Enable notifications - Увімкнути сповіщення - - - - Notification settings - Налаштування сповіщень - - - - Enable call's sound - Увімкнути звук дзвінка - - - - Notify about all messages in groups - - - - - pass - - - Enter password - - - - - Password: - - - - - Incorrect password - - - - - privacySettings - - - Privacy settings - Налаштування приватності - - - - Add to friend list - Додати до списку друзів - - - - Block by TOX ID: - Блокувати по TOX ID: - - - - Blocked users: - Блоковані користувачі: - - - - Change - Змінити - - - - Send typing notifications - Надсилати сповіщення про те, що я друкую - - - - Allow file auto accept - Дозволити автоприймання файлів - - - - Allow inlines - Дозволити інлайни - - - - Save chat history - Зберігати журнал бесіди - - - - Block user - Блокувати користувача - - - - Chat history - Журнал бесіди - - - - Unblock - Розблокувати - - - - History will be cleaned! Continue? - Журнал буде очищено! Продовжити? - - - - Auto accept default path: - Шлях за замовчуванням для автоприймання: - - - - Do you want to add this user to friend list? - Ви хочете додати цього користувача у список друзів? - - - - Block by public key: - - - - - Save unsent messages only - - - - - tray - - - Exit - Вихід - - - - Open Toxygen - Відкрити Toxygen - - - - Set status - - - - - Online - - - - - Away - - - - - Busy - - - - - updateSettingsForm - - - Update settings - - - - - Select update mode: - - - - - Update Toxygen - - - - - Disabled - - - - - Manual - - - - - Auto - - - - - Error - - - - - Problems with internet connection - - - - - Updater not found - - - - - No updates found - - - - - Toxygen is up to date - - - - - videoSettingsForm - - - Video settings - - - - - Device: - - - - - Desktop - - - - - Select region - - - - diff --git a/toxygen/ui/__init__.py b/toxygen/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/ui/av_widgets.py b/toxygen/ui/av_widgets.py deleted file mode 100644 index e750231..0000000 --- a/toxygen/ui/av_widgets.py +++ /dev/null @@ -1,182 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import logging -import threading -import wave - -from qtpy import QtCore, QtGui, QtWidgets - -from ui import widgets -import utils.util as util -import toxygen_wrapper.tests.support_testing as ts -with ts.ignoreStderr(): - import pyaudio - -global LOG -LOG = logging.getLogger('app.'+__name__) - -class IncomingCallWidget(widgets.CenteredWidget): - - 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) - self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) - self.avatar_label.setScaledContents(False) - self.name = widgets.DataLabel(self) - self.name.setGeometry(QtCore.QRect(90, 20, 300, 25)) - self._friend_number = friend_number - font = QtGui.QFont() - font.setFamily(settings['font']) - font.setPointSize(16) - font.setBold(True) - self.name.setFont(font) - self.call_type = widgets.DataLabel(self) - self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25)) - self.call_type.setFont(font) - self.accept_audio = QtWidgets.QPushButton(self) - self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150)) - self.accept_video = QtWidgets.QPushButton(self) - 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.join_path(util.get_images_directory(), 'accept_audio.png')) - icon = QtGui.QIcon(pixmap) - self.accept_audio.setIcon(icon) - 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.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)) - self.accept_video.setIconSize(QtCore.QSize(140, 140)) - self.decline.setIconSize(QtCore.QSize(140, 140)) - #self.accept_audio.setStyleSheet("QPushButton { border: none }") - #self.accept_video.setStyleSheet("QPushButton { border: none }") - #self.decline.setStyleSheet("QPushButton { border: none }") - self.setWindowTitle(text) - self.name.setText(name) - self.call_type.setText(text) - self._processing = False - self.accept_audio.clicked.connect(self.accept_call_with_audio) - self.accept_video.clicked.connect(self.accept_call_with_video) - self.decline.clicked.connect(self.decline_call) - - output_device_index = self._settings._oArgs.audio['output'] - - if self._settings['calls_sound']: - class SoundPlay(QtCore.QThread): - - def __init__(self): - QtCore.QThread.__init__(self) - self.a = None - - def run(self): - class AudioFile: - chunk = 1024 - - def __init__(self, fl): - self.stop = False - self.fl = fl - self.wf = wave.open(self.fl, 'rb') - self.p = pyaudio.PyAudio() - self.stream = self.p.open( - format=self.p.get_format_from_width(self.wf.getsampwidth()), - channels=self.wf.getnchannels(), - rate=self.wf.getframerate(), - # why no device? - output_device_index=output_device_index, - output=True) - - def play(self): - while not self.stop: - data = self.wf.readframes(self.chunk) - # dunno - if not data: break - while data and not self.stop: - self.stream.write(data) - data = self.wf.readframes(self.chunk) - self.wf = wave.open(self.fl, 'rb') - - def close(self): - try: - self.stream.close() - self.p.terminate() - except Exception as e: - # malloc_consolidate(): unaligned fastbin chunk detected - LOG.warn("SoundPlay close exception {e}") - - self.a = AudioFile(util.join_path(util.get_sounds_directory(), 'call.wav')) - self.a.play() - self.a.close() - - self.thread = SoundPlay() - self.thread.start() - else: - self.thread = None - - def stop(self): - LOG.debug(f"stop from friend_number={self._friend_number}") - if self._processing: - self.close() - if self.thread is not None: - self.thread.a.stop = True - i = 0 - while i < ts.iTHREAD_JOINS: - self.thread.wait(ts.iTHREAD_TIMEOUT) - if not self.thread.isRunning(): break - i = i + 1 - else: - LOG.warn(f"stop {self.thread.a} BLOCKED") - self.thread.a.stream.close() - self.thread.a.p.terminate() - self.thread.a.close() - # dunno -failsafe - self.thread.terminate() - #? dunno - self._processing = False - - def accept_call_with_audio(self): - if self._processing: - LOG.warn(f" accept_call_with_audio from {self._friend_number}") - return - LOG.debug(f" accept_call_with_audio from {self._friend_number}") - self._processing = True - try: - self._calls_manager.accept_call(self._friend_number, True, False) - finally: - #? self.stop() - LOG.debug(f" accept_call_with_audio NOT stop from={self._friend_number}") - pass - - def accept_call_with_video(self): - # ts.trepan_handler() - - if self._processing: - LOG.warn(f" accept_call_with_video from {self._friend_number}") - return - self.setWindowTitle('Answering video call') - self._processing = True - LOG.debug(f" accept_call_with_video from {self._friend_number}") - try: - self._calls_manager.accept_call(self._friend_number, True, True) - finally: - self.stop() - - def decline_call(self): - LOG.debug(f"decline_call from {self._friend_number}") - if self._processing: - return - self._processing = True - try: - self._calls_manager.stop_call(self._friend_number, False) - except Exception as e: - LOG.warn(f"decline_call from {self._friend_number} {e}") - finally: - self.stop() - - def set_pixmap(self, pixmap): - self.avatar_label.setPixmap(pixmap) diff --git a/toxygen/ui/contact_items.py b/toxygen/ui/contact_items.py deleted file mode 100644 index bdff447..0000000 --- a/toxygen/ui/contact_items.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from qtpy import QtCore, QtGui, QtWidgets -from toxygen_wrapper.toxcore_enums_and_consts import * - -from utils.util import * -from ui.widgets import DataLabel - - -class ContactItem(QtWidgets.QWidget): - """ - Contact in friends list - """ - - def __init__(self, settings, parent=None, kind='friend'): - 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.kind = DataLabel(self) - self.kind.setGeometry(QtCore.QRect(50 if mode else 75, 38 if mode else 48, 190, 15 if mode else 20)) - font.setBold(False) - font.setItalic(True) - self.kind.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 deleted file mode 100644 index f507a1d..0000000 --- a/toxygen/ui/create_profile_screen.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - -from ui.widgets import * -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 deleted file mode 100644 index 60f1e9e..0000000 --- a/toxygen/ui/group_bans_widgets.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic, QtWidgets, QtCore - -from ui.widgets import CenteredWidget -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): # pylint: disable=method-hidden - # FixMe broken - # self._cancel_ban(self._ban.ban_id) - pass - - def can_cancel_ban(self): # pylint: disable=method-hidden - # FixMe missing - pass - -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)) - pass - - 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 deleted file mode 100644 index 6b9fa9e..0000000 --- a/toxygen/ui/group_invites_widgets.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import logging -from qtpy import uic, QtWidgets - -import utils.util as util -from ui.widgets import * - -global LOG -LOG = logging.getLogger('app') - -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 - self._tox = self._groups_service._tox - - 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() - - if not nick: - nick = self._tox.self_get_name() - selected_invites = self._get_selected_invites() - for invite in selected_invites: - LOG.debug(f"_accept_invites {nick}") - 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: - LOG.debug(f"_groups_service.decline_group_invite") - 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 and 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 deleted file mode 100644 index ec8f95d..0000000 --- a/toxygen/ui/group_peers_list.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from ui.widgets import * -from toxygen_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(' *') - self.nameLabel.setText(name) - if peer.status == TOX_USER_STATUS['NONE']: - if peer.is_current_user: - style = 'QLabel {color: magenta}' - else: - style = 'QLabel {color: green}' - elif peer.status == TOX_USER_STATUS['AWAY']: - style = 'QLabel {color: blue}' - 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 deleted file mode 100644 index 5fd04d4..0000000 --- a/toxygen/ui/group_settings_widgets.py +++ /dev/null @@ -1,86 +0,0 @@ -from ui.widgets import CenteredWidget -from qtpy 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.deletePushButton.clicked.connect(self._delete) - 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.deletePushButton.setText(util_ui.tr('Delete')) - 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 _delete(self): - self._groups_service.leave_group(self._group.number) - self.close() - - def _disconnect(self): - self._groups_service.disconnect_from_group(self._group.number) - self.close() - - 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 deleted file mode 100644 index ceda0ef..0000000 --- a/toxygen/ui/groups_widgets.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - -import utils.util as util -from ui.widgets import * -from toxygen_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 deleted file mode 100644 index 530839c..0000000 --- a/toxygen/ui/items_factories.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 - -# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_handler.py", line 216, in transfer_finished -# self._file_transfers_message_service.add_inline_message(transfer, index) -# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_messages_service.py", line 47, in add_inline_message -# self._create_inline_item(transfer.data, count + index + 1) -# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_messages_service.py", line 75, in _create_inline_item -# return self._messages_items_factory.create_inline_item(data, False, position) -# File "/var/local/src/toxygen/toxygen/ui/items_factories.py", line 50, in create_inline_item -# item = InlineImageItem(message.data, self._messages.width(), elem, self._messages) -# AttributeError: 'bytes' object has no attribute 'data' - - def create_inline_item(self, message, append=True, position=0): - elem = QtWidgets.QListWidgetItem() - # AttributeError: 'bytes' object has no attribute 'data' - if type(message) == bytes: - # was used - data = message - elif hasattr(message, 'data'): - # used - data = message.data - else: - # unreached - return None - item = InlineImageItem(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 deleted file mode 100644 index 93362fd..0000000 --- a/toxygen/ui/login_screen.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os.path - -from qtpy import uic - -from ui.widgets import * -import utils.util as util -import utils.ui as util_ui - -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 deleted file mode 100644 index f65a0af..0000000 --- a/toxygen/ui/main_screen.py +++ /dev/null @@ -1,1073 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import os -import traceback - -from qtpy import uic -from qtpy import QtCore, QtGui, QtWidgets -from qtpy.QtGui import (QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QFontMetrics) - -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 user_data.settings import Settings - -import logging -global LOG -LOG = logging.getLogger('app.'+'mains') - -iMAX = 70 - -try: - # https://github.com/pyqtconsole/pyqtconsole - from pyqtconsole.console import PythonConsole - import pyqtconsole.highlighter as hl -except Exception as e: - LOG.warn(e) - PythonConsole = None -else: - if True: - # I want to do reverse video but I cant figure how - bg='white' - def hl_format(color, style=''): - """Return a QTextCharFormat with the given attributes. - """ - _color = QColor() - _color.setNamedColor(color) - - _format = QTextCharFormat() - _format.setForeground(_color) - if 'bold' in style: - _format.setFontWeight(QFont.Bold) - if 'italic' in style: - _format.setFontItalic(True) - - _bgcolor = QColor() - _bgcolor.setNamedColor(bg) - _format.setBackground(_bgcolor) - return _format - - aFORMATS = { - 'keyword': hl_format('blue', 'bold'), - 'operator': hl_format('red'), - 'brace': hl_format('darkGray'), - 'defclass': hl_format('black', 'bold'), - 'string': hl_format('magenta'), - 'string2': hl_format('darkMagenta'), - 'comment': hl_format('darkGreen', 'italic'), - 'self': hl_format('black', 'italic'), - 'numbers': hl_format('brown'), - 'inprompt': hl_format('darkBlue', 'bold'), - 'outprompt': hl_format('darkRed', 'bold'), - } - else: - bg = 'black' - def hl_format(color, style=''): - - """Return a QTextCharFormat with the given attributes. - unused - """ - _color = QColor() - _color.setNamedColor(color) - - _format = QTextCharFormat() - _format.setForeground(_color) - if 'bold' in style: - _format.setFontWeight(QFont.Bold) - if 'italic' in style: - _format.setFontItalic(True) - - _bgcolor = QColor() - _bgcolor.setNamedColor(bg) - _format.setBackground(_bgcolor) - return _format - aFORMATS = { - 'keyword': hl_format('blue', 'bold'), - 'operator': hl_format('red'), - 'brace': hl_format('lightGray'), - 'defclass': hl_format('white', 'bold'), - 'string': hl_format('magenta'), - 'string2': hl_format('lightMagenta'), - 'comment': hl_format('lightGreen', 'italic'), - 'self': hl_format('white', 'italic'), - 'numbers': hl_format('lightBrown'), - 'inprompt': hl_format('lightBlue', 'bold'), - 'outprompt': hl_format('lightRed', 'bold'), - } - -class QTextEditLogger(logging.Handler): - def __init__(self, parent, app): - super().__init__() - self.widget = QtWidgets.QPlainTextEdit(parent) - self.widget.setReadOnly(True) - - if app and app._settings: - size = app._settings['message_font_size'] - font_name = app._settings['font'] - else: - size = 12 - font_name = "Courier New" - font = QtGui.QFont(font_name, size, QtGui.QFont.Bold) - self.widget.setFont(font) - - def emit(self, record): - msg = self.format(record) - self.widget.appendPlainText(msg) - - -class LogDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit): - - def __init__(self, parent=None, app=None): - global iMAX - super().__init__(parent) - - logTextBox = QTextEditLogger(self, app) - # You can format what is printed to text box - %(levelname)s - logTextBox.setFormatter(logging.Formatter('%(name)s %(asctime)-4s - %(message)s')) - logTextBox.setLevel(app._args.loglevel) - logging.getLogger().addHandler(logTextBox) - - self._button = QtWidgets.QPushButton(self) - self._button.setText('Copy All') - self._logTextBox = logTextBox - - layout = QtWidgets.QVBoxLayout() - # Add the new logging box widget to the layout - layout.addWidget(logTextBox.widget) - layout.addWidget(self._button) - self.setLayout(layout) - settings = Settings.get_default_settings(app._args) - #self.setBaseSize( - self.resize(min(iMAX * settings['message_font_size'], parent.width()), 350) - - # Connect signal to slot - self._button.clicked.connect(self.test) - - def test(self): - # FixMe: 65:8: E1101: Instance of 'QTextEditLogger' has no 'selectAll' member (no-member) - # :66:8: E1101: Instance of 'QTextEditLogger' has no 'copy' member (no-member) - if hasattr(self._logTextBox, 'selectAll'): - self._logTextBox.selectAll() - self._logTextBox.copy() - -class MainWindow(QtWidgets.QMainWindow): - - def __init__(self, settings, tray, app): - super().__init__() - self._settings = settings - self._contacts_manager = None - self._tray = tray - self._app = app - self._tox = app._tox - self._widget_factory = None - self._modal_window = None - self._plugins_loader = None - self.setAcceptDrops(True) - self._saved = False - self._smiley_window = None - self._profile = None - self._toxes = None - 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() - global iMAX - if iMAX == 100: - # take a rough guess of 2/3 the default width at the default font - iMAX = settings['width'] * 2/3 / settings['message_font_size'] - self._me = LogDialog(self, app) - self._pe = None - self._we = None - - def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader, - file_transfer_handler, history_loader, calls_manager, groups_service, toxes, app): - 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._app = app - self._messenger = messenger - self._contacts_manager.active_contact_changed.add_callback(self._new_contact_selected) - self.messageEdit.set_dependencies(messenger, contacts_manager, file_transfer_handler) - - 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(True) # was False - self.menubar.setMinimumSize(self.width(), 250) - self.menubar.setMaximumSize(self.width(), 32) - self.menubar.setBaseSize(self.width(), 250) - - self.actionTest_tox = QtWidgets.QAction(window) - self.actionTest_tox.setObjectName("actionTest_tox") - self.actionTest_nmap = QtWidgets.QAction(window) - self.actionTest_nmap.setObjectName("actionTest_nmap") - self.actionTest_main = QtWidgets.QAction(window) - self.actionTest_main.setObjectName("actionTest_main") - self.actionQuit_program = QtWidgets.QAction(window) - self.actionQuit_program.setObjectName("actionQuit_program") - - 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) # alignment=QtCore.Qt.AlignRight - self.menuAbout.setObjectName("menuAbout") - - self.actionAdd_friend = QtWidgets.QAction(window) - self.actionAdd_friend.setObjectName("actionAdd_friend") - - self.actionProfile_settings = QtWidgets.QAction(window) - self.actionProfile_settings.setObjectName("actionProfile_settings") - 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.actionLog_console = QtWidgets.QAction(window) - self.actionLog_console.setObjectName("actionLog_console") - self.actionPython_console = QtWidgets.QAction(window) - self.actionPython_console.setObjectName("actionLog_console") - self.actionWeechat_console = QtWidgets.QAction(window) - self.actionWeechat_console.setObjectName("actionLog_console") - 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.reloadToxchat = 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.menuProfile.addAction(self.actionTest_tox) - self.menuProfile.addAction(self.actionTest_nmap) - self.menuProfile.addAction(self.actionTest_main) - self.menuProfile.addAction(self.actionQuit_program) - - self.menuGC.addAction(self.createGC) - self.menuGC.addAction(self.joinGC) - self.menuGC.addAction(self.gc_invites) - self.menuSettings.addAction(self.actionProfile_settings) - 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.menuPlugins.addAction(self.reloadToxchat) - self.menuPlugins.addAction(self.actionLog_console) - self.menuPlugins.addAction(self.actionPython_console) - self.menuPlugins.addAction(self.actionWeechat_console) - - 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.actionTest_nmap.triggered.connect(self.test_nmap) - self.actionTest_main.triggered.connect(self.test_main) - self.actionTest_tox.triggered.connect(self.test_tox) - - self.actionQuit_program.triggered.connect(self.quit_program) - self.actionAbout_program.triggered.connect(self.about_program) - self.actionLog_console.triggered.connect(self.log_console) - self.actionPython_console.triggered.connect(self.python_console) - self.actionWeechat_console.triggered.connect(self.weechat_console) - 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.actionProfile_settings.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.reloadToxchat.triggered.connect(self.reload_toxchat) - 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: - if hasattr(self, '_tray') and self._tray: - self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) - self.messages.repaint() - return super().event(event) - - def status(self, line): - """For now, this uses the unused space on the menubar line - It could be a status line at the bottom, or a statusline with history.""" - self.menuAbout.setTitle(line[:iMAX]) - return line - - def retranslateUi(self): - self.lockApp.setText(util_ui.tr("Lock")) - self.menuPlugins.setTitle(util_ui.tr("Plugins")) - 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.actionProfile_settings.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.actionLog_console.setText(util_ui.tr("Console Log")) - self.actionPython_console.setText(util_ui.tr("Python Console")) - self.actionWeechat_console.setText(util_ui.tr("Weechat Console")) - self.actionTest_tox.setText(util_ui.tr("Bootstrap")) - self.actionTest_nmap.setText(util_ui.tr("Test Nodes")) - self.actionTest_main.setText(util_ui.tr("Test Program")) - self.actionQuit_program.setText(util_ui.tr("Quit program")) - self.actionSettings.setText(util_ui.tr("Settings")) - self.audioSettings.setText(util_ui.tr("Audio")) - self.videoSettings.setText(util_ui.tr("Video")) - self.updateSettings.setText(util_ui.tr("Updates")) - self.importPlugin.setText(util_ui.tr("Import plugin")) - self.reloadPlugins.setText(util_ui.tr("Reload plugins")) - self.reloadToxchat.setText(util_ui.tr("Reload tox.chat")) - - self.searchLineEdit.setPlaceholderText(util_ui.tr("Search")) - self.sendMessageButton.setToolTip(util_ui.tr("Send message")) - 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")) - self.contactsFilterComboBox.addItem(util_ui.tr("Kind")) - - 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.setBold(True) - 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() -# LOG.debug(f"invites_button_visible={invites_button_visible}") - self.friends_list.setGeometry(0, 125 if invites_button_visible else 100, - 270, self.height() - 150 if invites_button_visible else self.height() - 125) - - 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 log_console(self): - self._me.show() - - def python_console(self): - if not PythonConsole: return - app = self._app - if app and app._settings: - size = app._settings['message_font_size'] - font_name = app._settings['font'] - else: - size = 12 - font_name = "Courier New" - - size = font_width = 10 - font_name = "DejaVu Sans Mono" - - try: - if not self._pe: - self._pe = PythonConsole(formats=aFORMATS) - self._pe.setWindowTitle('variable: app is the application') -# self._pe.edit.setStyleSheet('foreground: white; background-color: black;}') - # Fix the pyconsole geometry - - font = self._pe.edit.document().defaultFont() - font.setFamily(font_name) - font.setBold(True) - if font_width is None: - font_width = QFontMetrics(font).width('M') - self._pe.setFont(font) - geometry = self._pe.geometry() - geometry.setWidth(int(font_width*50+20)) - geometry.setHeight(int(font_width*24*13/8)) - self._pe.setGeometry(geometry) - self._pe.resize(int(font_width*50+20), int(font_width*24*13/8)) - - self._pe.show() - self._pe.eval_queued() - # or self._pe.eval_in_thread() - return - except Exception as e: - LOG.warn(f"python_console EXCEPTION {e}") - - def weechat_console(self): - if self._we: - self._we.show() - return - try: - from qweechat import qweechat - from qweechat.config import write - LOG.info("Loading WeechatConsole") - except ImportError as e: - LOG.error(f"ImportError Loading import qweechat {e} {sys.path}") - LOG.debug(traceback.print_exc()) - text = f"ImportError Loading import qweechat {e} {sys.path}" - title = util_ui.tr('Error importing qweechat') - util_ui.message_box(text, title) - return - - try: - # WeeChat backported from PySide6 to PyQt5 - LOG.info("Adding WeechatConsole") - class WeechatConsole(qweechat.MainWindow): - def __init__(self, *args): - qweechat.MainWindow.__init__(self, *args) - - def closeEvent(self, event): - """Called when QWeeChat window is closed.""" - self.network.disconnect_weechat() - if self.network.debug_dialog: - self.network.debug_dialog.close() - write(self.config) - except Exception as e: - LOG.exception(f"ERROR WeechatConsole {e}") - MainWindow = None - return - app = self._app - if app and app._settings: - size = app._settings['message_font_size'] - font_name = app._settings['font'] - else: - size = 12 - font_name = "Courier New" - - font_name = "DejaVu Sans Mono" - - try: - LOG.info("Creating WeechatConsole") - self._we = WeechatConsole() - self._we.show() - self._we.setWindowTitle('File/Connect to 127.0.0.1:9000') - # Fix the pyconsole geometry - try: - font = self._we.buffers[0].widget.chat.defaultFont() - font.setFamily(font_name) - font.setBold(True) - if font_width is None: - font_width = QFontMetrics(font).width('M') - self._we.setFont(font) - except Exception as e: -# LOG.debug(e) - font_width = size - geometry = self._we.geometry() - # make this configable? - geometry.setWidth(int(font_width*70)) - geometry.setHeight(int(font_width*(2+24)*11/8)) - self._we.setGeometry(geometry) - #? QtCore.QSize() - self._we.resize(int(font_width*80+20), int(font_width*(2+24)*11/8)) - - self._we.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred, - QtWidgets.QSizePolicy.Preferred) - self._we.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) - - LOG.info("Showing WeechatConsole") - self._we.show() - # or self._we.eval_in_thread() - return - except Exception as e: - LOG.exception(f"Error creating WeechatConsole {e}") - - def about_program(self): - # TODO: replace with window - text = util_ui.tr('Toxygen is Tox client written in Python.\nVersion: ') - text += '' + '\nGitHub: https://git.plastiras.org/emdee/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 hasattr(self, '_plugin_loader') and self._plugin_loader is not None: - self._plugin_loader.reload() - - def reload_toxchat(self): - pass - - @staticmethod - def import_plugin(): - directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugins')) - if directory and os.path.isdir(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 test_tox(self): - self._app._test_tox() - - def test_nmap(self): - self._app._test_nmap() - - def test_main(self): - self._app._test_main() - - def quit_program(self): - try: - self.close_window() - self._app._stop_app() - except KeyboardInterrupt: - pass - sys.stderr.write('sys.exit' +'\n') - # unreached? - sys.exit(0) - - def show_menu(self): - if not hasattr(self, 'menu'): - self.menu = DropdownMenu(self) - 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(): - #? - self.search_field.show() - return - if not hasattr(self._contacts_manager, 'get_curr_friend') or \ - self._contacts_manager.get_curr_friend() is None: - #? return - pass - 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 - LOG.debug(f"update_gc_invites_button_state invites_count={invites_count}") - - # Fixme - self.groupInvitesPushButton.setVisible(True) # invites_count > 0 - text = util_ui.tr(f'{invites_count} new invites to group chats') - self.groupInvitesPushButton.setText(text) - self.resizeEvent() diff --git a/toxygen/ui/main_screen_widgets.py b/toxygen/ui/main_screen_widgets.py deleted file mode 100644 index 064cb09..0000000 --- a/toxygen/ui/main_screen_widgets.py +++ /dev/null @@ -1,512 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import urllib -import re - -from qtpy import QtCore, QtGui, QtWidgets -from qtpy.QtCore import Signal - -from ui.widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit -import utils.util as util -import utils.ui as util_ui -from stickers.stickers import load_stickers - -import logging -LOG = logging.getLogger('app.'+'msw') - -class MessageArea(QtWidgets.QPlainTextEdit): - """User types messages here""" - - def __init__(self, parent, form): - super().__init__(parent) - self._messenger = None - 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._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): - mimeData = QtWidgets.QApplication.clipboard().mimeData() - if mimeData.hasUrls(): - for url in mimeData.urls(): - self.pasteEvent(url.toString()) - else: - self.pasteEvent() - - elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): - modifiers = event.modifiers() - if modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier: - self.insertPlainText('\n') - else: - if self._timer.isActive(): - self._timer.stop() - try: - self._messenger.send_typing(False) - self._messenger.send_message() - except Exception as e: - LOG.error(f"keyPressEvent ERROR send_message to {self._messenger}") - util_ui.message_box(str(e), - util_ui.tr(f"keyPressEvent ERROR send_message to {self._messenger}")) - - elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): - 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() - text_cursor = self.textCursor() - pos = text_cursor.position() - current_word = re.split(r"\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._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()) - menu.exec_(event.globalPos()) - del menu - - def dragEnterEvent(self, e): - e.accept() - - def dragMoveEvent(self, e): - e.accept() - - def dropEvent(self, e): - if e.mimeData().hasFormat('text/plain') or e.mimeData().hasFormat('text/html'): - e.accept() - self.pasteEvent(e.mimeData().text()) - elif e.mimeData().hasUrls(): - for url in e.mimeData().urls(): - self.pasteEvent(url.toString()) - e.accept() - else: - e.ignore() - - def pasteEvent(self, text=None): - text = text or QtWidgets.QApplication.clipboard().text() - if text.startswith('file://'): - 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) - - @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 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() - - def mouseReleaseEvent(self, event): - if self.rubberband.isVisible(): - self.rubberband.hide() - rect = self.rubberband.geometry() - if rect.width() and rect.height(): - screen = QtWidgets.QApplication.primaryScreen() - p = screen.grabWindow(0, - rect.x() + 4, - rect.y() + 4, - rect.width() - 8, - rect.height() - 8) - byte_array = QtCore.QByteArray() - buffer = QtCore.QBuffer(byte_array) - buffer.open(QtCore.QIODevice.WriteOnly) - p.save(buffer, 'PNG') - friend = self._contacts_manager.get_curr_contact() - self._file_transfer_handler.send_screenshot(bytes(byte_array.data()), friend.number) - self.close() - - -class SmileyWindow(QtWidgets.QWidget): - """ - Smiley selection window - """ - - def __init__(self, parent, smiley_loader): - super().__init__(parent) - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - 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 - else: - 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, 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): # 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 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().__init__(parent) - self.enter = enter - - def enterEvent(self, event): - self.enter() - super().enterEvent(event) - - -class DropdownMenu(QtWidgets.QWidget): - - def __init__(self, parent): - super().__init__(parent) - self.installEventFilter(self) - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - self.setMaximumSize(120, 120) - self.setMinimumSize(120, 120) - self.screenshotButton = QRightClickButton(self) - self.screenshotButton.setGeometry(QtCore.QRect(0, 60, 60, 60)) - self.screenshotButton.setObjectName("screenshotButton") - - self.fileTransferButton = QtWidgets.QPushButton(self) - self.fileTransferButton.setGeometry(QtCore.QRect(60, 60, 60, 60)) - self.fileTransferButton.setObjectName("fileTransferButton") - - self.smileyButton = QtWidgets.QPushButton(self) - self.smileyButton.setGeometry(QtCore.QRect(0, 0, 60, 60)) - - self.stickerButton = QtWidgets.QPushButton(self) - self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60)) - - 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.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.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.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(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) - self.screenshotButton.rightClicked.connect(lambda: parent.send_screenshot(True)) - self.smileyButton.clicked.connect(parent.send_smiley) - self.stickerButton.clicked.connect(parent.send_sticker) - - def leaveEvent(self, event): - self.close() - - def eventFilter(self, obj, event): - if event.type() == QtCore.QEvent.WindowDeactivate: - self.close() - return False - - -class StickerItem(QtWidgets.QWidget): - - def __init__(self, fl): - super().__init__() - self._image_label = QtWidgets.QLabel(self) - self.path = fl - self.pixmap = QtGui.QPixmap() - self.pixmap.load(fl) - if self.pixmap.width() > 150: - self.pixmap = self.pixmap.scaled(150, 200, QtCore.Qt.KeepAspectRatio) - self.setFixedSize(150, self.pixmap.height()) - self._image_label.setPixmap(self.pixmap) - - -class StickerWindow(QtWidgets.QWidget): - """Sticker selection window""" - - 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._stickers = load_stickers() - for sticker in self._stickers: - item = StickerItem(sticker) - elem = QtWidgets.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(250, item.height())) - self.list.addItem(elem) - self.list.setItemWidget(elem, item) - self.list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.list.setSpacing(3) - self.list.clicked.connect(self.click) - - def click(self, index): - num = index.row() - friend = self._contacts_manager.get_curr_contact() - self._file_transfer_handler.send_sticker(self._stickers[num], friend.number) - self.close() - - def leaveEvent(self, event): - self.close() - - -class WelcomeScreen(CenteredWidget): - - def __init__(self, settings): - super().__init__() - self._settings = settings - self.setMaximumSize(250, 200) - self.setMinimumSize(250, 200) - self.center() - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.text = QtWidgets.QTextBrowser(self) - self.text.setGeometry(QtCore.QRect(0, 0, 250, 170)) - self.text.setOpenExternalLinks(True) - self.checkbox = QtWidgets.QCheckBox(self) - self.checkbox.setGeometry(QtCore.QRect(5, 170, 240, 30)) - 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 = util_ui.tr('Press Esc if you want hide app to tray.') - elif num == 1: - text = util_ui.tr('Right click on screenshot button hides app to tray during screenshot.') - elif num == 2: - text = util_ui.tr('You can use Tox over Tor. For more info read this post') - elif num == 3: - text = util_ui.tr('Use Settings -> Interface to customize interface.') - elif num == 4: - text = util_ui.tr('Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.') - elif num == 5: - text = util_ui.tr('Since v0.1.3 Toxygen supports plugins. Read more') - elif num == 6: - 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 = util_ui.tr('New in Toxygen 0.4.1:
Downloading nodes from tox.chat
Bug fixes') - elif num == 8: - 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 = util_ui.tr( 'Use right click on inline image to save it') - else: - 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): - self._settings['show_welcome_screen'] = False - self._settings.save() - - -class MainMenuButton(QtWidgets.QPushButton): - - def __init__(self, *args): - super().__init__(*args) - self.setObjectName("mainmenubutton") - - def setText(self, text): - metrics = QtGui.QFontMetrics(self.font()) - self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20) - super().setText(text) - - -class ClickableLabel(QtWidgets.QLabel): - # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' - clicked = Signal() - - def __init__(self, *args): - super().__init__(*args) - - def mouseReleaseEvent(self, ev): - self.clicked.emit() - - -class SearchScreen(QtWidgets.QWidget): - - 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 - - self.search_text = LineEdit(self) - self.search_text.setGeometry(0, 0, width - 160, 40) - - self.search_button = ClickableLabel(self) - self.search_button.setGeometry(width - 160, 0, 40, 40) - pixmap = QtGui.QPixmap() - 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) - self.search_button.clicked.connect(self.search) - - font = QtGui.QFont() - font.setPointSize(32) - font.setBold(True) - - self.prev_button = QtWidgets.QPushButton(self) - self.prev_button.setGeometry(width - 120, 0, 40, 40) - self.prev_button.clicked.connect(self.prev) - self.prev_button.setText('\u25B2') - - self.next_button = QtWidgets.QPushButton(self) - self.next_button.setGeometry(width - 80, 0, 40, 40) - self.next_button.clicked.connect(self.next) - self.next_button.setText('\u25BC') - - self.close_button = QtWidgets.QPushButton(self) - self.close_button.setGeometry(width - 40, 0, 40, 40) - self.close_button.clicked.connect(self.close) - self.close_button.setText('×') - self.close_button.setFont(font) - - font.setPointSize(18) - self.next_button.setFont(font) - self.prev_button.setFont(font) - - self.retranslateUi() - - def retranslateUi(self): - self.search_text.setPlaceholderText(util_ui.tr('Search')) - - def show(self): - super().show() - self.search_text.setFocus() - - def search(self): - self._contacts_manager.update() - text = self.search_text.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): - contact = self._contacts_manager.get_curr_contact() - if contact is not None: - index = contact.search_prev() - self.load_messages(index) - - def next(self): - contact = self._contacts_manager.get_curr_contact() - text = self.search_text.text() - if contact is not None: - index = contact.search_next() - if index is not None: - count = self._messages.count() - index += count - item = self._messages.item(index) - self._messages.scrollToItem(item) - self._messages.itemWidget(item).select_text(text) - else: - self.not_found(text) - - def load_messages(self, index): - text = self.search_text.text() - if index is not None: - count = self._messages.count() - while count + index < 0: - self._history_loader.load_history() - count = self._messages.count() - index += count - item = self._messages.item(index) - self._messages.scrollToItem(item) - self._messages.itemWidget(item).select_text(text) - else: - self.not_found(text) - - def closeEvent(self, *args): - self._messages.setGeometry(0, 0, self._messages.width(), self._messages.height() + 40) - super().closeEvent(*args) - - @staticmethod - def not_found(text): - 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 deleted file mode 100644 index 9f0fc05..0000000 --- a/toxygen/ui/menu.py +++ /dev/null @@ -1,804 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from qtpy import QtCore, QtGui, QtWidgets, uic - -import toxygen_wrapper.tests.support_testing as ts -with ts.ignoreStderr(): # not out - import pyaudio - -from user_data.settings import * -from utils.util import * -from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow -import updater.updater as updater -import utils.ui as util_ui -from user_data import settings - -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) - -global oPYA -oPYA = pyaudio.PyAudio() -class AddContact(CenteredWidget): - """Add contact form""" - - def __init__(self, dsettings, contacts_manager, tox_id=''): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - self._contacts_manager = contacts_manager - uic.loadUi(get_views_path('add_contact_screen'), self) - self._update_ui(tox_id) - self._adding = False - self._bootstrap = 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.addBootstrapPushButton.clicked.connect(self._add_bootstrap) - self._retranslate_ui() - - def _add_bootstrap(self): - if self._bootstrap: - return - self._bootstrap = True - - 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 - pass - elif send and type(send) == str: # print error data - self.errorLabel.setText(send) - self.close() - - 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')) - -# unfinished copy of addContact -class AddBootstrap(CenteredWidget): - """Add bootstrap form""" - - def __init__(self, dsettings, bootstraps_manager, tox_id=''): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - self._bootstraps_manager = bootstraps_manager - uic.loadUi(get_views_path('add_bootstrap_screen'), self) - self._update_ui(tox_id) - self._adding = False - self._bootstrap = 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 bootstrap list.')) - self.addBootstrapPushButton.clicked.connect(self._add_friend) - - # self.addBootstrapPushButton.clicked.connect(self._add_bootstrap) - self._retranslate_ui() - - def _add_bootstrap(self): - if self._bootstrap: - return - self._bootstrap = True - tox_id = self.toxIdLineEdit.text().strip() - if tox_id.startswith('tox:'): - tox_id = tox_id[4:] - message = self.messagePlainTextEdit.toPlainText() - send = self._bootstraps_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 bootstrap')) - self.addBootstrapPushButton.setText(util_ui.tr('Send request')) - self.toxIdLabel.setText(util_ui.tr('Port:')) - self.messageLabel.setText(util_ui.tr('Message:')) - self.toxIdLineEdit.setPlaceholderText(util_ui.tr('IP or hostname of public key of bootstrap')) - - -class NetworkSettings(CenteredWidget): - """Network settings form: UDP, Ipv6 and proxy""" - def __init__(self, dsettings, reset): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - 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.urlLineEdit = LineEdit(self) - self.urlLineEdit.setGeometry(100, 370, 270, 30) - - self.restartCorePushButton.clicked.connect(self._restart_core) - self.ipv6CheckBox.setChecked(self._settings['ipv6_enabled']) - self.udpCheckBox.setChecked(self._settings['udp_enabled']) - self.proxyCheckBox.setChecked(self._settings['proxy_type']) - self.ipLineEdit.setText(self._settings['proxy_host']) - self.portLineEdit.setText(str(self._settings['proxy_port'])) - self.urlLineEdit.setText(str(self._settings['download_nodes_url'])) - self.httpProxyRadioButton.setChecked(self._settings['proxy_type'] == 1) - self.socksProxyRadioButton.setChecked(self._settings['proxy_type'] != 1) - self.downloadNodesCheckBox.setChecked(self._settings['download_nodes_list']) - self.lanCheckBox.setChecked(self._settings['local_discovery_enabled']) - 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.urlLabel.setText(util_ui.tr("ChatUrl:")) - self.restartCorePushButton.setText(util_ui.tr("Restart TOX core")) - self.httpProxyRadioButton.setText(util_ui.tr("HTTP")) - self.socksProxyRadioButton.setText(util_ui.tr("Socks 5")) - self.downloadNodesCheckBox.setText(util_ui.tr("Download nodes list from tox.chat")) -# self.warningLabel.setText(util_ui.tr("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) - self.warningLabel.setText(util_ui.tr("Changing settings require 'Restart TOX core'")) - - 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_url'] = str(self.urlLineEdit.text()) - self._settings['download_nodes_list'] = self.downloadNodesCheckBox.isChecked() - self._settings['local_discovery_enabled'] = self.lanCheckBox.isChecked() - self._settings.save() - # recreate tox instance - self._reset() - self.close() - except Exception as ex: - LOG.error('ERROR: Exception in restart: ' + str(ex)) - - -class PrivacySettings(CenteredWidget): - """Privacy settings form: history, typing notifications""" - - def __init__(self, contacts_manager, dsettings): - """ - :type contacts_manager: ContactsManager - """ - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._contacts_manager = contacts_manager - self._settings = dsettings - 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, dsettings): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings # pylint: disable=undefined-variable - 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, dsettings, smiley_loader): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - 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(settings.built_in_themes().keys()) - self.themeComboBox.addItems(themes) - theme = self._settings['theme'] - if theme in 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) - - self._app_closing_setting = self._settings['close_app'] - self.closeRadioButton.setChecked(self._app_closing_setting == 0) - self.hideRadioButton.setChecked(self._app_closing_setting == 1) - self.closeToTrayRadioButton.setChecked(self._app_closing_setting == 2) - - self.compactModeCheckBox.setChecked(self._settings['compact_mode']) - self.showAvatarsCheckBox.setChecked(self._settings['show_avatars']) - 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): - - self._settings['theme'] = str(self.themeComboBox.currentText()) - try: - theme = self._settings['theme'] - styles_path = join_path(get_styles_directory(), settings.built_in_themes()[theme]) - with open(styles_path) as fl: - style = fl.read() - self._app.setStyleSheet(style) - except IsADirectoryError: - pass - - self._settings['smileys'] = self.smileysCheckBox.isChecked() - - restart = False -# if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): -# self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() -# restart = True - - if self._settings['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] - self._app.removeTranslator(self._app.translator) - self._app.translator.load(join_path(get_translations_directory(), path)) - self._app.installTranslator(self._app.translator) - - app_closing_setting = 0 - if self.hideRadioButton.isChecked(): - 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, dsettings): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - 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): - if 'audio' not in self._settings: - ex = f"self._settings=id(self._settings) {self._settings}" - LOG.warn('AudioSettings.closeEvent settings error: ' + str(ex)) - else: - self._settings['audio']['input'] = \ - self._in_indexes[self.inputDeviceComboBox.currentIndex()] - self._settings['audio']['output'] = \ - self._out_indexes[self.outputDeviceComboBox.currentIndex()] - self._settings.save() - - def _update_ui(self): - p = oPYA - 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) - try: - self.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings['audio']['input'])) - self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings['audio']['output'])) - except: pass - - self._retranslate_ui() - - def _retranslate_ui(self): - 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): - """ - Video calls settings form - """ - - def __init__(self, dsettings): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - 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: - # AttributeError: 'VideoSettings' object has no attribute 'devices' - # ERROR: Saving video settings error: 'VideoSettings' object has no attribute 'input' - index = self.deviceComboBox.currentIndex() - if index in self._devices: - self._settings['video']['device'] = self._devices[index] - else: - LOG.warn(f"{index} not in deviceComboBox self._devices {self._devices}") - text = self.resolutionComboBox.currentText() - if len(text.split(' ')[0]) > 1: - self._settings['video']['width'] = int(text.split(' ')[0]) - self._settings['video']['height'] = int(text.split(' ')[-1]) - self._settings.save() - except Exception as ex: - LOG.error('ERROR: Saving video settings error: ' + str(ex)) - - def save(self, x, y, width, height): - self.desktopAreaSelection = None - self._settings['video']['device'] = -1 - self._settings['video']['width'] = width - self._settings['video']['height'] = height - self._settings['video']['x'] = x - self._settings['video']['y'] = y - self._settings.save() - - def _update_ui(self): - try: - with ts.ignoreStdout(): import cv2 - except ImportError: - cv2 = None - 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) - with ts.ignoreStdout(): - # was range(10) - for i in map(int, ts.get_video_indexes()): - v = cv2.VideoCapture(i) # pylint: disable=no-member - if v.isOpened(): - v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) # pylint: disable=no-member - v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) # pylint: disable=no-member - - width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) # pylint: disable=no-member - height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT)) # pylint: disable=no-member - del v - self._devices.append(i) - self._frame_max_sizes.append((width, height)) - self.deviceComboBox.addItem(util_ui.tr('Device #') + str(i)) - - if 'device' not in self._settings['video']: - LOG.warn(f"'device' not in self._settings['video']: {self._settings}") - self._settings['video']['device'] = self._devices[-1] - iIndex = self._settings['video']['device'] - try: - index = self._devices.index(iIndex) - self.deviceComboBox.setCurrentIndex(index) - except Exception as e: - # off by one - what's Desktop? - se = f"Video devices index error: index={iIndex} {e}" - LOG.warn(se) - # util_ui.message_box(se, util_ui.tr(f"ERROR: Video devices error")) - self._settings['video']['device'] = self._devices[-1] - - 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(True) # 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._app = QtWidgets.QApplication.instance() - 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] # ['SearchPlugin', True, 'Description', 'srch'] - # key in self._plugins and hasattr(self._plugins[key], 'instance'): - window = self._plugin_loader.plugin_window(plugin[-1]) - if window is not None and not hasattr(window, 'show'): - LOG.error(util_ui.tr('ERROR: No show for the plugin: ' +repr(window) +' ' +repr(window))) - util_ui.message_box(util_ui.tr('ERROR: No show for the plugin ' +repr(window)), util_ui.tr('Error')) - elif window is not None: - try: - self._window = window - self._window.show() - except Exception as e: - LOG.error(util_ui.tr('ERROR: Error for the plugin: ' +repr(window) +' ' +str(e))) - util_ui.message_box(util_ui.tr('ERROR: Error for the plugin: ' +repr(window)), util_ui.tr('Error')) - elif window is None: - LOG.warn(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window')) - util_ui.message_box(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window'), util_ui.tr('Error')) - - def update_list(self): - self.comboBox.clear() - 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, dsettings, version): - super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings - 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/ui/messages_widgets.py b/toxygen/ui/messages_widgets.py deleted file mode 100644 index 2c0ddfb..0000000 --- a/toxygen/ui/messages_widgets.py +++ /dev/null @@ -1,463 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import html as h -import re - -from qtpy import QtCore, QtGui, QtWidgets - -from toxygen_wrapper.toxcore_enums_and_consts import * -import ui.widgets as widgets -import utils.util as util -import ui.menu as menu -from ui.widgets import * -from messenger.messages import MESSAGE_AUTHOR -from file_transfers.file_transfers import * - -class MessageBrowser(QtWidgets.QTextBrowser): - - 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) - self.document().setTextWidth(width) - self.setOpenExternalLinks(True) - self.setAcceptRichText(True) - self.setOpenLinks(False) - path = smileys_loader.get_smileys_path() - if path is not None: - self.setSearchPaths([path]) - self.document().setDefaultStyleSheet('a { color: #306EFF; }') - text = self.decoratedText(text) - if message_type != TOX_MESSAGE_TYPE['NORMAL']: - self.setHtml('

' + text + '

') - else: - self.setHtml(text) - font = QtGui.QFont() - font.setFamily(settings['font']) - font.setPixelSize(settings['message_font_size']) - font.setBold(False) - self.setFont(font) - try: - # was self.resize(width, self.document().size().height()) - # guessing QSize - self.resize(QtCore.QSize(width, int(self.document().size().height()))) - except TypeError as e: - # TypeError: arguments did not match any overloaded call: - # resize(self, a0: QSize): argument 1 has unexpected type 'int' - # resize(self, w: int, h: int): argument 2 has unexpected type 'float' - pass - - self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) - self.anchorClicked.connect(self.on_anchor_clicked) - - def contextMenuEvent(self, event): - 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: - 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 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:'): - self._add_contact = menu.AddContact(text[4:]) - self._add_contact.show() - else: - QtGui.QDesktopServices.openUrl(url) - self.clearFocus() - - def addAnimation(self, url, file_name): - movie = QtGui.QMovie(self) - movie.setFileName(file_name) - self.urls[movie] = url - # Value 'movie.frameChanged' is unsubscriptable - movie.frameChanged().connect(lambda x: self.animate(movie)) - movie.start() - - def animate(self, movie): - self.document().addResource(QtGui.QTextDocument.ImageResource, - self.urls[movie], - movie.currentPixmap()) - self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth()) - - def decoratedText(self, text): - text = h.escape(text) # replace < and > - exp = QtCore.QRegExp( - '(' - '(?:\\b)((www\\.)|(http[s]?|ftp)://)' - '\\w+\\S+)' - '|(?:\\b)(file:///)([\\S| ]*)' - '|(?:\\b)(tox:[a-zA-Z\\d]{76}$)' - '|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)' - '|(?:\\b)(tox:\\S+@\\S+)') - offset = exp.indexIn(text, 0) - while offset != -1: # add links - url = exp.cap() - if exp.cap(2) == 'www.': - html = '{0}'.format(url) - else: - html = '{0}'.format(url) - text = text[:offset] + html + text[offset + len(exp.cap()):] - offset += len(html) - offset = exp.indexIn(text, offset) - arr = text.split('\n') - for i in range(len(arr)): # quotes - if arr[i].startswith('>'): - arr[i] = '' + arr[i][4:] + '' - text = '
'.join(arr) - text = self._smileys_loader.add_smileys_to_text(text, self) - return text - - -class MessageItem(QtWidgets.QWidget): - """ - Message in messages list - """ - def __init__(self, text_message, settings, message_browser_factory_method, delete_action, parent=None): - QtWidgets.QWidget.__init__(self, parent) - 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['font']) - font.setPointSize(11) - font.setBold(True) - 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 = 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(util.convert_time(text_message.time)) - self.t = False - - 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; }") - self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 160, self.message.height())) - self.setFixedHeight(self.message.height()) - - 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(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): - self._delete_action(self._message) - - def mark_as_sent(self): - if self.t: - self.time.setText(util.convert_time(self._time)) - self.t = False - return True - return False - - def set_avatar(self, pixmap): - self.name.setAlignment(QtCore.Qt.AlignCenter) - self.message.setAlignment(QtCore.Qt.AlignVCenter) - self.setFixedHeight(max(self.height(), 36)) - self.name.setFixedHeight(self.height()) - self.message.setFixedHeight(self.height()) - self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) - - def select_text(self, text): - tmp = self.message.toHtml() - text = h.escape(text) - strings = re.findall(text, tmp, flags=re.IGNORECASE) - for s in strings: - tmp = self.replace_all(tmp, s) - self.message.setHtml(tmp) - - @staticmethod - def replace_all(text, substring): - i, l = 0, len(substring) - while i < len(text) - l + 1: - index = text[i:].find(substring) - if index == -1: - break - i += index - lgt, rgt = text[i:].find('<'), text[i:].find('>') - if rgt < lgt: - i += rgt + 1 - continue - sub = '{}'.format(substring) - text = text[:i] + sub + text[i + l:] - i += len(sub) - return text - - -class FileTransferItem(QtWidgets.QListWidget): - - 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 transfer_message.state == FILE_TRANSFER_STATE['CANCELLED']: - self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') - 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 = 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['font']) - font.setPointSize(11) - font.setBold(True) - self.name.setFont(font) - 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(util.convert_time(transfer_message.time)) - - self.cancel = QtWidgets.QPushButton(self) - self.cancel.setGeometry(QtCore.QRect(width - 125, 2, 30, 30)) - 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(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 transfer_message.state == FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - self.accept_or_pause.setVisible(True) - self.button_update('accept') - elif transfer_message.state in DO_NOT_SHOW_ACCEPT_BUTTON: - self.accept_or_pause.setVisible(False) - 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(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}') - - self.pb = QtWidgets.QProgressBar(self) - self.pb.setGeometry(QtCore.QRect(100, 7, 100, 20)) - self.pb.setValue(0) - self.pb.setStyleSheet('QProgressBar { background-color: #302F2F; }') - 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 = transfer_message.size // 1024 - if not file_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, transfer_message.file_name) - self.file_name.setText(file_data) - 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(transfer_message.state == FILE_TRANSFER_STATE['RUNNING']) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.paused = False - - def cancel_transfer(self, 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 == FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - directory = util_ui.directory_dialog(util_ui.tr('Choose folder')) - self.pb.setVisible(True) - if directory: - self._file_transfer_handler.accept_transfer(directory + '/' + self.saved_name, - friend_number, file_number, size) - self.button_update('pause') - elif self.state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume - self.paused = False - self._file_transfer_handler.resume_transfer(friend_number, file_number) - self.button_update('pause') - self.state = FILE_TRANSFER_STATE['RUNNING'] - else: # pause - self.paused = True - 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(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)) - - def update_transfer_state(self, state, progress, time): - self.pb.setValue(int(progress * 100)) - if time + 1: - 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 == 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 == 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 == 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 == 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 == FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']: - self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') - self.accept_or_pause.setVisible(False) - self.time_left.setVisible(False) - self.pb.setVisible(False) - elif not self.paused: # active - self.pb.setVisible(True) - self.accept_or_pause.setVisible(True) # setup to pause - self.button_update('pause') - self.setStyleSheet('QListWidget { border: 1px solid green; }') - self.state = state - self.time_left.setVisible(True) - - -class UnsentFileItem(FileTransferItem): - - 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 - 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): - self._file_transfer_handler.cancel_not_started_transfer(self._friend_number, self._message_id) - - -class InlineImageItem(QtWidgets.QScrollArea): - - def __init__(self, data, width, elem, parent=None): - - QtWidgets.QScrollArea.__init__(self, parent) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self._elem = elem - self._image_label = QtWidgets.QLabel(self) - self._image_label.raise_() - self.setWidget(self._image_label) - self._image_label.setScaledContents(False) - self._pixmap = QtGui.QPixmap() - self._pixmap.loadFromData(data, 'PNG') - self._max_size = width - 30 - self._resize_needed = not (self._pixmap.width() <= self._max_size) - self._full_size = not self._resize_needed - if not self._resize_needed: - self._image_label.setPixmap(self._pixmap) - self.resize(QtCore.QSize(self._max_size + 5, self._pixmap.height() + 5)) - self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) - else: - pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) - self._image_label.setPixmap(pixmap) - self.resize(QtCore.QSize(self._max_size + 5, pixmap.height())) - self._image_label.setGeometry(5, 0, self._max_size + 5, pixmap.height()) - self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton and self._resize_needed: # scale inline - if self._full_size: - pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) - self._image_label.setPixmap(pixmap) - self.resize(QtCore.QSize(self._max_size, pixmap.height())) - self._image_label.setGeometry(5, 0, pixmap.width(), pixmap.height()) - else: - self._image_label.setPixmap(self._pixmap) - self.resize(QtCore.QSize(self._max_size, self._pixmap.height() + 17)) - self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) - 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 = util_ui.directory_dialog(util_ui.tr('Choose folder')) - if directory: - fl = QtCore.QFile(directory + '/toxygen_inline_' + util.curr_time().replace(':', '_') + '.png') - self._pixmap.save(fl, 'PNG') diff --git a/toxygen/ui/peer_screen.py b/toxygen/ui/peer_screen.py deleted file mode 100644 index 6bca903..0000000 --- a/toxygen/ui/peer_screen.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - -from ui.widgets import CenteredWidget -import utils.util as util -import utils.ui as util_ui -from ui.contact_items import * -import toxygen_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) - - if self._peer: - 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 deleted file mode 100644 index 5b4658f..0000000 --- a/toxygen/ui/profile_settings_screen.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import QtGui, QtCore, uic - -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 - -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(str(self._profile.status_message)) - self.defaultProfilePushButton.clicked.connect(self._toggle_auto_profile) - self.copyToxIdPushButton.clicked.connect(self._copy_tox_id) - self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) - 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" +' (64)')) - - 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 deleted file mode 100644 index 7f30653..0000000 --- a/toxygen/ui/self_peer_screen.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - -from ui.widgets import CenteredWidget, LineEdit -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 deleted file mode 100644 index c838a0d..0000000 --- a/toxygen/ui/tray.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import QtWidgets, QtGui, QtCore -# from PyQt5.QtCore import pyqtSignal as Signal -from qtpy.QtCore import Signal - -from utils.ui import tr -from utils.util import * -from ui.password_screen import UnlockAppScreen -import os.path - -class SystemTrayIcon(QtWidgets.QSystemTrayIcon): - # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' - leftClicked = Signal() - - 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_bootstrap_screen.ui b/toxygen/ui/views/add_bootstrap_screen.ui deleted file mode 100644 index 0549e90..0000000 --- a/toxygen/ui/views/add_bootstrap_screen.ui +++ /dev/null @@ -1,99 +0,0 @@ - - - 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/add_contact_screen.ui b/toxygen/ui/views/add_contact_screen.ui deleted file mode 100644 index 0f26a25..0000000 --- a/toxygen/ui/views/add_contact_screen.ui +++ /dev/null @@ -1,99 +0,0 @@ - - - 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 deleted file mode 100644 index a404592..0000000 --- a/toxygen/ui/views/audio_settings_screen.ui +++ /dev/null @@ -1,87 +0,0 @@ - - - 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 deleted file mode 100644 index 16339d8..0000000 --- a/toxygen/ui/views/bans_list_screen.ui +++ /dev/null @@ -1,29 +0,0 @@ - - - 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 deleted file mode 100644 index 3a3358a..0000000 --- a/toxygen/ui/views/create_group_screen.ui +++ /dev/null @@ -1,127 +0,0 @@ - - - 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 deleted file mode 100644 index bfffee5..0000000 --- a/toxygen/ui/views/create_profile_screen.ui +++ /dev/null @@ -1,128 +0,0 @@ - - - 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 deleted file mode 100644 index a57d0e1..0000000 --- a/toxygen/ui/views/gc_ban_item.ui +++ /dev/null @@ -1,58 +0,0 @@ - - - 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 deleted file mode 100644 index 6eddbeb..0000000 --- a/toxygen/ui/views/gc_invite_item.ui +++ /dev/null @@ -1,71 +0,0 @@ - - - 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 deleted file mode 100644 index 526c156..0000000 --- a/toxygen/ui/views/gc_settings_screen.ui +++ /dev/null @@ -1,83 +0,0 @@ - - - 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 deleted file mode 100644 index 183f801..0000000 --- a/toxygen/ui/views/group_invites_screen.ui +++ /dev/null @@ -1,113 +0,0 @@ - - - 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 deleted file mode 100644 index de7c21e..0000000 --- a/toxygen/ui/views/group_management_screen.ui +++ /dev/null @@ -1,123 +0,0 @@ - - - Form - - - - 0 - 0 - 658 - 283 - - - - 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 - 300 - 41 - - - - PushButton - - - - - - 20 - 220 - 611 - 41 - - - - PushButton - - - - - - diff --git a/toxygen/ui/views/interface_settings_screen.ui b/toxygen/ui/views/interface_settings_screen.ui deleted file mode 100644 index b762903..0000000 --- a/toxygen/ui/views/interface_settings_screen.ui +++ /dev/null @@ -1,255 +0,0 @@ - - - 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 - 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 deleted file mode 100644 index 077a332..0000000 --- a/toxygen/ui/views/join_group_screen.ui +++ /dev/null @@ -1,139 +0,0 @@ - - - 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 deleted file mode 100644 index d100803..0000000 --- a/toxygen/ui/views/login_screen.ui +++ /dev/null @@ -1,135 +0,0 @@ - - - loginScreen - - - - 0 - 0 - 400 - 200 - - - - - 400 - 200 - - - - - 400 - 200 - - - - Form - - - - - 0 - 5 - 401 - 30 - - - - - 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 deleted file mode 100644 index ffbff71..0000000 --- a/toxygen/ui/views/ms_left_column.ui +++ /dev/null @@ -1,94 +0,0 @@ - - - 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 deleted file mode 100644 index f6e2960..0000000 --- a/toxygen/ui/views/network_settings_screen.ui +++ /dev/null @@ -1,196 +0,0 @@ - - - Form - - - - 0 - 0 - 400 - 545 - - - - - 400 - 545 - - - - - 400 - 545 - - - - 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 - 380 - 60 - 20 - - - - Chat Url - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 30 - 430 - 340 - 40 - - - - PushButton - - - - - - 30 - 60 - 340 - 30 - - - - CheckBox - - - - - - 30 - 480 - 340 - 65 - - - - TextLabel - - - - - - diff --git a/toxygen/ui/views/notifications_settings_screen.ui b/toxygen/ui/views/notifications_settings_screen.ui deleted file mode 100644 index 67e2dc6..0000000 --- a/toxygen/ui/views/notifications_settings_screen.ui +++ /dev/null @@ -1,71 +0,0 @@ - - - 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 deleted file mode 100644 index 086dd18..0000000 --- a/toxygen/ui/views/peer_screen.ui +++ /dev/null @@ -1,202 +0,0 @@ - - - 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 - - - - - - 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 deleted file mode 100644 index 1c899ab..0000000 --- a/toxygen/ui/views/profile_settings_screen.ui +++ /dev/null @@ -1,280 +0,0 @@ - - - Form - - - - 0 - 0 - 900 - 680 - - - - Form - - - - - 30 - 10 - 161 - 30 - - - - TextLabel - - - - - - 30 - 90 - 161 - 30 - - - - TextLabel - - - - - - 30 - 50 - 421 - 30 - - - - - - - 30 - 130 - 421 - 30 - - - - - - - 520 - 30 - 311 - 30 - - - - - - - 40 - 180 - 131 - 20 - - - - TextLabel - - - - - - 40 - 210 - 831 - 60 - - - - TextLabel - - - true - - - - - - 40 - 280 - 371 - 30 - - - - PushButton - - - - - - 440 - 280 - 371 - 30 - - - - PushButton - - - - - - 520 - 80 - 321 - 34 - - - - PushButton - - - - - - 520 - 130 - 321 - 34 - - - - PushButton - - - - - - 60 - 380 - 161 - 30 - - - - TextLabel - - - - - - 50 - 420 - 421 - 30 - - - - - - - 50 - 470 - 421 - 30 - - - - - - - 500 - 420 - 381 - 20 - - - - TextLabel - - - - - - 60 - 580 - 381 - 20 - - - - TextLabel - - - - - - 40 - 630 - 831 - 34 - - - - PushButton - - - - - - 50 - 520 - 421 - 34 - - - - PushButton - - - - - - 500 - 470 - 381 - 20 - - - - TextLabel - - - - - - 40 - 330 - 371 - 34 - - - - PushButton - - - - - - 440 - 330 - 371 - 34 - - - - PushButton - - - - - - diff --git a/toxygen/ui/views/self_peer_screen.ui b/toxygen/ui/views/self_peer_screen.ui deleted file mode 100644 index 38e1f88..0000000 --- a/toxygen/ui/views/self_peer_screen.ui +++ /dev/null @@ -1,119 +0,0 @@ - - - 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 deleted file mode 100644 index 76e7c57..0000000 --- a/toxygen/ui/views/update_settings_screen.ui +++ /dev/null @@ -1,67 +0,0 @@ - - - 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 deleted file mode 100644 index cfa36fb..0000000 --- a/toxygen/ui/views/video_settings_screen.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - 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/ui/widgets.py b/toxygen/ui/widgets.py deleted file mode 100644 index 78e9a0a..0000000 --- a/toxygen/ui/widgets.py +++ /dev/null @@ -1,213 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from qtpy import QtCore, QtGui, QtWidgets -# from PyQt5.QtCore import pyqtSignal as Signal -from qtpy.QtCore import Signal - -import utils.ui as util_ui -import logging - -global LOG -LOG = logging.getLogger('app') - -class DataLabel(QtWidgets.QLabel): - """ - Label with elided text - """ - def setText(self, text): - try: - text = ''.join('\u25AF' if len(bytes(str(c), 'utf-8')) >= 4 else c for c in str(text)) - except Exception as e: - LOG.error(f"DataLabel::setText: {e}") - return - - try: - metrics = QtGui.QFontMetrics(self.font()) - text = metrics.elidedText(str(text), QtCore.Qt.ElideRight, self.width()) - except Exception as e: - # RuntimeError: wrapped C/C++ object of type DataLabel has been deleted - text = str(text) - - super().setText(text) - -class ComboBox(QtWidgets.QComboBox): - - def __init__(self, *args): - super().__init__(*args) - self.view().setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) - - -class CenteredWidget(QtWidgets.QWidget): - - def __init__(self): - super().__init__() - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.center() - - def center(self): - qr = self.frameGeometry() - cp = QtWidgets.QDesktopWidget().availableGeometry().center() - qr.moveCenter(cp) - 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().__init__(parent) - - def contextMenuEvent(self, event): - menu = create_menu(self.createStandardContextMenu()) - menu.exec_(event.globalPos()) - del menu - - -class QRightClickButton(QtWidgets.QPushButton): - """ - Button with right click support - """ - # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' - rightClicked = Signal() - - def __init__(self, parent=None): - super().__init__(parent) - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - self.rightClicked.emit() - else: - super().mousePressEvent(event) - - -class RubberBand(QtWidgets.QRubberBand): - - def __init__(self): - 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) - self.painter = QtGui.QPainter() - - def paintEvent(self, event): - - self.painter.begin(self) - self.painter.setPen(self.pen) - self.painter.drawRect(event.rect()) - self.painter.end() - - -class RubberBandWindow(QtWidgets.QWidget): - - def __init__(self, parent): - super().__init__() - self.parent = parent - self.setMouseTracking(True) - self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) - self.showFullScreen() - self.setWindowOpacity(0.5) - self.rubberband = RubberBand() - self.rubberband.setWindowFlags(self.rubberband.windowFlags() | QtCore.Qt.FramelessWindowHint) - self.rubberband.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - def mousePressEvent(self, event): - self.origin = event.pos() - self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize())) - self.rubberband.show() - QtWidgets.QWidget.mousePressEvent(self, event) - - def mouseMoveEvent(self, event): - if self.rubberband.isVisible(): - self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized()) - left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height())) - right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height())) - top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y()) - bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height()) - self.setMask(left + right + top + bottom) - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Escape: - self.rubberband.setHidden(True) - self.close() - else: - super().keyPressEvent(event) - - -def create_menu(menu): - """ - :return translated menu - """ - for action in menu.actions(): - text = action.text() - if 'Link Location' in text: - text = text.replace('Copy &Link Location', - util_ui.tr("Copy link location")) - elif '&Copy' in text: - text = text.replace('&Copy', util_ui.tr("Copy")) - elif 'All' in text: - text = text.replace('Select All', util_ui.tr("Select all")) - elif 'Delete' in text: - text = text.replace('Delete', util_ui.tr("Delete")) - elif '&Paste' in text: - text = text.replace('&Paste', util_ui.tr("Paste")) - elif 'Cu&t' in text: - text = text.replace('Cu&t', util_ui.tr("Cut")) - elif '&Undo' in text: - text = text.replace('&Undo', util_ui.tr("Undo")) - elif '&Redo' in text: - text = text.replace('&Redo', util_ui.tr("Redo")) - else: - menu.removeAction(action) - continue - action.setText(text) - return menu - - -class MultilineEdit(CenteredWidget): - - def __init__(self, title, text, save): - super(MultilineEdit, self).__init__() - self.resize(350, 200) - self.setMinimumSize(QtCore.QSize(350, 200)) - self.setMaximumSize(QtCore.QSize(350, 200)) - self.setWindowTitle(title) - self.edit = QtWidgets.QTextEdit(self) - self.edit.setGeometry(QtCore.QRect(0, 0, 350, 150)) - self.edit.setText(text) - self.button = QtWidgets.QPushButton(self) - self.button.setGeometry(QtCore.QRect(0, 150, 350, 50)) - self.button.setText(util_ui.tr("Save")) - self.button.clicked.connect(self.button_click) - self.center() - self.save = save - - 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 deleted file mode 100644 index 08861a4..0000000 --- a/toxygen/ui/widgets_factory.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from ui.main_screen_widgets import * -from ui.menu import * -from ui.groups_widgets import * -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): - try: - with ts.ignoreStdout(): import cv2 - except ImportError: - cv2 = None - if cv2 is None: return None - 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 deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/updater/updater.py b/toxygen/updater/updater.py deleted file mode 100644 index 0eb81f3..0000000 --- a/toxygen/updater/updater.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import utils.util as util -import utils.ui as util_ui -import os -import platform -import urllib -from qtpy import QtNetwork, QtCore -import subprocess - -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) - -TIMEOUT=10 - -def connection_available(): - return False - try: - urllib.request.urlopen('http://216.58.192.142', timeout=TIMEOUT) # google.com - return True - except: - return False - -def updater_available(): - if is_from_sources(): - return os.path.exists(util.curr_directory() + '/toxygen_updater.py') - elif platform.system() == 'Windows': - return os.path.exists(util.curr_directory() + '/toxygen_updater.exe') - else: - return os.path.exists(util.curr_directory() + '/toxygen_updater') - - -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, settings): - return version - return None # no new version was found - - -def is_from_sources(): - return __file__.endswith('.py') - - -def test_url(version): - return 'https://github.com/toxygen-project/toxygen/releases/tag/v' + version - - -def get_url(version): - if is_from_sources(): - return 'https://github.com/toxygen-project/toxygen/archive/v' + version + '.zip' - else: - if platform.system() == 'Windows': - name = 'toxygen_windows.zip' - elif util.is_64_bit(): - name = 'toxygen_linux_64.tar.gz' - else: - name = 'toxygen_linux.tar.gz' - return 'https://github.com/toxygen-project/toxygen/releases/download/v{}/{}'.format(version, name) - - -def get_params(url, version): - if is_from_sources(): - if platform.system() == 'Windows': - return ['python', 'toxygen_updater.py', url, version] - else: - return ['python3', 'toxygen_updater.py', url, version] - elif platform.system() == 'Windows': - return [util.curr_directory() + '/toxygen_updater.exe', url, version] - else: - return ['./toxygen_updater', url, version] - - -def download(version): - os.chdir(util.curr_directory()) - url = get_url(version) - params = get_params(url, version) - LOG.info('Updating Toxygen') - try: - subprocess.Popen(params) - except Exception as ex: - LOG.error('running updater failed with ' + str(ex)) - - -def send_request(version, settings): - netman = QtNetwork.QNetworkAccessManager() - proxy = QtNetwork.QNetworkProxy() - 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: - request = QtNetwork.QNetworkRequest() - request.setUrl(QtCore.QUrl(url)) - reply = netman.get(request) - while not reply.isFinished(): - QtCore.QThread.msleep(1) - QtCore.QCoreApplication.processEvents() - attr = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute) - return attr is not None and 200 <= attr < 300 - except Exception as ex: - LOG.error('TOXYGEN UPDATER ' + str(ex)) - return False - - -def generate_versions(major, minor, patch): - new_major = '.'.join([str(major + 1), '0', '0']) - 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 deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/user_data/backup_service.py b/toxygen/user_data/backup_service.py deleted file mode 100644 index 9f3a051..0000000 --- a/toxygen/user_data/backup_service.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 deleted file mode 100644 index a6f5df0..0000000 --- a/toxygen/user_data/profile_manager.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import utils.util as util -import os - -from user_data.settings import Settings -from common.event import Event -from user_data.settings import get_user_config_path - -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) -from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE - -class ProfileManager: - """ - Class with methods for search, load and save profiles - """ - def __init__(self, toxes, path, app=None): - assert path - self._toxes = toxes - self._path = path - assert path - self._app = app - 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) - profile_path = self._path.replace('.json', '.tox') - try: - suf = f"{os.getpid()}" - with open(profile_path+suf, 'wb') as fl: - fl.write(data) - stat = os.stat(profile_path+suf) - if hasattr(stat, 'st_blocks'): - assert stat.st_blocks > 0, f"Zero length file {profile_path+suf}" - os.rename(profile_path+suf,profile_path) - LOG_INFO('Profile saved successfully to' +profile_path) - except Exception as e: - LOG_WARN(f"Profile save failed to {profile_path}\n{e}") - - self._profile_saved_event(data) - - def export_profile(self, settings, new_path, use_new_path): - profile_path = self._path.replace('.json', '.tox') - with open(profile_path, 'rb') as fin: - data = fin.read() - path = new_path + os.path.basename(profile_path) - with open(path, 'wb') as fout: - fout.write(data) - LOG.info('Profile exported successfully to ' +path) - util.copy(os.path.join(self._directory, 'avatars'), - os.path.join(new_path, 'avatars')) - if use_new_path: - profile_path = os.path.join(new_path, os.path.basename(profile_path)) - self._directory = new_path - settings.update_path(new_path) - - @staticmethod - def find_profiles(): - """ - Find available tox profiles - """ - path = get_user_config_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/user_data/settings.py b/toxygen/user_data/settings.py deleted file mode 100644 index c87eec3..0000000 --- a/toxygen/user_data/settings.py +++ /dev/null @@ -1,428 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os -from platform import system -import json - -from utils import util -from utils.util import log, join_path -from common.event import Event -import utils.ui as util_ui -import utils.util as util_utils -import user_data -from toxygen_wrapper.tests import support_testing as ts - -global LOG -import logging -LOG = logging.getLogger('settings') - -def merge_args_into_settings(args, settings): - if args: - if not hasattr(args, 'audio'): - LOG.warn('No audio ' +repr(args)) - settings['audio'] = getattr(args, 'audio') - if not hasattr(args, 'video'): - LOG.warn('No video ' +repr(args)) - settings['video'] = getattr(args, 'video') - for key in settings.keys(): - # proxy_type proxy_port proxy_host - not_key = 'not_' +key - if hasattr(args, key): - val = getattr(args, key) - if type(val) == bytes: - # proxy_host - ascii? - # filenames - ascii? - val = str(val, 'UTF-8') - settings[key] = val - elif hasattr(args, not_key): - val = not getattr(args, not_key) - settings[key] = val - clean_settings(settings) - return - -def clean_settings(self): - # failsafe to ensure C tox is bytes and Py settings is str - - # overrides - self['mirror_mode'] = False - # REQUIRED!! - if not os.path.exists('/proc/sys/net/ipv6'): - LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist') - self['ipv6_enabled'] = False - - if 'proxy_type' in self and self['proxy_type'] == 0: - self['proxy_host'] = '' - self['proxy_port'] = 0 - - if 'proxy_type' in self and self['proxy_type'] != 0 and \ - 'proxy_host' in self and self['proxy_host'] != '' and \ - 'proxy_port' in self and self['proxy_port'] != 0: - if 'udp_enabled' in self and self['udp_enabled']: - # We don't currently support UDP over proxy. - LOG.info("UDP enabled and proxy set: disabling UDP") - self['udp_enabled'] = False - if 'local_discovery_enabled' in self and self['local_discovery_enabled']: - LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled") - self['local_discovery_enabled'] = False - if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']: - LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled") - self['dht_announcements_enabled'] = False - - if 'auto_accept_path' in self and \ - type(self['auto_accept_path']) == bytes: - self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8') - - for key in Settings.get_default_settings(): - if key not in self: continue - if type(self[key]) == bytes: - LOG.warn('bytes setting in: ' +key \ - +' ' + repr(self[key])) - # ascii? - # self[key] = str(self[key], 'utf-8') - LOG.debug("Cleaned settings") - -def get_user_config_path(): - system = util_utils.get_platform() - if system == 'Windows': - return os.path.join(os.getenv('APPDATA'), 'Tox/') - elif system == 'Darwin': - return os.path.join(os.getenv('HOME'), 'Library/Application Support/Tox/') - else: - return os.path.join(os.getenv('HOME'), '.config/tox/') - -def supported_languages(): - return { - 'English': 'en_EN', - 'French': 'fr_FR', - 'Russian': 'ru_RU', - 'Ukrainian': 'uk_UA' - } - -def built_in_themes(): - return { - 'dark': 'dark_style.qss', - 'default': 'style.qss' - } - -#def get_global_settings_path(): -# return os.path.join(get_base_directory(), 'toxygen.json') - -def is_active_profile(profile_path): - sFile = profile_path + '.lock' - if not os.path.isfile(sFile): - return False - try: - import psutil - except Exception as e: - return True - with open(sFile, 'rb') as iFd: - sPid = iFd.read() - if sPid and int(sPid.strip()) in psutil.pids(): - return True - LOG.debug('Unlinking stale lock file ' +sFile) - try: - os.unlink(sFile) - except: - pass - return False - -class Settings(dict): - """ - Settings of current profile + global app settings - """ - - def __init__(self, toxes, json_path, app): - self._toxes = toxes - self._app = app - self._path = app._path - self._args = app._args - self._oArgs = app._args - self._log = lambda l: LOG.log(self._oArgs.loglevel, l) - self._profile_path = json_path.replace('.json', '.tox') - - self._settings_saved_event = Event() - path = json_path.replace('.tox', '.json') - if path and os.path.isfile(path): - try: - with open(path, 'rb') as fl: - data = fl.read() - if self._toxes.is_data_encrypted(data): - data = self._toxes.pass_decrypt(data) - info = json.loads(str(data, 'utf-8')) - LOG.debug('Parsed settings from: ' + str(path)) - except Exception as ex: - title = f"Error opening/parsing settings file:" - text = title +f"\n{path}\n" - LOG.error(text +str(ex)) - util_ui.message_box(text, title) - info = Settings.get_default_settings(app._args) - user_data.settings.clean_settings(info) - else: - LOG.debug('get_default_settings for: ' + repr(path)) - info = Settings.get_default_settings(app._args) - - if not path or not os.path.exists(path): - merge_args_into_settings(app._args, info) - else: - aC = self._changed(app._args, info) - if aC: - title = 'Override profile with commandline - ' - if path: - title += os.path.basename(path) - text = 'Override profile with command-line settings? \n' - # text += '\n'.join([str(key) +'=' +str(val) for - # key,val in self._changed(app._args).items()]) - text += repr(aC) - reply = util_ui.question(text, title) - if reply: - merge_args_into_settings(app._args, info) - info['audio'] = getattr(app._args, 'audio') - info['video'] = getattr(app._args, 'video') - if getattr(app._args, 'trace_enabled'): - info['trace_enabled'] = getattr(app._args, 'trace_enabled') - else: - LOG.warn("app._args, 'trace_enabled") - info['trace_enabled'] = False - super().__init__(info) - self._upgrade() - - LOG.info('Parsed settings from: ' + str(path)) - ex = f"self=id(self) {self}" - LOG.debug(ex) - - self.save() - self.locked = False - self.closing = False - self.unlockScreen = False - - # 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') - json_path = os.path.join(get_user_config_path(), 'toxygen.json') - tmp = json_path + str(os.getpid()) - try: - with open(tmp, 'wb') as fl: - fl.write(text) - if os.path.exists(json_path+'.bak'): - os.remove(json_path+'.bak') - os.rename(json_path, json_path+'.bak') - os.rename(tmp, json_path) - except Exception as e: - LOG.warn(f'Error saving to {json_path} ' +str(e)) - else: - 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, profile_path): - """ - Mark current profile as active - """ - if not profile_path: - profile_path = self.get_auto_profile() - - path = profile_path + '.lock' - try: - import shutil - except: - pass - else: - shutil.copy2(profile_path, path) - # need to open this with the same perms as _profile_path - # copy profile_path and then write? - with open(path, 'wb') as fl: - fl.write(bytes(str(os.getpid()), 'ascii')) - - def export(self, path): - text = json.dumps(self) - 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(appdir=None): - if appdir is None: - appdir = ts.get_user_config_path() - # self._path = - p = os.path.join(appdir, 'toxygen.json') - if not os.path.isfile(p): - return None - try: - with open(p, 'rb') as fl: - data = fl.read() - if self._toxes.is_data_encrypted(data): - data = self._toxes.pass_decrypt(data) - except Exception as ex: - LOG.warn(f"fl.read {p}: {ex}") - return None - try: - auto = json.loads(str(data, 'utf-8')) - except Exception as ex: - LOG.warn(f"json.loads {p}: {ex}") - auto = {} - if 'profile_path' in auto: - path = str(auto['profile_path']) - if not os.path.isabs(path): - path = join_path(path, os.path.dirname(os.path.realpath(__file__))) - if os.path.isfile(path): - return path - return None - - @staticmethod - def supported_languages(): - # backwards - return supported_languages() - - @staticmethod - def set_auto_profile(path): - p = os.path.join(os.path.dirname(path), 'toxygen.json') - if os.path.isfile(p): - with open(p) as fl: - data = fl.read() - data = json.loads(data) - else: - data = {} - data['profile_path'] = str(path) - with open(p, 'w') as fl: - fl.write(json.dumps(data)) - - @staticmethod - def reset_auto_profile(): - appdir = ts.get_user_config_path() - p = os.path.join(appdir, 'toxygen.json') - if os.path.isfile(p): - with open(p) as fl: - data = fl.read() - data = json.loads(data) - else: - data = {} - if 'profile_path' in data: - del data['profile_path'] - with open(p, 'w') as fl: - fl.write(json.dumps(data)) - - @staticmethod - def get_default_settings(args=None): - """ - Default profile settings - """ - retval = { - # FixMe: match? /var/local/src/c-toxcore/toxcore/tox.h - 'ipv6_enabled': True, - 'udp_enabled': True, - 'trace_enabled': False, - 'local_discovery_enabled': True, - 'dht_announcements_enabled': True, - 'proxy_type': 0, - 'proxy_host': '', - 'proxy_port': 0, - 'start_port': 0, - 'end_port': 0, - 'tcp_port': 0, - 'local_discovery_enabled': True, - 'hole_punching_enabled': False, - # tox_log_cb *log_callback; - 'experimental_thread_safety': False, - # operating_system - - 'theme': 'default', - 'notifications': False, - 'sound_notifications': False, - 'language': 'English', - 'calls_sound': False, # was True - - 'save_history': True, - 'save_unsent_only': False, - 'allow_inline': True, - 'allow_auto_accept': True, - 'auto_accept_path': None, - 'sorting': 0, - 'auto_accept_from_friends': [], - 'paused_file_transfers': {}, - 'resend_files': True, - 'friends_aliases': [], - 'show_avatars': False, - 'typing_notifications': False, - 'blocked': [], - 'plugins': [], - 'notes': {}, - 'smileys': True, - 'smiley_pack': 'default', - 'mirror_mode': False, - 'width': 920, - 'height': 500, - 'x': 400, - 'y': 400, - 'message_font_size': 14, - 'unread_color': 'red', - 'compact_mode': False, - 'identicons': True, - 'show_welcome_screen': False, - 'close_app': 0, - 'font': 'Times New Roman', - 'update': 0, - 'group_notifications': True, - 'download_nodes_list': False, # - 'download_nodes_url': 'https://nodes.tox.chat/json', - 'notify_all_gc': False, - 'backup_directory': None, - - 'audio': {'input': -1, - 'output': -1, - 'enabled': True}, - 'video': {'device': -1, - 'width': 320, - 'height': 240, - 'x': 0, - 'y': 0}, - 'current_nodes': None, - 'network': 'new', - 'tray_icon': False, - } - return retval - - # 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] - - def _changed(self, aArgs, info): - aRet = dict() - default = Settings.get_default_settings() - for key in default: - if key in ['audio', 'video']: continue - if key not in aArgs.__dict__: continue - val = aArgs.__dict__[key] - if val in ['0.0.0.0']: continue - if key in aArgs.__dict__ and key not in info: - # dunno = network - continue - if key in aArgs.__dict__ and info[key] != val: - aRet[key] = val - return aRet - diff --git a/toxygen/user_data/toxes.py b/toxygen/user_data/toxes.py deleted file mode 100644 index 84b8636..0000000 --- a/toxygen/user_data/toxes.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -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 new file mode 100644 index 0000000..5b078fb --- /dev/null +++ b/toxygen/util.py @@ -0,0 +1,49 @@ +import os +import time +import shutil + +program_version = '0.2.3' + + +def log(data): + with open(curr_directory() + '/logs.log', 'a') as fl: + fl.write(str(data) + '\n') + + +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 convert_time(t): + sec = int(t) - time.timezone + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + return '%02d:%02d' % (h, m) + + +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 deleted file mode 100644 index e69de29..0000000 diff --git a/toxygen/utils/ui.py b/toxygen/utils/ui.py deleted file mode 100644 index f7d20e8..0000000 --- a/toxygen/utils/ui.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy 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, file_filter=None): - return QtWidgets.QFileDialog.getSaveFileName(None, caption, util.curr_directory(), - filter=file_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 deleted file mode 100644 index 70cc7ff..0000000 --- a/toxygen/utils/util.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import datetime -import os -import platform -import re -import shutil -import sys -import time - - -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 - -oFD=None -def log(data=None): - global oFD - if not oFD: - if 'TMPDIR' in os.environ: - logdir = os.environ['TMPDIR'] - else: - logdir = '/tmp' - try: - oFD = open(join_path(logdir, 'toxygen.log'), 'a') - except Exception as ex: - oFD = None - print(f"ERROR: opening toxygen.log: {ex}") - return '' - if data is None: return oFD - try: - oFD.write(str(data) +'\n') - except Exception as ex: - print(f"ERROR: writing to toxygen.log: {ex}") - return data - -def curr_directory(current_file=None): - return os.path.dirname(os.path.realpath(current_file or __file__)) - - -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() - -def get_user_config_path(): - if get_platform() == 'Windows': - return os.getenv('APPDATA') + '/Tox/' - elif get_platform() == 'Darwin': - return os.getenv('HOME') + '/Library/Application Support/Tox/' - else: - return os.getenv('HOME') + '/.config/tox/' diff --git a/toxygen/widgets.py b/toxygen/widgets.py new file mode 100644 index 0000000..2abc7b7 --- /dev/null +++ b/toxygen/widgets.py @@ -0,0 +1,133 @@ +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui + + +class DataLabel(QtGui.QLabel): + """ + Label with elided text + """ + def setText(self, text): + text = ''.join(c if c <= '\U0010FFFF' else '\u25AF' for c in text) + metrics = QtGui.QFontMetrics(self.font()) + text = metrics.elidedText(text, QtCore.Qt.ElideRight, self.width()) + super().setText(text) + + +class CenteredWidget(QtGui.QWidget): + + def __init__(self): + super(CenteredWidget, self).__init__() + self.center() + + def center(self): + qr = self.frameGeometry() + cp = QtGui.QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + +class LineEdit(QtGui.QLineEdit): + + def __init__(self, parent=None): + super(LineEdit, self).__init__(parent) + + def contextMenuEvent(self, event): + menu = create_menu(self.createStandardContextMenu()) + menu.exec_(event.globalPos()) + del menu + + +class QRightClickButton(QtGui.QPushButton): + """ + Button with right click support + """ + + def __init__(self, parent): + super(QRightClickButton, self).__init__(parent) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.emit(QtCore.SIGNAL("rightClicked()")) + else: + super(QRightClickButton, self).mousePressEvent(event) + + +class RubberBand(QtGui.QRubberBand): + + def __init__(self): + super(RubberBand, self).__init__(QtGui.QRubberBand.Rectangle, None) + self.setPalette(QtGui.QPalette(QtCore.Qt.transparent)) + self.pen = QtGui.QPen(QtCore.Qt.blue, 4) + self.pen.setStyle(QtCore.Qt.SolidLine) + self.painter = QtGui.QPainter() + + def paintEvent(self, event): + + self.painter.begin(self) + self.painter.setPen(self.pen) + self.painter.drawRect(event.rect()) + self.painter.end() + + +def create_menu(menu): + """ + :return translated menu + """ + for action in menu.actions(): + text = action.text() + if 'Link Location' in text: + text = text.replace('Copy &Link Location', + QtGui.QApplication.translate("MainWindow", "Copy link location", None, + QtGui.QApplication.UnicodeUTF8)) + elif '&Copy' in text: + text = text.replace('&Copy', QtGui.QApplication.translate("MainWindow", "Copy", None, + QtGui.QApplication.UnicodeUTF8)) + elif 'All' in text: + text = text.replace('Select All', QtGui.QApplication.translate("MainWindow", "Select all", None, + QtGui.QApplication.UnicodeUTF8)) + elif 'Delete' in text: + text = text.replace('Delete', QtGui.QApplication.translate("MainWindow", "Delete", None, + QtGui.QApplication.UnicodeUTF8)) + elif '&Paste' in text: + text = text.replace('&Paste', QtGui.QApplication.translate("MainWindow", "Paste", None, + QtGui.QApplication.UnicodeUTF8)) + elif 'Cu&t' in text: + text = text.replace('Cu&t', QtGui.QApplication.translate("MainWindow", "Cut", None, + QtGui.QApplication.UnicodeUTF8)) + elif '&Undo' in text: + text = text.replace('&Undo', QtGui.QApplication.translate("MainWindow", "Undo", None, + QtGui.QApplication.UnicodeUTF8)) + elif '&Redo' in text: + text = text.replace('&Redo', QtGui.QApplication.translate("MainWindow", "Redo", None, + QtGui.QApplication.UnicodeUTF8)) + else: + menu.removeAction(action) + continue + action.setText(text) + return menu + + +class MultilineEdit(CenteredWidget): + + def __init__(self, title, text, save): + super(MultilineEdit, self).__init__() + self.resize(350, 200) + self.setMinimumSize(QtCore.QSize(350, 200)) + self.setMaximumSize(QtCore.QSize(350, 200)) + self.setWindowTitle(title) + self.edit = QtGui.QTextEdit(self) + self.edit.setGeometry(QtCore.QRect(0, 0, 350, 150)) + self.edit.setText(text) + self.button = QtGui.QPushButton(self) + self.button.setGeometry(QtCore.QRect(0, 150, 350, 50)) + self.button.setText(QtGui.QApplication.translate("MainWindow", "Save", None, QtGui.QApplication.UnicodeUTF8)) + self.button.clicked.connect(self.button_click) + self.center() + self.save = save + + def button_click(self): + self.save(self.edit.toPlainText()) + self.close() +