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..0a8182a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,15 @@ -.pylint.err -.pylint.out *.pyc *.pyo - -*.zip -*.bak -*.lis -*.dst -*.so - toxygen/toxcore tests/tests -toxygen/libs +tests/libs tests/.cache tests/__pycache__ tests/avatars toxygen/libs .idea *~ -#* *.iml -*.junk - *.so *.log toxygen/build @@ -37,5 +25,4 @@ 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/README.md b/README.md index 8a15a09..914fdfe 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # Toxygen -Toxygen is powerful cross-platform [Tox](https://tox.chat/) client -for Tox and IRC/weechat written in pure Python3. +Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pure Python3. -### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) +[![Release](https://img.shields.io/github/release/toxygen-project/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/releases/latest) +[![Stars](https://img.shields.io/github/stars/toxygen-project/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/stargazers) +[![Open issues](https://img.shields.io/github/issues/toxygen-project/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) +[![Build Status](https://travis-ci.org/toxygen-project/toxygen.svg?branch=master)](https://travis-ci.org/toxygen-project/toxygen) -### Supported OS: Linux and Windows (only Linux is tested at the moment) +### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) - [Updater](https://github.com/toxygen-project/toxygen_updater) + +### Supported OS: Linux and Windows ### Features: -- PyQt5, PyQt6, and maybe PySide2, PySide6 via qtpy -- IRC via weechat /relay -- NGC groups - 1v1 messages - File transfers - Audio calls @@ -23,13 +25,14 @@ for Tox and IRC/weechat written in pure Python3. - Emoticons - Stickers - Screenshots +- Name lookups (toxme.io support) - 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 +- Proxy support - Avatars - Multiprofile - Multilingual @@ -40,108 +43,22 @@ for Tox and IRC/weechat written in pure Python3. - Changing nospam - File resuming - Read receipts -- uses gevent + +### 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 +### Docs +[Check /docs/ for more info](/docs/) -## Forked +Also visit [pythonhosted.org/Toxygen/](http://pythonhosted.org/Toxygen/) -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! +[Wiki](https://wiki.tox.chat/clients/toxygen) 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/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..0b45358 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:16.04 + +RUN apt-get update && \ +apt-get install build-essential libtool autotools-dev automake checkinstall cmake check git yasm libsodium-dev libopus-dev libvpx-dev pkg-config -y && \ +git clone https://github.com/ingvar1995/toxcore.git --branch=ngc_rebase && \ +cd toxcore && mkdir _build && cd _build && \ +cmake .. && make && make install + +RUN apt-get install portaudio19-dev python3-pyqt5 python3-pyaudio python3-pip -y && \ +pip3 install numpy pydenticon opencv-python pyinstaller + +RUN useradd -ms /bin/bash toxygen +USER toxygen diff --git a/build/build.sh b/build/build.sh new file mode 100644 index 0000000..fb6c4b2 --- /dev/null +++ b/build/build.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +cd ~ +git clone https://github.com/toxygen-project/toxygen.git --branch=next_gen +cd toxygen/toxygen + +pyinstaller --windowed --icon=images/icon.ico main.py + +cp -r styles dist/main/ +find . -type f ! -name '*.qss' -delete +cp -r plugins dist/main/ +mkdir -p dist/main/ui/views +cp -r ui/views dist/main/ui/ +cp -r sounds dist/main/ +cp -r smileys dist/main/ +cp -r stickers dist/main/ +cp -r bootstrap dist/main/ +find . -type f ! -name '*.json' -delete +cp -r images dist/main/ +cp -r translations dist/main/ +find . -name "*.ts" -type f -delete + +cd dist +mv main toxygen +cd toxygen +mv main toxygen +wget -O updater https://github.com/toxygen-project/toxygen_updater/releases/download/v0.1/toxygen_updater_linux_64 +echo "[Paths]" >> qt.conf +echo "Prefix = PyQt5/Qt" >> qt.conf +cd .. + +tar -zcvf toxygen_linux_64.tar.gz toxygen > /dev/null +rm -rf toxygen diff --git a/docs/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/contact.md b/docs/contact.md index 5eb2fa6..9f80595 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 (NGC) - ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA diff --git a/docs/contributing.md b/docs/contributing.md index b2cebf4..93292aa 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -7,15 +7,12 @@ Help us find all bugs in Toxygen! Please provide following info: - Toxygen executable info - python executable (.py), precompiled binary, from package etc. - Steps to reproduce the bug -Want to see new feature in Toxygen? -[Ask for it!](https://git.plastiras.org/emdee/toxygen/issues) +Want to see new feature in Toxygen? [Ask for it!](https://github.com/toxygen-project/toxygen/issues) # Pull requests Developer? Feel free to open pull request. Our dev team is small so we glad to get help. -Don't know what to do? Improve UI, fix -[issues](https://git.plastiras.org/emdee/toxygen/issues) -or implement features from our TODO list. +Don't know what to do? Improve UI, fix [issues](https://github.com/toxygen-project/toxygen/issues) or implement features from our TODO list. You can find our TODO's in code, issues list and [here](/README.md). Also you can implement [plugins](/docs/plugins.md) for Toxygen. Note that we have a lot of branches for different purposes. Master branch is for stable versions (releases) only, so I recommend to open PR's to develop branch. Development of next Toxygen version usually goes there. Other branches used for implementing different tasks such as file transfers improvements or audio calls implementation etc. diff --git a/docs/install.md b/docs/install.md index 7d2b773..00f8188 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,15 +1,33 @@ # How to install Toxygen +## Use precompiled binary (recommended for users): +[Check our releases page](https://github.com/toxygen-project/toxygen/releases) + +## Using pip3 + +### Windows + +``pip install toxygen`` + +Run app using ``toxygen`` command. + ### Linux -1. Install [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/) +5. Install toxygen: +``sudo pip3 install toxygen`` 6. Run toxygen using ``toxygen`` command. +## Packages + +Arch Linux: [AUR](https://aur.archlinux.org/packages/toxygen-git/) + +Debian/Ubuntu: [tox.chat](https://tox.chat/download.html#gnulinux) + ## From source code (recommended for developers) ### Windows @@ -21,31 +39,34 @@ Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly r 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 +6. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip) +7. Unpack archive 8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\ 9. Run \toxygen\main.py. +Optional: install toxygen using setup.py: ``python setup.py install`` + +[libtox.dll for 32-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86_shared_release.zip) + +[libtox.dll for 64-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86-64_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86-64_shared_release.zip) + +[libsodium.a for 32-bit Python](https://build.tox.chat/view/libsodium/job/libsodium_build_windows_x86_static_release/lastSuccessfulBuild/artifact/libsodium_build_windows_x86_static_release.zip) + +[libsodium.a for 64-bit Python](https://build.tox.chat/view/libsodium/job/libsodium_build_windows_x86-64_static_release/lastSuccessfulBuild/artifact/libsodium_build_windows_x86-64_static_release.zip) + ### Linux 1. Install latest Python3: ``sudo apt-get install python3`` -2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` -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 . -`` +2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` or ``sudo pip3 install pyqt5`` +3. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/) +4. Install PyAudio: +``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``sudo pip3 install pyaudio``) +5. Install NumPy: ``sudo pip3 install numpy`` +6. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python`` +7. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip) +8. Unpack archive 9. Run app: -``python3 ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/bin/toxygen`` +``python3 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..d549e68 100644 --- a/docs/plugin_api.md +++ b/docs/plugin_api.md @@ -1,6 +1,6 @@ # 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.4 - 3.6) 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. @@ -53,5 +53,5 @@ Plugin's methods MUST NOT raise exceptions. # 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/toxygen-project/toxygen_plugins) 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..fb80363 --- /dev/null +++ b/setup.py @@ -0,0 +1,93 @@ +from setuptools import setup +from setuptools.command.install import install +from platform import system +from subprocess import call +import main +import sys +import os +from utils.util import curr_directory, join_path + + +version = main.__version__ + '.0' + + +if system() == 'Windows': + MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python', 'pydenticon'] +else: + MODULES = [] + try: + import pyaudio + except ImportError: + MODULES.append('PyAudio') + try: + import PyQt5 + except ImportError: + MODULES.append('PyQt5') + try: + import numpy + except ImportError: + MODULES.append('numpy') + try: + import cv2 + except ImportError: + MODULES.append('opencv-python') + try: + import pydenticon + except ImportError: + MODULES.append('pydenticon') + + +def get_packages(): + directory = join_path(curr_directory(__file__), 'toxygen') + for root, dirs, files in os.walk(directory): + packages = map(lambda d: 'toxygen.' + d, dirs) + packages = ['toxygen'] + list(packages) + + return packages + + +class InstallScript(install): + """This class configures Toxygen after installation""" + + def run(self): + install.run(self) + try: + if system() != 'Windows': + 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, "--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/toxygen-project/toxygen/', + keywords='toxygen tox messenger', + author='Ingvar', + maintainer='Ingvar', + license='GPL3', + packages=get_packages(), + install_requires=MODULES, + include_package_data=True, + classifiers=[ + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + 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/travis.py b/tests/travis.py index af8f83f..30d5edd 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 # check for syntax errors 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 index 4326849..a23816d 100644 --- a/toxygen/app.py +++ b/toxygen/app.py @@ -1,387 +1,141 @@ -# -*- 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 - +from PyQt5 import QtWidgets, QtGui, QtCore import ui.password_screen as password_screen +import updater.updater as updater +import os +from middleware.tox_factory import tox_factory +import wrapper.toxencryptsave as tox_encrypt_save +import user_data.toxes +from user_data.settings import Settings from ui.login_screen import LoginScreen +from user_data.profile_manager import ProfileManager +from plugin_support.plugin_support import PluginLoader from ui.main_screen import MainWindow from ui import tray - import utils.ui as util_ui import utils.util as util -from 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 contacts.contact_provider import ContactProvider +from contacts.friend_factory import FriendFactory +from contacts.group_factory import GroupFactory +from contacts.contacts_manager import ContactsManager +from av.calls_manager import CallsManager from history.database import Database -from history.history import History +from ui.widgets_factory import WidgetsFactory +from smileys.smileys import SmileyLoader +from ui.items_factories import MessagesItemsFactory, ContactItemsFactory from messenger.messenger import Messenger from network.tox_dns import ToxDns -from smileys.smileys import SmileyLoader +from history.history import History +from file_transfers.file_transfers_messages_service import FileTransfersMessagesService +from groups.groups_service import GroupsService from ui.create_profile_screen import CreateProfileScreen -from ui.items_factories import MessagesItemsFactory, ContactItemsFactory -from ui.widgets_factory import WidgetsFactory +from common.provider import Provider +from contacts.group_peer_factory import GroupPeerFactory from user_data.backup_service import BackupService import styles.style # TODO: dynamic loading -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) - + def __init__(self, version, path_to_profile=None, uri=None): self._version = version - self._tox = None - self._app = self._settings = self._profile_manager = None - self._plugin_loader = self._messenger = None + self._app = self._settings = self._profile_manager = self._plugin_loader = self._messenger = None self._tox = self._ms = self._init = self._main_loop = self._av_loop = None - self._uri = self._toxes = self._tray = None - self._file_transfer_handler = self._contacts_provider = None - self._friend_factory = self._calls_manager = None - self._contacts_manager = self._smiley_loader = None + self._uri = self._toxes = self._tray = self._file_transfer_handler = self._contacts_provider = None + self._friend_factory = self._calls_manager = self._contacts_manager = self._smiley_loader = None self._group_peer_factory = self._tox_dns = self._backup_service = None self._group_factory = self._groups_service = self._profile = None if uri is not None and uri.startswith('tox:'): self._uri = uri[4:] - self._history = None - self.bAppExiting = False + self._path = path_to_profile + # ----------------------------------------------------------------------------------------------------------------- # 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: + def main(self): """ Main function of app. loads login screen if needed and starts main screen """ - self._app = QApplication([]) + self._app = QtWidgets.QApplication([]) self._load_icon() - # is this still needed? - if util.get_platform() == 'Linux' and \ - hasattr(QtCore.Qt, 'AA_X11InitThreads'): + if util.get_platform() == 'Linux': 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 + if not self._select_and_load_profile(): + return - self._load_app_styles() - if self._args.language != 'English': - # (Pdb) Fatal Python error: Segmentation fault - self._load_app_translations() - self._create_dependencies() + if self._try_to_update(): + return - self._start_threads(True) + self._load_app_styles() + self._load_app_translations() - 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._create_dependencies() + self._start_threads() + + if self._uri is not None: + self._ms.add_contact(self._uri) 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 + self._execute_app() + self._stop_app() + + # ----------------------------------------------------------------------------------------------------------------- # App executing + # ----------------------------------------------------------------------------------------------------------------- - def _execute_app(self) -> None: - LOG.debug("_execute_app") - + def _execute_app(self): while True: try: self._app.exec_() except Exception as ex: - LOG.error('Unhandled exception: ' + str(ex)) + util.log('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() - + def _stop_app(self): 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._stop_threads() + self._file_transfer_handler.stop() + self._tray.hide() + self._save_profile() 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 + def _load_base_style(self): + with open(util.join_path(util.get_styles_directory(), 'dark_style.qss')) as fl: + style = fl.read() self._app.setStyleSheet(style) - def _load_app_styles(self) -> None: - LOG.debug(f"_load_app_styles {list(settings.built_in_themes().keys())}") + def _load_app_styles(self): # application color scheme - if self._settings['theme'] in ['', 'default']: return - for theme in settings.built_in_themes().keys(): + if self._settings['theme'] == 'dark': + return + for theme in self._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 + theme_path = self._settings.built_in_themes()[theme] + file_path = util.join_path(util.get_styles_directory(), theme_path) + with open(file_path) as fl: + style = fl.read() self._app.setStyleSheet(style) - LOG.info('_load_app_styles: loaded theme ' +self._args.theme) break - def _load_login_screen_translations(self) -> None: - LOG.debug("_load_login_screen_translations") + def _load_login_screen_translations(self): current_language, supported_languages = self._get_languages() if current_language not in supported_languages: return @@ -391,78 +145,48 @@ class App: self._app.installTranslator(translator) self._app.translator = translator - def _load_icon(self) -> None: - LOG.debug("_load_icon") + def _load_icon(self): icon_file = os.path.join(util.get_images_directory(), 'icon.png') self._app.setWindowIcon(QtGui.QIcon(icon_file)) @staticmethod - def _get_languages() -> tuple: - LOG.debug("_get_languages") + def _get_languages(): current_locale = QtCore.QLocale() curr_language = current_locale.languageToString(current_locale.language()) - supported_languages = settings.supported_languages() + supported_languages = Settings.supported_languages() return curr_language, supported_languages - def _load_app_translations(self) -> None: - LOG.debug("_load_app_translations") - lang = settings.supported_languages()[self._settings['language']] + def _load_app_translations(self): + lang = Settings.supported_languages()[self._settings['language']] translator = QtCore.QTranslator() translator.load(os.path.join(util.get_translations_directory(), lang)) self._app.installTranslator(translator) self._app.translator = translator - def _select_and_load_profile(self) -> bool: - LOG.debug("_select_and_load_profile: " +repr(self._path)) + def _select_and_load_profile(self): + encrypt_save = tox_encrypt_save.ToxEncryptSave() + self._toxes = user_data.toxes.ToxES(encrypt_save) - if self._path is not None: - # toxygen was started with path to profile - 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 + if self._path is not None: # toxygen was started with path to profile + self._load_existing_profile(self._path) else: auto_profile = Settings.get_auto_profile() if auto_profile is None: # no default profile - 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}") + if Settings.is_active_profile(self._path): # profile is in use profile_name = util.get_profile_name_from_path(self._path) title = util_ui.tr('Profile {}').format(profile_name) text = util_ui.tr( @@ -471,39 +195,30 @@ class App: if not reply: return False - # is self._path right - was pathless - self._settings.set_active_profile(self._path) + self._settings.set_active_profile() return True + # ----------------------------------------------------------------------------------------------------------------- # Threads + # ----------------------------------------------------------------------------------------------------------------- - def _start_threads(self, initial_start=True) -> None: - LOG.debug(f"_start_threads before: {threading.enumerate()}") + def _start_threads(self, initial_start=True): # init thread - self._init = threads.InitThread(self._tox, - self._plugin_loader, - self._settings, - self, - initial_start) + self._init = threads.InitThread(self._tox, self._plugin_loader, self._settings, 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 = threads.ToxIterateThread(self._tox) self._main_loop.start() - self._av_loop = threads.ToxAVIterateThread(self._tox.AV) self._av_loop.start() if initial_start: threads.start_file_transfer_thread() - LOG.debug(f"_start_threads after: {[t.name for t in threading.enumerate()]}") - def _stop_threads(self, is_app_closing=True) -> None: - LOG.debug("_stop_threads") - self._init.stop_thread(1.0) + def _stop_threads(self, is_app_closing=True): + self._init.stop_thread() self._av_loop.stop_thread() self._main_loop.stop_thread() @@ -511,54 +226,34 @@ class App: 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() + 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) + def _load_existing_profile(self, profile_path): + self._profile_manager = ProfileManager(self._toxes, profile_path) data = self._profile_manager.open_profile() if self._toxes.is_data_encrypted(data): - 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._settings = Settings(self._toxes, profile_path.replace('.tox', '.json')) + self._tox = self._create_tox(data) - 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) + def _create_new_profile(self, profile_name): result = self._get_create_profile_screen_result() if result is None: return False if result.save_into_default_folder: - profile_path = util.join_path(get_user_config_path(), profile_name + '.tox') + profile_path = util.join_path(Settings.get_default_path(), profile_name + '.tox') else: profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') if os.path.isfile(profile_path): @@ -566,24 +261,19 @@ class App: util_ui.tr('Error')) return False name = profile_name or 'toxygen_user' - assert self._args + self._tox = tox_factory() + self._tox.self_set_name(name if name else 'Toxygen User') + self._tox.self_set_status_message('Toxing on Toxygen') self._path = profile_path if result.password: self._toxes.set_password(result.password) - self._settings = Settings(self._toxes, - self._path.replace('.tox', '.json'), - 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._settings = Settings(self._toxes, self._path.replace('.tox', '.json')) self._profile_manager = ProfileManager(self._toxes, profile_path) try: self._save_profile() except Exception as ex: - #? print(ex) - LOG.error('Profile creation exception: ' + str(ex)) + print(ex) + util.log('Profile creation exception: ' + str(ex)) text = util_ui.tr('Profile saving error! Does Toxygen have permission to write to this directory?') util_ui.message_box(text, util_ui.tr('Error')) @@ -596,36 +286,33 @@ class App: return True def _get_create_profile_screen_result(self): - LOG.debug("_get_create_profile_screen_result") cps = CreateProfileScreen() cps.show() self._app.exec_() return cps.result - def _save_profile(self, data=None) -> None: - LOG.debug("_save_profile") + def _save_profile(self, data=None): data = data or self._tox.get_savedata() self._profile_manager.save_profile(data) + # ----------------------------------------------------------------------------------------------------------------- # Other private methods + # ----------------------------------------------------------------------------------------------------------------- def _enter_password(self, data): """ Show password screen """ - 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 + self._force_exit() - def _reset(self) -> None: - LOG.debug("_reset") + def _reset(self): """ Create new tox instance (new network settings) :return: tox instance @@ -636,102 +323,50 @@ class App: 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) + # create new tox instance + self._tox = self._create_tox(data) + self._start_threads(False) - tox_savers = [self._friend_factory, self._group_factory, - self._plugin_loader, self._contacts_manager, - self._contacts_provider, self._messenger, - self._file_transfer_handler, - self._groups_service, self._profile] - for tox_saver in tox_savers: - tox_saver.set_tox(self._tox) + tox_savers = [self._friend_factory, self._group_factory, self._plugin_loader, self._contacts_manager, + self._contacts_provider, self._messenger, self._file_transfer_handler, self._groups_service, + self._profile] + for tox_saver in tox_savers: + tox_saver.set_tox(self._tox) - self._calls_manager.set_toxav(self._tox.AV) - self._contacts_manager.update_friends_numbers() - self._contacts_manager.update_groups_lists() - self._contacts_manager.update_groups_numbers() + self._calls_manager.set_toxav(self._tox.AV) + self._contacts_manager.update_friends_numbers() + self._contacts_manager.update_groups_lists() + self._contacts_manager.update_groups_numbers() - self._init_callbacks() - 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) + self._init_callbacks() - 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) + def _create_dependencies(self): + self._backup_service = BackupService(self._settings, self._profile_manager) self._smiley_loader = SmileyLoader(self._settings) self._tox_dns = ToxDns(self._settings) - self._ms = MainWindow(self._settings, self._tray, 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 + self._ms = MainWindow(self._settings, self._tray) + db = Database(self._path.replace('.tox', '.db'), self._toxes) contact_items_factory = ContactItemsFactory(self._settings, self._ms) - self._friend_factory = FriendFactory(self._profile_manager, - self._settings, - self._tox, - db, - contact_items_factory) - self._group_factory = GroupFactory(self._profile_manager, - self._settings, - self._tox, - db, - contact_items_factory) - self._group_peer_factory = GroupPeerFactory(self._tox, - self._profile_manager, - db, - contact_items_factory) - self._contacts_provider = ContactProvider(self._tox, - self._friend_factory, - self._group_factory, - self._group_peer_factory, - app=self) - self._profile = Profile(self._profile_manager, - self._tox, - self._ms, - self._contacts_provider, - self._reset) + self._friend_factory = FriendFactory(self._profile_manager, self._settings, + self._tox, db, contact_items_factory) + self._group_factory = GroupFactory(self._profile_manager, self._settings, self._tox, db, contact_items_factory) + self._group_peer_factory = GroupPeerFactory(self._tox, self._profile_manager, db, contact_items_factory) + self._contacts_provider = ContactProvider(self._tox, self._friend_factory, self._group_factory, + self._group_peer_factory) + self._profile = Profile(self._profile_manager, self._tox, self._ms, self._contacts_provider, self._reset) self._init_profile() self._plugin_loader = PluginLoader(self._settings, self) history = None - messages_items_factory = MessagesItemsFactory(self._settings, - self._plugin_loader, - self._smiley_loader, - self._ms, - lambda m: history.delete_message(m)) - history = History(self._contacts_provider, db, - self._settings, self._ms, messages_items_factory) - self._contacts_manager = ContactsManager(self._tox, - self._settings, - self._ms, - self._profile_manager, - self._contacts_provider, - history, - self._tox_dns, + messages_items_factory = 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._calls_manager = CallsManager(self._tox.AV, self._settings, self._ms, self._contacts_manager) + self._messenger = Messenger(self._tox, self._plugin_loader, self._ms, self._contacts_manager, self._contacts_provider, messages_items_factory, self._profile, self._calls_manager) file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, @@ -741,310 +376,49 @@ class App: messages_items_factory.set_file_transfers_handler(self._file_transfer_handler) widgets_factory = None widgets_factory_provider = Provider(lambda: widgets_factory) - self._groups_service = GroupsService(self._tox, - self._contacts_manager, - self._contacts_provider, - self._ms, + self._groups_service = GroupsService(self._tox, self._contacts_manager, self._contacts_provider, self._ms, widgets_factory_provider) - widgets_factory = WidgetsFactory(self._settings, - self._profile, - self._profile_manager, - self._contacts_manager, - self._file_transfer_handler, - self._smiley_loader, - self._plugin_loader, - self._toxes, - self._version, - self._groups_service, - history, + widgets_factory = WidgetsFactory(self._settings, self._profile, self._profile_manager, self._contacts_manager, + self._file_transfer_handler, self._smiley_loader, self._plugin_loader, + self._toxes, self._version, self._groups_service, history, self._contacts_provider) - 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) + self._tray = tray.init_tray(self._profile, self._settings, self._ms, self._toxes) + self._ms.set_dependencies(widgets_factory, self._tray, self._contacts_manager, self._messenger, self._profile, + self._plugin_loader, self._file_transfer_handler, history, self._calls_manager, + self._groups_service, self._toxes) - if bSHOW_TRAY: # broken - # the tray icon does not die with the app - self._tray.show() + 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 _create_tox(self, data): + return tox_factory(data, self._settings) - def _force_exit(self, retval=0) -> None: - LOG.debug("_force_exit") - sys.exit(0) + def _force_exit(self): + raise SystemExit() - 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_callbacks(self): + callbacks.init_callbacks(self._tox, self._profile, self._settings, self._plugin_loader, self._contacts_manager, + self._calls_manager, self._file_transfer_handler, self._ms, self._tray, + self._messenger, self._groups_service, self._contacts_provider) - def _init_profile(self) -> None: - LOG.debug("_init_profile") + def _init_profile(self): if not self._profile.has_avatar(): self._profile.reset_avatar(self._settings['identicons']) - def _kill_toxav(self) -> None: -# LOG_debug("_kill_toxav") + def _kill_toxav(self): self._calls_manager.set_toxav(None) self._tox.AV.kill() - def _kill_tox(self) -> None: -# LOG.debug("_kill_tox") + def _kill_tox(self): 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/call.py b/toxygen/av/call.py index 73caa25..d3e023b 100644 --- a/toxygen/av/call.py +++ b/toxygen/av/call.py @@ -1,4 +1,4 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + class Call: @@ -17,7 +17,9 @@ class Call: is_active = property(get_is_active, set_is_active) + # ----------------------------------------------------------------------------------------------------------------- # Audio + # ----------------------------------------------------------------------------------------------------------------- def get_in_audio(self): return self._in_audio @@ -35,7 +37,9 @@ class Call: out_audio = property(get_out_audio, set_out_audio) + # ----------------------------------------------------------------------------------------------------------------- # Video + # ----------------------------------------------------------------------------------------------------------------- def get_in_video(self): return self._in_video diff --git a/toxygen/av/calls.py b/toxygen/av/calls.py index 9b40fc1..d5f2fe7 100644 --- a/toxygen/av/calls.py +++ b/toxygen/av/calls.py @@ -1,80 +1,21 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import pyaudio import time import threading -import logging +from wrapper.toxav_enums import * +import cv2 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 +import numpy as np 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 @@ -84,125 +25,65 @@ class AV(common.tox_save.ToxAvSave): self._audio_running = False self._out_stream = None + self._audio_rate = 8000 self._audio_channels = 1 self._audio_duration = 60 - self._audio_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._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000 self._video = None self._video_thread = None - self._video_running = None + self._video_running = False - self._video_width = 320 - self._video_height = 240 + self._video_width = 640 + self._video_height = 480 - # 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}") + def stop(self): self._running = False self.stop_audio_thread() self.stop_video_thread() - def __contains__(self, friend_number:int) -> bool: + def __contains__(self, friend_number): 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._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0) self._calls[friend_number] = Call(audio, video) - threading.Timer(TIMER_TIMEOUT, - lambda: self.finish_not_started_call(friend_number)).start() + threading.Timer(30.0, 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() + self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0) if audio_enabled: - LOG.debug(f"calls accept_call calling start_audio_thread F={friend_number}") - # may raise self.start_audio_thread() + if video_enabled: + self.start_video_thread() - def finish_call(self, friend_number, by_friend=False) -> None: - LOG.debug(f"finish_call {friend_number}") + 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] - 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 + 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() - 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: + def finish_not_started_call(self, friend_number): 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: + def toxav_call_state_cb(self, friend_number, state): """ New call state """ - LOG.debug(f"toxav_call_state_cb {friend_number}") call = self._calls[friend_number] call.is_active = True @@ -215,108 +96,41 @@ class AV(common.tox_save.ToxAvSave): if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video: self.start_video_thread() - def is_video_call(self, number) -> bool: + def is_video_call(self, number): return number in self and self._calls[number].in_video + # ----------------------------------------------------------------------------------------------------------------- # Threads + # ----------------------------------------------------------------------------------------------------------------- - def start_audio_thread(self, bSTREAM_CALLBACK=False) -> None: + def start_audio_thread(self): """ 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'") + self._audio_running = True - 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}") + self._audio = pyaudio.PyAudio() + self._audio_stream = self._audio.open(format=pyaudio.paInt16, + rate=self._audio_rate, + channels=self._audio_channels, + input=True, + input_device_index=self._settings.audio['input'], + frames_per_buffer=self._audio_sample_count * 10) - def stop_audio_thread(self) -> None: - LOG_DEBUG(f"stop_audio_thread {self._audio_stream}") + 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._stop_thread = True + + self._audio_thread.join() self._audio_thread = None self._audio_stream = None @@ -327,215 +141,99 @@ class AV(common.tox_save.ToxAvSave): self._out_stream.close() self._out_stream = None - def start_video_thread(self) -> None: + def start_video_thread(self): if self._video_thread is not None: return - s = self._settings - if 'video' not in s: - LOG.warn("AV.__init__ 'video' not in s" ) - LOG.debug(f"start_video_thread {s}" ) - 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_width = s.video['width'] + self._video_height = s.video['height'] + + if s.video['device'] == -1: + self._video = screen_sharing.DesktopGrabber(self._settings.video['x'], self._settings.video['y'], + self._settings.video['width'], self._settings.video['height']) + else: + self._video = cv2.VideoCapture(self._settings.video['device']) + self._video.set(cv2.CAP_PROP_FPS, 25) + self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width) + self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height) + + self._video_thread = threading.Thread(target=self.send_video) self._video_thread.start() - def stop_video_thread(self) -> None: - LOG_DEBUG(f"stop_video_thread {self._video_thread}") + def stop_video_thread(self): 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.join() self._video_thread = None self._video = None - # Incoming chunks - def audio_chunk(self, samples, channels_count, rate) -> None: + # ----------------------------------------------------------------------------------------------------------------- + # Incoming chunks + # ----------------------------------------------------------------------------------------------------------------- + + def audio_chunk(self, samples, channels_count, rate): """ 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}") + self._out_stream = self._audio.open(format=pyaudio.paInt16, + channels=channels_count, + rate=rate, + output_device_index=self._settings.audio['output'], + output=True) + self._out_stream.write(samples) + # ----------------------------------------------------------------------------------------------------------------- # 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: + def send_audio(self): """ This method sends audio to friends """ - i=0 - count = self._audio_sample_count_tox - LOG_DEBUG(f"send_audio stream={self._audio_stream}") + while self._audio_running: try: - pcm = self._audio_stream.read(count, exception_on_overflow=False) - if not pcm: - sleep(0.1) - else: - self.send_audio_data(pcm, count) + pcm = self._audio_stream.read(self._audio_sample_count) + if pcm: + for friend_num in self._calls: + if self._calls[friend_num].out_audio: + try: + self._toxav.audio_send_frame(friend_num, pcm, self._audio_sample_count, + self._audio_channels, self._audio_rate) + except: + pass except: - LOG_DEBUG(f"error send_audio {i}") - else: - LOG_TRACE(f"send_audio {i}") - i += 1 - sleep(0.01) + pass - def send_video(self) -> None: + time.sleep(0.01) + + def send_video(self): """ 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}") + if result: + height, width, channels = frame.shape + for friend_num in self._calls: + if self._calls[friend_num].out_video: + try: + y, u, v = self.convert_bgr_to_yuv(frame) + self._toxav.video_send_frame(friend_num, width, height, y, u, v) + except: + pass + except: pass - sleep( 1.0/iFPS) + time.sleep(0.01) - def convert_bgr_to_yuv(self, frame) -> tuple: + def convert_bgr_to_yuv(self, frame): """ :param frame: input bgr frame :return y, u, v: y, u, v values of frame @@ -566,20 +264,16 @@ class AV(common.tox_save.ToxAvSave): Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable() Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes """ - with ts.ignoreStdout(): - import cv2 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) y = frame[:self._video_height, :] y = list(itertools.chain.from_iterable(y)) - import numpy as np - # was np.int - u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) + u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int) u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2] u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:] u = list(itertools.chain.from_iterable(u)) - v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) + v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int) 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)) diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py index d0d6683..5a48672 100644 --- a/toxygen/av/calls_manager.py +++ b/toxygen/av/calls_manager.py @@ -1,40 +1,29 @@ -# -*- 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 cv2 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 + def __init__(self, toxav, settings, screen, contacts_manager): + self._call = av.calls.AV(toxav, settings) # object with data about calls self._call_widgets = {} # dict of incoming call widgets self._incoming_calls = set() self._settings = settings - self._main_screen = main_screen + self._screen = screen self._contacts_manager = contacts_manager self._call_started_event = event.Event() # friend_number, audio, video, is_outgoing self._call_finished_event = event.Event() # friend_number, is_declined - self._app = app - def set_toxav(self, toxav) -> None: - self._callav.set_toxav(toxav) + def set_toxav(self, toxav): + self._call.set_toxav(toxav) + # ----------------------------------------------------------------------------------------------------------------- # Events + # ----------------------------------------------------------------------------------------------------------------- def get_call_started_event(self): return self._call_started_event @@ -46,33 +35,35 @@ class CallsManager: call_finished_event = property(get_call_finished_event) + # ----------------------------------------------------------------------------------------------------------------- # AV support + # ----------------------------------------------------------------------------------------------------------------- - def call_click(self, audio=True, video=False) -> None: + def call_click(self, audio=True, video=False): """User clicked audio button in main window""" num = self._contacts_manager.get_active_number() if not self._contacts_manager.is_active_a_friend(): return - if num not in self._callav and self._contacts_manager.is_active_online(): # start call - if not self._settings['audio']['enabled']: + if num not in self._call 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(num, audio, video) + self._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 + 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) -> None: + def incoming_call(self, audio, video, friend_number): """ Incoming call from friend. """ - LOG.debug(f"CM incoming_call {friend_number}") - # if not self._settings['audio']['enabled']: return + 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() + self._screen.incoming_call() else: friend.actions = True text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") @@ -80,105 +71,46 @@ class CallsManager: 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: + def accept_call(self, friend_number, audio, video): """ Accept incoming call with audio or video - Called from a thread """ + self._call.accept_call(friend_number, audio, video) + self._screen.active_call() + if friend_number in self._incoming_calls: + self._incoming_calls.remove(friend_number) + del self._call_widgets[friend_number] - LOG.debug(f"CM accept_call from friend_number={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: + def stop_call(self, friend_number, by_friend): """ 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 + self._screen.call_finished() + is_video = self._call.is_video_call(friend_number) + self._call.finish_call(friend_number, by_friend) # finish or decline call if friend_number in self._call_widgets: - LOG.debug(f"CM.stop_call _call_widgets close") - self.close_call(friend_number) + self._call_widgets[friend_number].close() + del self._call_widgets[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 + def destroy_window(): + if is_video: 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") + threading.Timer(2.0, destroy_window).start() 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) + def friend_exit(self, friend_number): + if friend_number in self._call: + self._call.finish_call(friend_number, True) + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- def _get_incoming_call_widget(self, friend_number, text, friend_name): return av_widgets.IncomingCallWidget(self._settings, self, friend_number, text, friend_name) diff --git a/toxygen/av/screen_sharing.py b/toxygen/av/screen_sharing.py index e0f783b..265658c 100644 --- a/toxygen/av/screen_sharing.py +++ b/toxygen/av/screen_sharing.py @@ -1,6 +1,6 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import numpy as np +from PyQt5 import QtWidgets -from qtpy import QtWidgets class DesktopGrabber: @@ -13,11 +13,10 @@ class DesktopGrabber: self._height -= height % 4 self._screen = QtWidgets.QApplication.primaryScreen() - def read(self) -> tuple: + def read(self): pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height) image = pixmap.toImage() s = image.bits().asstring(self._width * self._height * 4) - import numpy as np arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4)) return True, arr diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py index 6d64783..fad68c4 100644 --- a/toxygen/bootstrap/bootstrap.py +++ b/toxygen/bootstrap/bootstrap.py @@ -1,48 +1,83 @@ -# -*- 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 +import urllib.request from utils.util import * +from PyQt5 import QtNetwork, QtCore +import json -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') +DEFAULT_NODES_COUNT = 4 -def download_nodes_list(settings, oArgs) -> str: + +class Node: + + def __init__(self, node): + self._ip, self._port, self._tox_key = node['ipv4'], node['port'], node['public_key'] + self._priority = random.randint(1, 1000000) if node['status_tcp'] and node['status_udp'] else 0 + + def get_priority(self): + return self._priority + + priority = property(get_priority) + + def get_data(self): + return self._ip, self._port, self._tox_key + + +def generate_nodes(nodes_count=DEFAULT_NODES_COUNT): + with open(_get_nodes_path(), 'rt') as fl: + json_nodes = json.loads(fl.read())['nodes'] + nodes = map(lambda json_node: Node(json_node), json_nodes) + nodes = filter(lambda n: n.priority > 0, nodes) + sorted_nodes = sorted(nodes, key=lambda x: x.priority) + if nodes_count is not None: + sorted_nodes = sorted_nodes[-DEFAULT_NODES_COUNT:] + for node in sorted_nodes: + yield node.get_data() + + +def download_nodes_list(settings): + url = 'https://nodes.tox.chat/json' 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 + return -def _save_nodes(nodes, app) -> None: + if not settings['proxy_type']: # no proxy + try: + req = urllib.request.Request(url) + req.add_header('Content-Type', 'application/json') + response = urllib.request.urlopen(req) + result = response.read() + _save_nodes(result) + except Exception as ex: + log('TOX nodes loading error: ' + str(ex)) + else: # proxy + netman = QtNetwork.QNetworkAccessManager() + proxy = QtNetwork.QNetworkProxy() + proxy.setType( + QtNetwork.QNetworkProxy.Socks5Proxy if settings['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) + proxy.setHostName(settings['proxy_host']) + proxy.setPort(settings['proxy_port']) + netman.setProxy(proxy) + try: + request = QtNetwork.QNetworkRequest() + request.setUrl(QtCore.QUrl(url)) + reply = netman.get(request) + + while not reply.isFinished(): + QtCore.QThread.msleep(1) + QtCore.QCoreApplication.processEvents() + data = bytes(reply.readAll().data()) + _save_nodes(data) + except Exception as ex: + log('TOX nodes loading error: ' + str(ex)) + + +def _get_nodes_path(): + return join_path(curr_directory(__file__), 'nodes.json') + + +def _save_nodes(nodes): if not nodes: return - with open(_get_nodes_path(app._args), 'wb') as fl: - LOG.info("Saving nodes to " +_get_nodes_path(app._args)) + print('Saving nodes...') + with open(_get_nodes_path(), 'wb') as fl: fl.write(nodes) diff --git a/toxygen/common/event.py b/toxygen/common/event.py index f51a51f..687a34d 100644 --- a/toxygen/common/event.py +++ b/toxygen/common/event.py @@ -1,4 +1,4 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + class Event: diff --git a/toxygen/common/provider.py b/toxygen/common/provider.py index 687fd9a..d16edb4 100644 --- a/toxygen/common/provider.py +++ b/toxygen/common/provider.py @@ -1,4 +1,4 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + class Provider: diff --git a/toxygen/common/tox_save.py b/toxygen/common/tox_save.py index 45563b2..09c159b 100644 --- a/toxygen/common/tox_save.py +++ b/toxygen/common/tox_save.py @@ -1,4 +1,4 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + class ToxSave: diff --git a/toxygen/contacts/basecontact.py b/toxygen/contacts/basecontact.py index b4b33f1..2058890 100644 --- a/toxygen/contacts/basecontact.py +++ b/toxygen/contacts/basecontact.py @@ -1,7 +1,6 @@ -# -*- 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 +from PyQt5 import QtCore, QtGui +from wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE import utils.util as util import common.event as event import contacts.common as common @@ -15,27 +14,26 @@ class BaseContact: Base class for all contacts. """ - def __init__(self, profile_manager, name, status_message, widget, tox_id, kind=''): + def __init__(self, profile_manager, name, status_message, widget, tox_id): """ :param name: name, example: 'Toxygen user' :param status_message: status message, example: 'Toxing on Toxygen' :param widget: ContactItem instance :param tox_id: tox id of contact - :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 @@ -55,7 +53,9 @@ class BaseContact: name_changed_event = property(get_name_changed_event) + # ----------------------------------------------------------------------------------------------------------------- # Status message + # ----------------------------------------------------------------------------------------------------------------- def get_status_message(self): return self._status_message @@ -75,7 +75,9 @@ class BaseContact: status_message_changed_event = property(get_status_message_changed_event) + # ----------------------------------------------------------------------------------------------------------------- # Status + # ----------------------------------------------------------------------------------------------------------------- def get_status(self): return self._status @@ -94,29 +96,30 @@ class BaseContact: 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 + avatar_path = self.get_avatar_path() + width = self._widget.avatar_label.width() + pixmap = QtGui.QPixmap(avatar_path) + self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation)) + self._widget.avatar_label.repaint() + self._avatar_changed_event(avatar_path) def reset_avatar(self, generate_new): avatar_path = self.get_avatar_path() @@ -158,23 +161,19 @@ class BaseContact: 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(): diff --git a/toxygen/contacts/common.py b/toxygen/contacts/common.py index bd46c32..27750a2 100644 --- a/toxygen/contacts/common.py +++ b/toxygen/contacts/common.py @@ -1,10 +1,10 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - +from pydenticon import Generator import hashlib -from pydenticon import Generator +# ----------------------------------------------------------------------------------------------------------------- # Typing notifications +# ----------------------------------------------------------------------------------------------------------------- class BaseTypingNotificationHandler: @@ -19,7 +19,7 @@ class BaseTypingNotificationHandler: class FriendTypingNotificationHandler(BaseTypingNotificationHandler): - def __init__(self, friend_number:int): + def __init__(self, friend_number): super().__init__() self._friend_number = friend_number @@ -30,7 +30,9 @@ class FriendTypingNotificationHandler(BaseTypingNotificationHandler): BaseTypingNotificationHandler.DEFAULT_HANDLER = BaseTypingNotificationHandler() +# ----------------------------------------------------------------------------------------------------------------- # Identicons support +# ----------------------------------------------------------------------------------------------------------------- def generate_avatar(public_key): diff --git a/toxygen/contacts/contact.py b/toxygen/contacts/contact.py index 70b9318..e88acf2 100644 --- a/toxygen/contacts/contact.py +++ b/toxygen/contacts/contact.py @@ -1,17 +1,10 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from history.database import TIMEOUT, \ - SAVE_MESSAGES, MESSAGE_AUTHOR - +from history.database import * from contacts import basecontact, common from messenger.messages import * from contacts.contact_menu import * from file_transfers import file_transfers as ft import re -# LOG=util.log -global LOG -import logging -LOG = logging.getLogger('app.'+__name__) class Contact(basecontact.BaseContact): """ @@ -42,7 +35,9 @@ class Contact(basecontact.BaseContact): if hasattr(self, '_message_getter'): del self._message_getter + # ----------------------------------------------------------------------------------------------------------------- # History support + # ----------------------------------------------------------------------------------------------------------------- def load_corr(self, first_time=True): """ @@ -119,7 +114,9 @@ class Contact(basecontact.BaseContact): return TextMessage(message, author, unix_time, message_type, unique_id) + # ----------------------------------------------------------------------------------------------------------------- # Unsent messages + # ----------------------------------------------------------------------------------------------------------------- def get_unsent_messages(self): """ @@ -132,11 +129,8 @@ class Contact(basecontact.BaseContact): """ :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] + messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) + and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr) return list(messages) def mark_as_sent(self, tox_message_id): @@ -145,10 +139,11 @@ class Contact(basecontact.BaseContact): 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}") + util.log('Mark as sent ex: ' + str(ex)) + # ----------------------------------------------------------------------------------------------------------------- # Message deletion + # ----------------------------------------------------------------------------------------------------------------- def delete_message(self, message_id): elem = list(filter(lambda m: m.message_id == message_id, self._corr))[0] @@ -194,7 +189,9 @@ class Contact(basecontact.BaseContact): 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 @@ -227,7 +224,9 @@ class Contact(basecontact.BaseContact): return i return None # not found + # ----------------------------------------------------------------------------------------------------------------- # Current text - text from message area + # ----------------------------------------------------------------------------------------------------------------- def get_curr_text(self): return self._curr_text @@ -237,7 +236,9 @@ class Contact(basecontact.BaseContact): curr_text = property(get_curr_text, set_curr_text) + # ----------------------------------------------------------------------------------------------------------------- # Alias support + # ----------------------------------------------------------------------------------------------------------------- def set_name(self, value): """ @@ -253,7 +254,9 @@ class Contact(basecontact.BaseContact): def has_alias(self): return self._alias + # ----------------------------------------------------------------------------------------------------------------- # Visibility in friends' list + # ----------------------------------------------------------------------------------------------------------------- def get_visibility(self): return self._visible @@ -263,7 +266,9 @@ class Contact(basecontact.BaseContact): visibility = property(get_visibility, set_visibility) + # ----------------------------------------------------------------------------------------------------------------- # Unread messages and other actions from friend + # ----------------------------------------------------------------------------------------------------------------- def get_actions(self): return self._new_actions @@ -291,7 +296,9 @@ class Contact(basecontact.BaseContact): messages = property(get_messages) + # ----------------------------------------------------------------------------------------------------------------- # Friend's or group's number (can be used in toxcore) + # ----------------------------------------------------------------------------------------------------------------- def get_number(self): return self._number @@ -301,19 +308,25 @@ class Contact(basecontact.BaseContact): 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 diff --git a/toxygen/contacts/contact_menu.py b/toxygen/contacts/contact_menu.py index 6f45ca6..8178d31 100644 --- a/toxygen/contacts/contact_menu.py +++ b/toxygen/contacts/contact_menu.py @@ -1,14 +1,10 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -from qtpy import QtWidgets - +from PyQt5 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 '' @@ -81,7 +77,9 @@ class ContactMenuBuilder: self._actions[self._index] = (text, handler) self._index += 1 +# ----------------------------------------------------------------------------------------------------------------- # Generators +# ----------------------------------------------------------------------------------------------------------------- class BaseContactMenuGenerator: @@ -92,15 +90,17 @@ class BaseContactMenuGenerator: 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)) + .with_action(util_ui.tr('Status message'), lambda: main_screen.copy_text(self._contact.status_message)) + .with_action(util_ui.tr('Public key'), lambda: main_screen.copy_text(self._contact.tox_id)) ) return copy_menu_builder @@ -108,11 +108,11 @@ class BaseContactMenuGenerator: def _generate_history_menu_builder(self, history_loader, main_screen): history_menu_builder = ContactMenuBuilder() (history_menu_builder - .with_name(util_ui.tr("Chat history")) - .with_action(util_ui.tr("Clear history"), lambda: history_loader.clear_history(self._contact) + .with_name(util_ui.tr('Chat history')) + .with_action(util_ui.tr('Clear history'), lambda: history_loader.clear_history(self._contact) or main_screen.messages.clear()) - .with_action(util_ui.tr("Export as text"), lambda: history_loader.export_history(self._contact)) - .with_action(util_ui.tr("Export as HTML"), lambda: history_loader.export_history(self._contact, False)) + .with_action(util_ui.tr('Export as text'), lambda: history_loader.export_history(self._contact)) + .with_action(util_ui.tr('Export as HTML'), lambda: history_loader.export_history(self._contact, False)) ) return history_menu_builder @@ -127,16 +127,16 @@ class FriendMenuGenerator(BaseContactMenuGenerator): groups_menu_builder = self._generate_groups_menu(contacts_manager, groups_service) allowed = self._contact.tox_id in settings['auto_accept_from_friends'] - auto = util_ui.tr("Disallow auto accept") if allowed else util_ui.tr('Allow auto accept') + auto = util_ui.tr('Disallow auto accept') if allowed else util_ui.tr('Allow auto accept') builder = ContactMenuBuilder() menu = (builder - .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) + .with_action(util_ui.tr('Set alias'), lambda: main_screen.set_alias(number)) .with_submenu(history_menu_builder) .with_submenu(copy_menu_builder) .with_action(auto, lambda: main_screen.auto_accept(number, not allowed)) - .with_action(util_ui.tr("Remove friend"), lambda: main_screen.remove_friend(number)) - .with_action(util_ui.tr("Block friend"), lambda: main_screen.block_friend(number)) + .with_action(util_ui.tr('Remove friend'), lambda: main_screen.remove_friend(number)) + .with_action(util_ui.tr('Block friend'), lambda: main_screen.block_friend(number)) .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) .with_optional_submenu(plugins_menu_builder) .with_optional_submenu(groups_menu_builder) @@ -144,7 +144,9 @@ class FriendMenuGenerator(BaseContactMenuGenerator): return menu + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _generate_plugins_menu_builder(plugin_loader, number): @@ -163,13 +165,11 @@ class FriendMenuGenerator(BaseContactMenuGenerator): def _generate_groups_menu(self, contacts_manager, groups_service): chats = contacts_manager.get_group_chats() - LOG.debug(f"_generate_groups_menu len(chats)={len(chats)} or self._contact.status={self._contact.status}") if not len(chats) or self._contact.status is None: - #? return None - pass + return None groups_menu_builder = ContactMenuBuilder() (groups_menu_builder - .with_name(util_ui.tr("Invite to group")) + .with_name(util_ui.tr('Invite to group')) .with_actions([(g.name, lambda: groups_service.invite_friend(self._contact.number, g.number)) for g in chats]) ) @@ -184,26 +184,26 @@ class GroupMenuGenerator(BaseContactMenuGenerator): builder = ContactMenuBuilder() menu = (builder - .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) + .with_action(util_ui.tr('Set alias'), lambda: main_screen.set_alias(number)) .with_submenu(copy_menu_builder) .with_submenu(history_menu_builder) - .with_optional_action(util_ui.tr("Manage group"), + .with_optional_action(util_ui.tr('Manage group'), lambda: groups_service.show_group_management_screen(self._contact), self._contact.is_self_founder()) - .with_optional_action(util_ui.tr("Group settings"), + .with_optional_action(util_ui.tr('Group settings'), lambda: groups_service.show_group_settings_screen(self._contact), not self._contact.is_self_founder()) - .with_optional_action(util_ui.tr("Set topic"), + .with_optional_action(util_ui.tr('Set topic'), lambda: groups_service.set_group_topic(self._contact), self._contact.is_self_moderator_or_founder()) -# .with_action(util_ui.tr("Bans list"), -# lambda: groups_service.show_bans_list(self._contact)) - .with_action(util_ui.tr("Reconnect to group"), + .with_action(util_ui.tr('Bans list'), + lambda: groups_service.show_bans_list(self._contact)) + .with_action(util_ui.tr('Reconnect to group'), lambda: groups_service.reconnect_to_group(self._contact.number)) - .with_optional_action(util_ui.tr("Disconnect from group"), + .with_optional_action(util_ui.tr('Disconnect from group'), lambda: groups_service.disconnect_from_group(self._contact.number), self._contact.status is not None) - .with_action(util_ui.tr("Leave group"), lambda: groups_service.leave_group(self._contact.number)) + .with_action(util_ui.tr('Leave group'), lambda: groups_service.leave_group(self._contact.number)) .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) ).build() @@ -218,10 +218,10 @@ class GroupPeerMenuGenerator(BaseContactMenuGenerator): builder = ContactMenuBuilder() menu = (builder - .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) + .with_action(util_ui.tr('Set alias'), lambda: main_screen.set_alias(number)) .with_submenu(copy_menu_builder) .with_submenu(history_menu_builder) - .with_action(util_ui.tr("Quit chat"), + .with_action(util_ui.tr('Quit chat'), lambda: contacts_manager.remove_group_peer(self._contact)) .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) ).build() diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py index 0c5a61d..76e8e79 100644 --- a/toxygen/contacts/contact_provider.py +++ b/toxygen/contacts/contact_provider.py @@ -1,32 +1,22 @@ -# -*- 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): + def __init__(self, tox, friend_factory, group_factory, group_peer_factory): super().__init__(tox) self._friend_factory = friend_factory self._group_factory = group_factory self._group_peer_factory = group_peer_factory self._cache = {} # key - contact's public key, value - contact instance - self._app = app + # ----------------------------------------------------------------------------------------------------------------- # Friends + # ----------------------------------------------------------------------------------------------------------------- + + def get_friend_by_number(self, friend_number): + public_key = self._tox.friend_get_public_key(friend_number) - 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): @@ -34,118 +24,67 @@ class ContactProvider(tox_save.ToxSave): 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} ") + self._add_to_cache(public_key, 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 [] + def get_all_friends(self): + friend_numbers = self._tox.self_get_friend_list() friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) + return list(friends) + # ----------------------------------------------------------------------------------------------------------------- # Groups + # ----------------------------------------------------------------------------------------------------------------- def get_all_groups(self): - """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 + group_numbers = range(self._tox.group_get_number_groups()) + groups = map(lambda n: self.get_group_by_number(n), group_numbers) + + return list(groups) def get_group_by_number(self, group_number): - 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 + public_key = self._tox.group_get_chat_id(group_number) - 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 + return self.get_group_by_public_key(public_key) def get_group_by_public_key(self, public_key): group = self._get_contact_from_cache(public_key) if group is not None: return group group = self._group_factory.create_group_by_public_key(public_key) - 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) + self._add_to_cache(public_key, group) return group + # ----------------------------------------------------------------------------------------------------------------- # Group peers + # ----------------------------------------------------------------------------------------------------------------- def get_all_group_peers(self): - return [] + return list() 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 + + return self._get_group_peer(group, peer) def get_group_peer_by_public_key(self, group, public_key): peer = group.get_peer_by_public_key(public_key) - 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 + return self._get_group_peer(group, peer) + + # ----------------------------------------------------------------------------------------------------------------- # All contacts + # ----------------------------------------------------------------------------------------------------------------- def get_all(self): return self.get_all_friends() + self.get_all_groups() + self.get_all_group_peers() + # ----------------------------------------------------------------------------------------------------------------- # Caching + # ----------------------------------------------------------------------------------------------------------------- def clear_cache(self): self._cache.clear() @@ -154,7 +93,9 @@ class ContactProvider(tox_save.ToxSave): 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 diff --git a/toxygen/contacts/contacts_manager.py b/toxygen/contacts/contacts_manager.py index 6f0dae8..87a61ff 100644 --- a/toxygen/contacts/contacts_manager.py +++ b/toxygen/contacts/contacts_manager.py @@ -1,36 +1,9 @@ -# -*- 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): """ @@ -42,14 +15,12 @@ class ContactsManager(ToxSave): 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._contacts, self._active_contact = [], -1 self._active_contact_changed = Event() self._sorting = settings['sorting'] self._filter_string = '' @@ -57,11 +28,6 @@ class ContactsManager(ToxSave): 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 @@ -70,46 +36,36 @@ class ContactsManager(ToxSave): def get_curr_contact(self): return self._contacts[self._active_contact] if self._active_contact + 1 else None - def save_profile(self) -> None: + def save_profile(self): data = self._tox.get_savedata() self._profile_manager.save_profile(data) - def is_friend_active(self, friend_number:int) -> bool: + def is_friend_active(self, friend_number): if not self.is_active_a_friend(): return False return self.get_curr_contact().number == friend_number - def is_group_active(self, group_number) -> bool: + def is_group_active(self, group_number): if self.is_active_a_friend(): return False return self.get_curr_contact().number == group_number - def is_contact_active(self, contact) -> 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 - + def is_contact_active(self, contact): return self._contacts[self._active_contact].tox_id == contact.tox_id + # ----------------------------------------------------------------------------------------------------------------- # Reconnection support + # ----------------------------------------------------------------------------------------------------------------- - def reset_contacts_statuses(self) -> None: + def reset_contacts_statuses(self): for contact in self._contacts: contact.status = None + # ----------------------------------------------------------------------------------------------------------------- # Work with active friend + # ----------------------------------------------------------------------------------------------------------------- def get_active(self): return self._active_contact @@ -139,16 +95,11 @@ class ContactsManager(ToxSave): current_contact.remove_messages_widgets() # TODO: if required self._unsubscribe_from_events(current_contact) - if self._active_contact >= 0 and self._active_contact != value: + if self._active_contact + 1 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() @@ -177,9 +128,10 @@ class ContactsManager(ToxSave): # 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 + except Exception as ex: # no friend found. ignore + util.log('Friend value: ' + str(value)) + util.log('Error in set active: ' + str(ex)) + raise active_contact = property(get_active, set_active) @@ -201,23 +153,21 @@ class ContactsManager(ToxSave): 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 + 4 - online and by name, 5 - online first and by name :param filter_str: show contacts which name contains this substring """ filter_str = filter_str.lower() current_contact = self.get_curr_contact() - for index, contact in enumerate(self._contacts): - if not contact._kind: - set_contact_kind(contact) - - if sorting > 6 or sorting < 0: + if sorting > 5 or sorting < 0: sorting = 0 if sorting in (1, 2, 4, 5): # online first @@ -233,30 +183,18 @@ class ContactsManager(ToxSave): 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): @@ -284,7 +222,9 @@ class ContactsManager(ToxSave): """ 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] @@ -295,15 +235,9 @@ class ContactsManager(ToxSave): def get_or_create_group_peer_contact(self, group_number, peer_id): group = self.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - if 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? + self.add_group_peer(group, peer) + return self.get_contact_by_tox_id(peer.public_key) def check_if_contact_exists(self, tox_id): @@ -321,7 +255,9 @@ class ContactsManager(ToxSave): 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): """ @@ -363,10 +299,7 @@ class ContactsManager(ToxSave): """ 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._tox.friend_delete(friend.number) self._delete_contact(num) def add_friend(self, tox_id): @@ -381,8 +314,8 @@ class ContactsManager(ToxSave): """ 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]: + tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] + if tox_id == self._tox.self_get_address[:TOX_PUBLIC_KEY_SIZE * 2]: return if tox_id not in self._settings['blocked']: self._settings['blocked'].append(tox_id) @@ -406,27 +339,21 @@ class ContactsManager(ToxSave): 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() + index = len(self._contacts) + self._contacts.append(group) + group.reset_avatar(self._settings['identicons']) + self._save_profile() + self.set_active(index) + self.update_filtration() def delete_group(self, group_number): group = self.get_group_by_number(group_number) @@ -434,32 +361,30 @@ class ContactsManager(ToxSave): 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' + return 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) + 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) + 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() @@ -477,48 +402,38 @@ class ContactsManager(ToxSave): return suggested_names[0] + # ----------------------------------------------------------------------------------------------------------------- # Friend requests + # ----------------------------------------------------------------------------------------------------------------- - def send_friend_request(self, sToxPkOrId, message): + def send_friend_request(self, tox_id, message): """ Function tries to send request to contact with specified id - :param sToxPkOrId: id of new contact or tox dns 4 value + :param tox_id: 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' + if '@' in tox_id: # value like groupbot@toxme.io + tox_id = self._tox_dns.lookup(tox_id) + if tox_id is None: + raise Exception('TOX DNS lookup failed') + if len(tox_id) == TOX_PUBLIC_KEY_SIZE * 2: # public key + self.add_friend(tox_id) + title = util_ui.tr('Friend added') + text = util_ui.tr('Friend added without sending friend request') + util_ui.message_box(text, title) else: - 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 - + self._tox.friend_add(tox_id, message.encode('utf-8')) + tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] + self._add_friend(tox_id) + self.update_filtration() + self.save_profile() + return True except Exception as ex: # wrong data - 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 + util.log('Friend request failed with ' + str(ex)) + return str(ex) def process_friend_request(self, tox_id, message): """ @@ -536,12 +451,14 @@ class ContactsManager(ToxSave): 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)) + util.log('Accept friend request failed! ' + str(ex)) def can_send_typing_notification(self): return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() + # ----------------------------------------------------------------------------------------------------------------- # Contacts numbers update + # ----------------------------------------------------------------------------------------------------------------- def update_friends_numbers(self): for friend in self._contact_provider.get_all_friends(): @@ -550,17 +467,9 @@ class ContactsManager(ToxSave): 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() @@ -569,22 +478,16 @@ class ContactsManager(ToxSave): 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 + for contact in filter(lambda c: not c.has_avatar(), self._contacts): contact.reset_avatar(self._settings['identicons']) self.update_filtration() @@ -594,7 +497,9 @@ class ContactsManager(ToxSave): 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) @@ -649,7 +554,7 @@ class ContactsManager(ToxSave): 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: + except: pass if contact.tox_id in self._settings['notes']: del self._settings['notes'][contact.tox_id] diff --git a/toxygen/contacts/friend.py b/toxygen/contacts/friend.py index 24b04ad..5c8eabb 100644 --- a/toxygen/contacts/friend.py +++ b/toxygen/contacts/friend.py @@ -1,11 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os - from contacts import contact, common from messenger.messages import * +import os from contacts.contact_menu import * + class Friend(contact.Contact): """ Friend in list of friends. @@ -16,7 +14,9 @@ class Friend(contact.Contact): self._receipts = 0 self._typing_notification_handler = common.FriendTypingNotificationHandler(number) + # ----------------------------------------------------------------------------------------------------------------- # File transfers support + # ----------------------------------------------------------------------------------------------------------------- def insert_inline(self, before_message_id, inline): """ @@ -29,7 +29,7 @@ class Friend(contact.Contact): self._corr.insert(i, inline) return i - len(self._corr) except: - return -1 + pass def get_unsent_files(self): messages = filter(lambda m: type(m) is UnsentFileMessage, self._corr) @@ -52,17 +52,23 @@ class Friend(contact.Contact): 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 index 31d5eec..8ebafd6 100644 --- a/toxygen/contacts/friend_factory.py +++ b/toxygen/contacts/friend_factory.py @@ -1,8 +1,7 @@ -# -*- 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): @@ -14,26 +13,28 @@ class FriendFactory(ToxSave): 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): + def create_friend_by_number(self, friend_number): aliases = self._settings['friends_aliases'] - sToxPk = self._tox.friend_get_public_key(friend_number) - assert sToxPk, sToxPk + tox_id = self._tox.friend_get_public_key(friend_number) try: - alias = list(filter(lambda x: x[0] == sToxPk, aliases))[0][1] + alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] except: alias = '' item = self._create_friend_item() - name = alias or self._tox.friend_get_name(friend_number) or sToxPk + name = alias or self._tox.friend_get_name(friend_number) or tox_id status_message = self._tox.friend_get_status_message(friend_number) - message_getter = self._db.messages_getter(sToxPk) - friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, sToxPk) + message_getter = self._db.messages_getter(tox_id) + friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, tox_id) friend.set_alias(alias) return friend + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- def _create_friend_item(self): """ diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py index c060e65..19ebc8e 100644 --- a/toxygen/contacts/group_chat.py +++ b/toxygen/contacts/group_chat.py @@ -1,17 +1,11 @@ -# -*- 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 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): @@ -31,7 +25,9 @@ class GroupChat(contact.Contact, ToxSave): def get_context_menu_generator(self): return GroupMenuGenerator(self) + # ----------------------------------------------------------------------------------------------------------------- # Properties + # ----------------------------------------------------------------------------------------------------------------- def get_is_private(self): return self._is_private @@ -57,7 +53,9 @@ class GroupChat(contact.Contact, ToxSave): peers_limit = property(get_peers_limit, set_peers_limit) + # ----------------------------------------------------------------------------------------------------------------- # Peers methods + # ----------------------------------------------------------------------------------------------------------------- def get_self_peer(self): return self._peers[0] @@ -75,20 +73,12 @@ class GroupChat(contact.Contact, ToxSave): return self.get_self_role() == constants.TOX_GROUP_ROLE['FOUNDER'] def add_peer(self, peer_id, is_current_user=False): - "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) + is_current_user) self._peers.append(peer) def remove_peer(self, peer_id): @@ -96,40 +86,25 @@ class GroupChat(contact.Contact, ToxSave): 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}") + self._peers.remove(peer) 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 + + return peers[0] 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 + + return peers[0] def remove_all_peers_except_self(self): self._peers = self._peers[:1] def get_peers_names(self): peers_names = map(lambda p: p.name, self._peers) - if peers_names: # broken - return list(peers_names) - else: - LOG_WARN(f"get_peers_names empty peers") - #? broken - return [] + + return list(peers_names) def get_peers(self): return self._peers[:] @@ -137,20 +112,21 @@ class GroupChat(contact.Contact, ToxSave): 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 -# + 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(): diff --git a/toxygen/contacts/group_factory.py b/toxygen/contacts/group_factory.py index 4345c4b..4083438 100644 --- a/toxygen/contacts/group_factory.py +++ b/toxygen/contacts/group_factory.py @@ -1,12 +1,7 @@ -# -*- 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 +import wrapper.toxcore_enums_and_consts as constants -global LOG -import logging -LOG = logging.getLogger(__name__) class GroupFactory(ToxSave): @@ -17,15 +12,12 @@ class GroupFactory(ToxSave): 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: @@ -43,7 +35,9 @@ class GroupFactory(ToxSave): return group + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- def _create_group_item(self): """ @@ -53,7 +47,7 @@ class GroupFactory(ToxSave): 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): + for i in range(self._tox.group_get_number_groups()): if self._tox.group_get_chat_id(i) == chat_id: return i return -1 diff --git a/toxygen/contacts/group_peer_contact.py b/toxygen/contacts/group_peer_contact.py index 3e6131c..8854198 100644 --- a/toxygen/contacts/group_peer_contact.py +++ b/toxygen/contacts/group_peer_contact.py @@ -1,13 +1,11 @@ -# -*- 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) + def __init__(self, profile_manager, message_getter, peer_number, name, widget, tox_id, group_pk): + super().__init__(profile_manager, message_getter, peer_number, name, str(), widget, tox_id) self._group_pk = group_pk def get_group_pk(self): diff --git a/toxygen/contacts/group_peer_factory.py b/toxygen/contacts/group_peer_factory.py index 1804b50..38b3a20 100644 --- a/toxygen/contacts/group_peer_factory.py +++ b/toxygen/contacts/group_peer_factory.py @@ -1,7 +1,7 @@ -# -*- 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): @@ -14,10 +14,7 @@ class GroupPeerFactory(ToxSave): 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) + item, peer.public_key, group.tox_id) group_peer_contact.status = peer.status return group_peer_contact diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py index 3afcf2b..81220af 100644 --- a/toxygen/contacts/profile.py +++ b/toxygen/contacts/profile.py @@ -1,27 +1,19 @@ -# -*- 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): + def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action): """ :param tox: tox instance :param screen: ref to main screen """ - assert tox basecontact.BaseContact.__init__(self, profile_manager, tox.self_get_name(), @@ -35,73 +27,61 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave): 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: + 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) -> None: + def set_status(self, status): super().set_status(status) if status is not None: self._tox.self_set_status(status) elif not self._waiting_for_reconnection: self._waiting_for_reconnection = True - self._timer = threading.Timer(iRECONNECT, self._reconnect) + self._timer = threading.Timer(50, self._reconnect) self._timer.start() - def set_name(self, value) -> None: + def set_name(self, value): if self.name == value: return super().set_name(value) self._tox.self_set_name(self._name) - def set_status_message(self, value) -> None: + def set_status_message(self, value): super().set_status_message(value) self._tox.self_set_status_message(self._status_message) def set_new_nospam(self): """Sets new nospam part of tox id""" - self._tox.self_set_nospam(random.randint(0, iUMAXINT)) # no spam - uint32 + self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32 self._tox_id = self._tox.self_get_address() - self._sToxId = self._tox.self_get_address() - return self._sToxId + return self._tox_id + + # ----------------------------------------------------------------------------------------------------------------- # Reset + # ----------------------------------------------------------------------------------------------------------------- - def restart(self) -> None: + def restart(self): """ Recreate tox instance """ self.status = None invoke_in_main_thread(self._reset_action) - def _reconnect(self) -> None: + def _reconnect(self): 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 = threading.Timer(50, 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/file_transfers.py index 5fa87f9..0f04e5b 100644 --- a/toxygen/file_transfers/file_transfers.py +++ b/toxygen/file_transfers/file_transfers.py @@ -1,15 +1,11 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os -from os import chdir, remove, rename +from wrapper.toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL from os.path import basename, getsize, exists, dirname +from os import remove, rename, chdir from time import time - +from wrapper.tox import Tox 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 = { 'RUNNING': 0, @@ -82,7 +78,6 @@ class FileTransfer: 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) @@ -117,6 +112,9 @@ class FileTransfer: if self._tox.file_control(self._friend_number, self._file_number, control): self.set_state(control) + def get_file_id(self): + return self._tox.file_get_file_id(self._friend_number, self._file_number) + def _signal(self): percentage = self._done / self._size if self._size else 0 if self._creation_time is None or not percentage: @@ -128,7 +126,9 @@ class FileTransfer: def _finished(self): self._finished_event(self._friend_number, self._file_number) +# ----------------------------------------------------------------------------------------------------------------- # Send file +# ----------------------------------------------------------------------------------------------------------------- class SendTransfer(FileTransfer): @@ -174,14 +174,11 @@ 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): + if path is None: avatar_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) + avatar_hash = Tox.hash(fl.read()) super().__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], avatar_hash) @@ -223,10 +220,12 @@ class SendFromFileBuffer(SendTransfer): def send_chunk(self, position, size): super().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): @@ -316,6 +315,7 @@ 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) @@ -328,11 +328,11 @@ class ReceiveAvatar(ReceiveTransfer): self._file.close() remove(full_path) 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) diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py index a9085c2..114383b 100644 --- a/toxygen/file_transfers/file_transfers_handler.py +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -1,22 +1,11 @@ -# -*- 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 @@ -30,16 +19,16 @@ class FileTransfersHandler(ToxSave): # key = (friend number, file number), value - message id profile.avatar_changed_event.add_callback(self._send_avatar_to_contacts) - self. lBlockAvatars = [] - - def stop(self) -> None: + + def stop(self): self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {} self._settings.save() + # ----------------------------------------------------------------------------------------------------------------- # File transfers support + # ----------------------------------------------------------------------------------------------------------------- - def incoming_file_transfer(self, friend_number, file_number, size, file_name) -> None: - # main thread + def incoming_file_transfer(self, friend_number, file_number, size, file_name): """ New transfer :param friend_number: number of friend who sent file @@ -48,15 +37,11 @@ class FileTransfersHandler(ToxSave): :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'] + inline = is_inline(file_name) and self._settings['allow_inline'] file_id = self._tox.file_get_file_id(friend_number, file_number) accepted = True if file_id in self._paused_file_transfers: - 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: @@ -67,33 +52,26 @@ class FileTransfersHandler(ToxSave): 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: + 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 """ - # callback if (friend_number, file_number) in self._file_transfers: tr = self._file_transfers[(friend_number, file_number)] if not already_cancelled: @@ -106,19 +84,17 @@ class FileTransfersHandler(ToxSave): 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 cancel_not_started_transfer(self, friend_number, message_id): + self._get_friend_by_number(friend_number).delete_one_unsent_file(message_id) - def pause_transfer(self, friend_number, file_number, by_friend=False) -> None: + def pause_transfer(self, friend_number, file_number, by_friend=False): """ Pause transfer with specified data """ tr = self._file_transfers[(friend_number, file_number)] tr.pause(by_friend) - def resume_transfer(self, friend_number, file_number, by_friend=False) -> None: + def resume_transfer(self, friend_number, file_number, by_friend=False): """ Resume transfer with specified data """ @@ -128,7 +104,7 @@ class FileTransfersHandler(ToxSave): else: tr.send_control(TOX_FILE_CONTROL['RESUME']) - def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0) -> None: + def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0): """ :param path: path for saving :param friend_number: friend number @@ -139,7 +115,6 @@ class FileTransfersHandler(ToxSave): """ path = self._generate_valid_path(path, from_position) friend = self._get_friend_by_number(friend_number) - if friend is None: return None if not inline: rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number, from_position) else: @@ -155,7 +130,7 @@ class FileTransfersHandler(ToxSave): if inline: self._insert_inline_before[(friend_number, file_number)] = message.message_id - def send_screenshot(self, data, friend_number) -> None: + def send_screenshot(self, data, friend_number): """ Send screenshot :param data: raw data - png format @@ -163,26 +138,22 @@ class FileTransfersHandler(ToxSave): """ self.send_inline(data, 'toxygen_inline.png', friend_number) - def send_sticker(self, path, friend_number) -> None: + def send_sticker(self, path, friend_number): with open(path, 'rb') as fl: data = fl.read() self.send_inline(data, 'sticker.png', friend_number) - def send_inline(self, data, file_name, friend_number, is_resend=False) -> None: + def send_inline(self, data, file_name, friend_number, is_resend=False): 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 + raise RuntimeError() st = SendFromBuffer(self._tox, friend.number, data, file_name) self._send_file_add_set_handlers(st, friend, file_name, True) - def send_file(self, path, friend_number, is_resend=False, file_id=None) -> None: + def send_file(self, path, friend_number, is_resend=False, file_id=None): """ Send file to current active friend :param path: file path @@ -191,52 +162,47 @@ class FileTransfersHandler(ToxSave): :param file_id: file id of transfer """ friend = self._get_friend_by_number(friend_number) - if friend is None: return None if friend.status is None and not is_resend: self._file_transfers_message_service.add_unsent_file_message(friend, path, None) return elif friend.status is None and is_resend: - LOG_WARN('Error in sending') + print('Error in sending') return st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id) file_name = os.path.basename(path) self._send_file_add_set_handlers(st, friend, file_name) - def incoming_chunk(self, friend_number, file_number, position, data) -> None: + 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) -> None: + def outgoing_chunk(self, friend_number, file_number, position, size): """ Outgoing chunk """ self._file_transfers[(friend_number, file_number)].send_chunk(position, size) - def transfer_finished(self, friend_number, file_number) -> None: + def transfer_finished(self, friend_number, file_number): transfer = self._file_transfers[(friend_number, file_number)] - friend = self._get_friend_by_number(friend_number) - if friend is None: return None t = type(transfer) if t is ReceiveAvatar: - friend.load_avatar() + self._get_friend_by_number(friend_number).load_avatar() elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image - LOG.debug('inline') + print('inline') inline = InlineImageMessage(transfer.data) message_id = self._insert_inline_before[(friend_number, file_number)] del self._insert_inline_before[(friend_number, file_number)] - if friend is None: return None - index = friend.insert_inline(message_id, inline) + index = self._get_friend_by_number(friend_number).insert_inline(message_id, inline) self._file_transfers_message_service.add_inline_message(transfer, index) del self._file_transfers[(friend_number, file_number)] - def send_files(self, friend_number:int) -> None: + def send_files(self, friend_number): + friend = self._get_friend_by_number(friend_number) + friend.remove_invalid_unsent_files() + files = friend.get_unsent_files() try: - friend = self._get_friend_by_number(friend_number) - if friend is None: return - 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: @@ -245,7 +211,6 @@ class FileTransfersHandler(ToxSave): self.send_file(path, friend_number, True) friend.clear_unsent_files() for key in self._paused_file_transfers.keys(): - # RuntimeError: dictionary changed size during iteration (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[key] if not os.path.exists(path): del self._paused_file_transfers[key] @@ -253,63 +218,32 @@ class FileTransfersHandler(ToxSave): 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)) + print('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: + def friend_exit(self, friend_number): + for friend_num, file_num in self._file_transfers.keys(): 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] + self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, False, -1] elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - 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._paused_file_transfers[ft.file_id] = [ft.path, friend_num, True, ft.total_size()] self.cancel_transfer(friend_num, file_num, True) + # ----------------------------------------------------------------------------------------------------------------- # Avatars support + # ----------------------------------------------------------------------------------------------------------------- - def send_avatar(self, friend_number, avatar_path=None) -> None: + def send_avatar(self, friend_number, avatar_path=None): """ :param friend_number: number of friend who should get new avatar :param avatar_path: path to avatar or None if reset """ - 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,) ) + sa = SendAvatar(avatar_path, self._tox, friend_number) + self._file_transfers[(friend_number, sa.file_number)] = sa - def incoming_avatar(self, friend_number, file_number, size) -> None: + def incoming_avatar(self, friend_number, file_number, size): """ Friend changed avatar :param friend_number: friend number @@ -317,7 +251,6 @@ class FileTransfersHandler(ToxSave): :param size: size of avatar or 0 (default avatar) """ friend = self._get_friend_by_number(friend_number) - if friend is None: return 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 @@ -325,21 +258,21 @@ class FileTransfersHandler(ToxSave): elif not size: friend.reset_avatar(self._settings['identicons']) - def _send_avatar_to_contacts(self, _) -> None: - # from a callback + def _send_avatar_to_contacts(self, _): friends = self._get_all_friends() for friend in filter(self._is_friend_online, friends): self.send_avatar(friend.number) + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- - def _is_friend_online(self, friend_number:int) -> bool: + def _is_friend_online(self, friend_number): friend = self._get_friend_by_number(friend_number) - if friend is None: return None return friend.status is not None - def _get_friend_by_number(self, friend_number:int): + def _get_friend_by_number(self, friend_number): return self._contact_provider.get_friend_by_number(friend_number) def _get_all_friends(self): diff --git a/toxygen/file_transfers/file_transfers_messages_service.py b/toxygen/file_transfers/file_transfers_messages_service.py index 1b292ee..4509183 100644 --- a/toxygen/file_transfers/file_transfers_messages_service.py +++ b/toxygen/file_transfers/file_transfers_messages_service.py @@ -1,15 +1,7 @@ -# -*- 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: @@ -20,9 +12,7 @@ class FileTransfersMessagesService: 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) @@ -37,7 +27,6 @@ class FileTransfersMessagesService: 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) @@ -50,21 +39,14 @@ class FileTransfersMessagesService: return tm - def add_inline_message(self, transfer, index) -> None: - """callback""" + def add_inline_message(self, transfer, index): 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) + self._create_inline_item(transfer.data, 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) @@ -76,9 +58,11 @@ class FileTransfersMessagesService: return tm + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- - def _is_friend_active(self, friend_number:int) -> bool: + def _is_friend_active(self, friend_number): if not self._contacts_manager.is_active_a_friend(): return False diff --git a/toxygen/groups/group_ban.py b/toxygen/groups/group_ban.py index 2b17a25..89ecc7e 100644 --- a/toxygen/groups/group_ban.py +++ b/toxygen/groups/group_ban.py @@ -1,4 +1,4 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + class GroupBan: diff --git a/toxygen/groups/group_invite.py b/toxygen/groups/group_invite.py index 2332933..a2eed47 100644 --- a/toxygen/groups/group_invite.py +++ b/toxygen/groups/group_invite.py @@ -1,4 +1,4 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + class GroupInvite: diff --git a/toxygen/groups/group_peer.py b/toxygen/groups/group_peer.py index a96c751..4eaf255 100644 --- a/toxygen/groups/group_peer.py +++ b/toxygen/groups/group_peer.py @@ -1,22 +1,22 @@ -# -*- 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): + def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False): self._peer_id = peer_id self._name = name self._status = status - self._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 @@ -33,12 +33,9 @@ class GroupChatPeer: 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 diff --git a/toxygen/groups/groups_service.py b/toxygen/groups/groups_service.py index 0e52d2a..b8fc7cc 100644 --- a/toxygen/groups/groups_service.py +++ b/toxygen/groups/groups_service.py @@ -1,16 +1,9 @@ -# -*- 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 +import wrapper.toxcore_enums_and_consts as constants -global LOG -LOG = logging.getLogger('app.'+'gs') class GroupsService(tox_save.ToxSave): @@ -23,22 +16,18 @@ class GroupsService(tox_save.ToxSave): 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: + def set_tox(self, tox): super().set_tox(tox) for group in self._get_all_groups(): group.set_tox(tox) + # ----------------------------------------------------------------------------------------------------------------- # Groups creation + # ----------------------------------------------------------------------------------------------------------------- - def create_new_gc(self, name, privacy_state, nick, status) -> 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 + def create_new_gc(self, name, privacy_state, nick, status): + group_number = self._tox.group_new(privacy_state, name, nick, status) if group_number == -1: return @@ -47,82 +36,51 @@ class GroupsService(tox_save.ToxSave): 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}") + def join_gc_by_id(self, chat_id, password, nick, status): + group_number = self._tox.group_join(chat_id, password, nick, status) self._add_new_group_by_number(group_number) - 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 leave_group(self, group_number): + self._tox.group_leave(group_number) + self._contacts_manager.delete_group(group_number) - def disconnect_from_group(self, group_number) -> None: + def disconnect_from_group(self, group_number): self._tox.group_disconnect(group_number) group = self._get_group_by_number(group_number) group.status = None self._clear_peers_list(group) - def reconnect_to_group(self, group_number) -> None: + def reconnect_to_group(self, group_number): self._tox.group_reconnect(group_number) group = self._get_group_by_number(group_number) group.status = constants.TOX_USER_STATUS['NONE'] self._clear_peers_list(group) + # ----------------------------------------------------------------------------------------------------------------- # Group invites + # ----------------------------------------------------------------------------------------------------------------- - def invite_friend(self, friend_number, group_number) -> 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 + def invite_friend(self, friend_number, group_number): + self._tox.group_invite_friend(group_number, friend_number) - 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: + def process_group_invite(self, friend_number, group_name, invite_data): 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: + def accept_group_invite(self, invite, name, status, password): 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: + def decline_group_invite(self, invite): self._delete_group_invite(invite) self._main_screen.update_gc_invites_button_state() @@ -136,13 +94,15 @@ class GroupsService(tox_save.ToxSave): 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: + def set_group_topic(self, group): if not group.is_self_moderator_or_founder(): return text = util_ui.tr('New topic for group "{}":'.format(group.name)) @@ -153,44 +113,46 @@ class GroupsService(tox_save.ToxSave): self._tox.group_set_topic(group.number, topic) group.status_message = topic - def show_group_management_screen(self, group) -> None: + def show_group_management_screen(self, group): widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_group_management_screen(group) self._screen.show() - def show_group_settings_screen(self, group) -> None: + def show_group_settings_screen(self, group): widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_group_settings_screen(group) self._screen.show() - def set_group_password(self, group, password) -> None: + def set_group_password(self, group, password): if group.password == password: return self._tox.group_founder_set_password(group.number, password) group.password = password - def set_group_peers_limit(self, group, peers_limit) -> None: + def set_group_peers_limit(self, group, peers_limit): if group.peers_limit == peers_limit: return self._tox.group_founder_set_peer_limit(group.number, peers_limit) group.peers_limit = peers_limit - def set_group_privacy_state(self, group, privacy_state) -> None: + def set_group_privacy_state(self, group, privacy_state): is_private = privacy_state == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE'] if group.is_private == is_private: return self._tox.group_founder_set_privacy_state(group.number, privacy_state) group.is_private = is_private + # ----------------------------------------------------------------------------------------------------------------- # Peers list + # ----------------------------------------------------------------------------------------------------------------- - def generate_peers_list(self) -> None: + def generate_peers_list(self): if not self._contacts_manager.is_active_a_group(): return group = self._contacts_manager.get_curr_contact() PeersListGenerator().generate(group.peers, self, self._peers_list_widget, group.tox_id) - def peer_selected(self, chat_id, peer_id) -> None: + def peer_selected(self, chat_id, peer_id): widgets_factory = self._get_widgets_factory() group = self._get_group_by_public_key(chat_id) self_peer = group.get_self_peer() @@ -200,18 +162,20 @@ class GroupsService(tox_save.ToxSave): 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: + def set_new_peer_role(self, group, peer, role): self._tox.group_mod_set_role(group.number, peer.id, role) peer.role = role self.generate_peers_list() - def toggle_ignore_peer(self, group, peer, ignore) -> None: + def toggle_ignore_peer(self, group, peer, ignore): self._tox.group_toggle_ignore(group.number, peer.id, ignore) peer.is_muted = ignore - def set_self_info(self, group, name, status) -> None: + def set_self_info(self, group, name, status): self._tox.group_self_set_name(group.number, name) self._tox.group_self_set_status(group.number, status) self_peer = group.get_self_peer() @@ -219,27 +183,29 @@ class GroupsService(tox_save.ToxSave): self_peer.status = status self.generate_peers_list() + # ----------------------------------------------------------------------------------------------------------------- # Bans support + # ----------------------------------------------------------------------------------------------------------------- - def show_bans_list(self, group) -> None: - return + def show_bans_list(self, group): widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_groups_bans_screen(group) self._screen.show() - def ban_peer(self, group, peer_id, ban_type) -> None: + def ban_peer(self, group, peer_id, ban_type): self._tox.group_mod_ban_peer(group.number, peer_id, ban_type) - def kick_peer(self, group, peer_id) -> None: + def kick_peer(self, group, peer_id): self._tox.group_mod_remove_peer(group.number, peer_id) - def cancel_ban(self, group_number, ban_id) -> None: + def cancel_ban(self, group_number, ban_id): self._tox.group_mod_remove_ban(group_number, ban_id) + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- - def _add_new_group_by_number(self, group_number) -> None: - LOG.debug(f"_add_new_group_by_number group_number={group_number}") + def _add_new_group_by_number(self, group_number): self._contacts_manager.add_group(group_number) def _get_group_by_number(self, group_number): @@ -251,41 +217,26 @@ class GroupsService(tox_save.ToxSave): def _get_all_groups(self): return self._contacts_provider.get_all_groups() - def _get_friend_by_number(self, friend_number:int): + def _get_friend_by_number(self, friend_number): return self._contacts_provider.get_friend_by_number(friend_number) def _get_friend_by_public_key(self, public_key): return self._contacts_provider.get_friend_by_public_key(public_key) - def _clear_peers_list(self, group) -> None: + def _clear_peers_list(self, group): group.remove_all_peers_except_self() self.generate_peers_list() - def _delete_group_invite(self, invite) -> None: + def _delete_group_invite(self, invite): 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 _join_gc_via_invite(self, invite_data, friend_number, nick, status, password): + group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, status, password) + self._add_new_group_by_number(group_number) - def _update_invites_button_state(self) -> None: + def _update_invites_button_state(self): self._main_screen.update_gc_invites_button_state() - def _get_widgets_factory(self) -> None: + def _get_widgets_factory(self): return self._widgets_factory_provider.get_item() diff --git a/toxygen/groups/peers_list.py b/toxygen/groups/peers_list.py index 97641d9..17495f5 100644 --- a/toxygen/groups/peers_list.py +++ b/toxygen/groups/peers_list.py @@ -1,10 +1,11 @@ -# -*- 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 wrapper.toxcore_enums_and_consts import * from ui.widgets import * + +# ----------------------------------------------------------------------------------------------------------------- # Builder +# ----------------------------------------------------------------------------------------------------------------- class PeerListBuilder: @@ -62,7 +63,9 @@ class PeerListBuilder: self._peers[self._index] = peer self._index += 1 +# ----------------------------------------------------------------------------------------------------------------- # Generators +# ----------------------------------------------------------------------------------------------------------------- class PeersListGenerator: diff --git a/toxygen/history/database.py b/toxygen/history/database.py index 7d8dd35..751c74b 100644 --- a/toxygen/history/database.py +++ b/toxygen/history/database.py @@ -1,20 +1,19 @@ -# -*- 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, @@ -25,33 +24,23 @@ CONTACT_TYPE = { class Database: def __init__(self, path, toxes): - self._path = path - self._toxes = toxes + self._path, self._toxes = path, toxes self._name = os.path.basename(path) + if os.path.exists(path): + try: + with open(path, 'rb') as fin: + data = fin.read() + if toxes.is_data_encrypted(data): + data = toxes.pass_decrypt(data) + with open(path, 'wb') as fout: + fout.write(data) + except Exception as ex: + util.log('Db reading error: ' + str(ex)) + os.remove(path) - 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(): @@ -69,7 +58,6 @@ class Database: 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() @@ -84,14 +72,11 @@ class Database: ' message_type INTEGER' ')') db.commit() - return True - except Exception as e: - LOG.error("dd_friend_to_db " +self._name +f" Database exception! {e}") + except: + print('Database is locked!') 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() @@ -99,14 +84,11 @@ class Database: 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}") + except: + print('Database is locked!') 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() @@ -114,16 +96,13 @@ class Database: cursor = db.cursor() cursor.executemany('INSERT INTO id' + tox_id + '(message, author_name, author_type, unix_time, message_type) ' + - 'VALUES (?, ?, ?, ?, ?);', messages_iter) + 'VALUES (?, ?, ?, ?, ?, ?);', messages_iter) db.commit() - return True - except Exception as e: - LOG.error("save_messages_to_db" +self._name +f" Database exception! {e}") + except: + print('Database is locked!') 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() @@ -132,14 +111,11 @@ class Database: 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}") + except: + print('Database is locked!') 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() @@ -147,14 +123,11 @@ class Database: 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}") + except: + print('Database is locked!') db.rollback() - return False finally: db.close() - LOG.debug(f"delete_message {tox_id}") def delete_messages(self, tox_id): db = self._connect() @@ -162,21 +135,20 @@ class Database: 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}") + except: + print('Database is locked!') 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: @@ -221,7 +193,9 @@ class Database: 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 index 971fa29..bd7e353 100644 --- a/toxygen/history/history.py +++ b/toxygen/history/history.py @@ -1,9 +1,5 @@ -# -*- 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: @@ -15,21 +11,22 @@ class History: 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']: + if self._settings['save_db']: for friend in self._contact_provider.get_all_friends(): self._db.add_friend_to_db(friend.tox_id) if not self._settings['save_unsent_only']: @@ -60,10 +57,8 @@ class History: 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() @@ -126,7 +121,9 @@ class History: return generator.generate() + # ----------------------------------------------------------------------------------------------------------------- # Items creation + # ----------------------------------------------------------------------------------------------------------------- def _create_message_item(self, message): return self._messages_items_factory.create_message_item(message, False) diff --git a/toxygen/history/history_logs_generators.py b/toxygen/history/history_logs_generators.py index 91c0a28..b8d0a56 100644 --- a/toxygen/history/history_logs_generators.py +++ b/toxygen/history/history_logs_generators.py @@ -1,7 +1,5 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import utils.util as util from messenger.messages import * +import utils.util as util class HistoryLogsGenerator: 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/avatar.png b/toxygen/images/avatar.png old mode 100644 new mode 100755 index 06255a1..83ac757 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 old mode 100644 new mode 100755 index ba153e9..ef9fa86 Binary files a/toxygen/images/call_video.png and b/toxygen/images/call_video.png differ diff --git a/toxygen/images/decline.png b/toxygen/images/decline.png old mode 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 old mode 100644 new mode 100755 index 9e4f830..8465106 Binary files a/toxygen/images/finish_call_video.png and b/toxygen/images/finish_call_video.png differ diff --git a/toxygen/images/group.png b/toxygen/images/group.png index 3ea6469..22adab0 100644 Binary files a/toxygen/images/group.png and b/toxygen/images/group.png differ diff --git a/toxygen/images/icon.png b/toxygen/images/icon.png index 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 old mode 100644 new mode 100755 index 2301877..4fe4c98 Binary files a/toxygen/images/incoming_call_video.png and b/toxygen/images/incoming_call_video.png differ diff --git a/toxygen/images/menu.png b/toxygen/images/menu.png old mode 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/main.py b/toxygen/main.py new file mode 100644 index 0000000..eca3ac3 --- /dev/null +++ b/toxygen/main.py @@ -0,0 +1,51 @@ +import app +from user_data.settings import * +import utils.util as util +import argparse + + +__maintainer__ = 'Ingvar' +__version__ = '0.5.0' + + +def clean(): + """Removes libs folder""" + directory = util.get_libs_directory() + util.remove(directory) + + +def reset(): + Settings.reset_auto_profile() + + +def print_toxygen_version(): + print('Toxygen v' + __version__) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--version', action='store_true', help='Prints Toxygen version') + parser.add_argument('--clean', action='store_true', help='Delete toxcore libs from libs folder') + parser.add_argument('--reset', action='store_true', help='Reset default profile') + parser.add_argument('--uri', help='Add specified Tox ID to friends') + parser.add_argument('profile', nargs='?', default=None, help='Path to Tox profile') + args = parser.parse_args() + + if args.version: + print_toxygen_version() + return + + if args.clean: + clean() + return + + if args.reset: + reset() + return + + toxygen = app.App(__version__, args.profile, args.uri) + toxygen.main() + + +if __name__ == '__main__': + main() diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py index d44a7a9..e777c4b 100644 --- a/toxygen/messenger/messages.py +++ b/toxygen/messenger/messages.py @@ -1,10 +1,8 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os.path - from history.database import MESSAGE_AUTHOR +import os.path from ui.messages_widgets import * + MESSAGE_TYPE = { 'TEXT': 0, 'ACTION': 1, @@ -40,8 +38,8 @@ class Message: MESSAGE_ID = 0 - def __init__(self, message_type, author, iTime): - self._time = iTime + def __init__(self, message_type, author, time): + self._time = time self._type = message_type self._author = author self._widget = None @@ -68,8 +66,7 @@ class Message: message_id = property(get_message_id) def get_widget(self, *args): - # FixMe - self._widget = self._create_widget(*args) # pylint: disable=assignment-from-none + self._widget = self._create_widget(*args) return self._widget @@ -84,11 +81,10 @@ class Message: self._widget.mark_as_sent() def _create_widget(self, *args): - # overridden - return None + pass @staticmethod - def _get_id() -> int: + def _get_id(): Message.MESSAGE_ID += 1 return int(Message.MESSAGE_ID) @@ -99,12 +95,12 @@ 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) + def __init__(self, message, owner, time, message_type, message_id=0): + super().__init__(message_type, owner, time) self._message = message self._id = message_id - def get_text(self) -> str: + def get_text(self): return self._message text = property(get_text) @@ -123,8 +119,8 @@ class TextMessage(Message): class OutgoingTextMessage(TextMessage): - def __init__(self, message, owner, iTime, message_type, tox_message_id=0): - super().__init__(message, owner, iTime, message_type) + def __init__(self, message, owner, time, message_type, tox_message_id=0): + super().__init__(message, owner, time, message_type) self._tox_message_id = tox_message_id def get_tox_message_id(self): @@ -138,8 +134,8 @@ class OutgoingTextMessage(TextMessage): class GroupChatMessage(TextMessage): - def __init__(self, cid, message, owner, iTime, message_type, name): - super().__init__(cid, message, owner, iTime, message_type) + def __init__(self, id, message, owner, time, message_type, name): + super().__init__(id, message, owner, time, message_type) self._user_name = name @@ -148,20 +144,20 @@ 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) + def __init__(self, author, time, state, size, file_name, friend_number, file_number): + super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, time) self._state = state self._size = size self._file_name = file_name self._friend_number, self._file_number = friend_number, file_number - def is_active(self, file_number) -> bool: + def is_active(self, file_number): if self._file_number != file_number: return False return self._state not in (FILE_TRANSFER_STATE['FINISHED'], FILE_TRANSFER_STATE['CANCELLED']) - def get_friend_number(self) -> int: + def get_friend_number(self): return self._friend_number friend_number = property(get_friend_number) @@ -189,10 +185,10 @@ class TransferMessage(Message): file_name = property(get_file_name) - def transfer_updated(self, state, percentage, iTime): + def transfer_updated(self, state, percentage, time): self._state = state if self._widget is not None: - self._widget.update_transfer_state(state, percentage, iTime) + self._widget.update_transfer_state(state, percentage, time) def _create_widget(self, *args): return FileTransferItem(self, *args) @@ -200,9 +196,9 @@ class TransferMessage(Message): class UnsentFileMessage(TransferMessage): - def __init__(self, path, data, iTime, author, size, friend_number): + def __init__(self, path, data, time, author, size, friend_number): file_name = os.path.basename(path) - super().__init__(author, iTime, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) + super().__init__(author, time, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) self._data, self._path = data, path def get_data(self): @@ -239,5 +235,5 @@ class InlineImageMessage(Message): class InfoMessage(TextMessage): - def __init__(self, message, iTime): - super().__init__(message, None, iTime, MESSAGE_TYPE['INFO_MESSAGE']) + def __init__(self, message, time): + super().__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py index c38bc31..e859135 100644 --- a/toxygen/messenger/messenger.py +++ b/toxygen/messenger/messenger.py @@ -1,15 +1,6 @@ -# -*- 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): @@ -28,19 +19,18 @@ class Messenger(tox_save.ToxSave): 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: + def get_last_message(self): contact = self._contacts_manager.get_curr_contact() if contact is None: return str() return contact.get_last_message_text() + # ----------------------------------------------------------------------------------------------------------------- # Messaging - friends + # ----------------------------------------------------------------------------------------------------------------- - def new_message(self, friend_number, message_type, message) -> None: + def new_message(self, friend_number, message_type, message): """ Current user gets new message :param friend_number: friend_num of friend who sent message @@ -52,7 +42,7 @@ class Messenger(tox_save.ToxSave): text_message = TextMessage(message, MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND']), t, message_type) self._add_message(text_message, friend) - def send_message(self) -> None: + def send_message(self): text = self._screen.messageEdit.toPlainText() plugin_command_prefix = '/plugin ' @@ -61,53 +51,33 @@ class Messenger(tox_save.ToxSave): 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):] + action_message_prefix = '/me ' + if text.startswith(action_message_prefix): + message_type = TOX_MESSAGE_TYPE['ACTION'] + text = text[len(action_message_prefix):] + else: + message_type = TOX_MESSAGE_TYPE['NORMAL'] - if 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) + if self._contacts_manager.is_active_a_friend(): + self.send_message_to_friend(text, message_type) + elif self._contacts_manager.is_active_a_group(): + self.send_message_to_group(text, message_type) + elif self._contacts_manager.is_active_a_group_chat_peer(): + self.send_message_to_group_peer(text, message_type) - def send_message_to_friend(self, text, message_type, friend_number=None) -> None: + def send_message_to_friend(self, text, message_type, friend_number=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") + + if not text or friend_number < 0: 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: @@ -124,7 +94,7 @@ class Messenger(tox_save.ToxSave): self._screen.messageEdit.clear() self._screen.messages.scrollToBottom() - def send_messages(self, friend_number:int) -> None: + def send_messages(self, friend_number): """ Send 'offline' messages to friend """ @@ -136,11 +106,13 @@ class Messenger(tox_save.ToxSave): message_id = self._tox.friend_send_message(friend_number, message.type, message.text.encode('utf-8')) message.tox_message_id = message_id except Exception as ex: - LOG.warn('Sending pending messages failed with ' + str(ex)) + util.log('Sending pending messages failed with ' + str(ex)) + # ----------------------------------------------------------------------------------------------------------------- # Messaging - groups + # ----------------------------------------------------------------------------------------------------------------- - def send_message_to_group(self, text, message_type, group_number=None) -> None: + def send_message_to_group(self, text, message_type, group_number=None): if group_number is None: group_number = self._contacts_manager.get_active_number() @@ -161,7 +133,7 @@ class Messenger(tox_save.ToxSave): self._screen.messageEdit.clear() self._screen.messages.scrollToBottom() - def new_group_message(self, group_number, message_type, message, peer_id) -> None: + def new_group_message(self, group_number, message_type, message, peer_id): """ Current user gets new message :param message_type: message type - plain text or action message (/me) @@ -169,63 +141,40 @@ class Messenger(tox_save.ToxSave): """ 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: + def send_message_to_group_peer(self, text, message_type, group_number=None, peer_id=None): if group_number is None or peer_id is None: group_peer_contact = self._contacts_manager.get_curr_contact() peer_id = group_peer_contact.number group = self._get_group_by_public_key(group_peer_contact.group_pk) group_number = group.number - if not text: - return - if group.number < 0: - return - if peer_id is not None and peer_id < 0: + if not text or group_number < 0 or peer_id < 0: return - assert_main_thread() + group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) group = self._get_group_by_number(group_number) 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 + self._tox.group_send_private_message(group_number, peer_id, message_type, message) message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) message = OutgoingTextMessage(text, message_author, t, message_type) - # 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) + 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: + def new_group_private_message(self, group_number, message_type, message, peer_id): """ Current user gets new message :param message: text of message @@ -233,26 +182,24 @@ class Messenger(tox_save.ToxSave): t = util.get_unix_time() group = self._get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - if not peer: - LOG.warn('FixMe new_group_private_message group.get_peer_by_id ' + str(peer_id)) - return text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) - 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: + def receipt(self, friend_number, message_id): friend = self._get_friend_by_number(friend_number) friend.mark_as_sent(message_id) + # ----------------------------------------------------------------------------------------------------------------- # Typing notifications + # ----------------------------------------------------------------------------------------------------------------- - def send_typing(self, typing) -> None: + def send_typing(self, typing): """ Send typing notification to a friend """ @@ -261,16 +208,18 @@ class Messenger(tox_save.ToxSave): contact = self._contacts_manager.get_curr_contact() contact.typing_notification_handler.send(self._tox, typing) - def friend_typing(self, friend_number, typing) -> None: + def friend_typing(self, friend_number, typing): """ Display incoming typing notification """ if self._contacts_manager.is_friend_active(friend_number): self._screen.typing.setVisible(typing) + # ----------------------------------------------------------------------------------------------------------------- # Contact info updated + # ----------------------------------------------------------------------------------------------------------------- - def new_friend_name(self, friend, old_name, new_name) -> None: + def new_friend_name(self, friend, old_name, new_name): if old_name == new_name or friend.has_alias(): return message = util_ui.tr('User {} is now known as {}') @@ -279,10 +228,12 @@ class Messenger(tox_save.ToxSave): friend.actions = True self._add_info_message(friend.number, message) + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- @staticmethod - def _split_message(message) -> list: + def _split_message(message): messages = [] while len(message) > TOX_MAX_MESSAGE_LENGTH: size = TOX_MAX_MESSAGE_LENGTH * 4 // 5 @@ -303,7 +254,7 @@ class Messenger(tox_save.ToxSave): return messages - def _get_friend_by_number(self, friend_number:int): + def _get_friend_by_number(self, friend_number): return self._contacts_provider.get_friend_by_number(friend_number) def _get_group_by_number(self, group_number): @@ -312,7 +263,7 @@ class Messenger(tox_save.ToxSave): 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: + def _on_profile_name_changed(self, new_name): if self._profile_name == new_name: return message = util_ui.tr('User {} is now known as {}') @@ -321,47 +272,39 @@ class Messenger(tox_save.ToxSave): self._add_info_message(friend.number, message) self._profile_name = new_name - def _on_call_started(self, friend_number, audio, video, is_outgoing) -> None: + def _on_call_started(self, friend_number, audio, video, is_outgoing): if is_outgoing: text = util_ui.tr("Outgoing video call") if video else util_ui.tr("Outgoing audio call") else: text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") self._add_info_message(friend_number, text) - def _on_call_finished(self, friend_number, is_declined) -> None: + def _on_call_finished(self, friend_number, is_declined): text = util_ui.tr("Call declined") if is_declined else util_ui.tr("Call finished") self._add_info_message(friend_number, text) - def _add_info_message(self, friend_number, text) -> None: + def _add_info_message(self, friend_number, text): 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() + def _create_info_message_item(self, message): self._items_factory.create_message_item(message) self._screen.messages.scrollToBottom() - def _add_message(self, text_message, contact) -> None: - assert_main_thread() - if not contact: - LOG.warn("_add_message null contact") - return + def _add_message(self, text_message, contact): 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: + def _create_message_item(self, text_message): # pixmap = self._contacts_manager.get_curr_contact().get_pixmap() self._items_factory.create_message_item(text_message) diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py index e0842f7..b9a4099 100644 --- a/toxygen/middleware/callbacks.py +++ b/toxygen/middleware/callbacks.py @@ -1,113 +1,48 @@ -# -*- 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 +from PyQt5 import QtGui +from wrapper.toxcore_enums_and_consts import * +from wrapper.toxav_enums import * +from wrapper.tox import bin_to_string import utils.ui as util_ui import utils.util as util +import cv2 +import numpy as np from middleware.threads import invoke_in_main_thread, execute from notifications.tray import tray_notification from notifications.sound import * -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 +import threading # 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 + print('Connection status: ', str(connection)) + status = tox.self_get_status() if connection != TOX_CONNECTION['NONE'] else None + invoke_in_main_thread(profile.set_status, status) return wrapped +# ----------------------------------------------------------------------------------------------------------------- # Callbacks - friends +# ----------------------------------------------------------------------------------------------------------------- def friend_status(contacts_manager, file_transfer_handler, profile, settings): - 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 + print("Friend's #{} status changed!".format(friend_number)) friend = contacts_manager.get_friend_by_number(friend_number) - if friend.status is None and settings['sound_notifications'] and \ - profile.status != TOX_USER_STATUS['BUSY']: + 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) @@ -126,7 +61,7 @@ def friend_connection_status(contacts_manager, profile, settings, plugin_loader, """ Check friend's connection status (offline, udp, tcp) """ - LOG_DEBUG(f"Friend #{friend_number} connection status: {new_status}") + print("Friend #{} connection status: {}".format(friend_number, new_status)) friend = contacts_manager.get_friend_by_number(friend_number) if new_status == TOX_CONNECTION['NONE']: invoke_in_main_thread(friend.set_status, None) @@ -144,35 +79,29 @@ def friend_connection_status(contacts_manager, profile, settings, plugin_loader, def friend_name(contacts_provider, messenger): - sSlot = 'friend_name' def wrapped(tox, friend_number, name, size, user_data): """ Friend changed his name """ - key = f"friend_number={friend_number}" - if bTooSoon(key, sSlot, 60): return + print('New name friend #' + str(friend_number)) friend = contacts_provider.get_friend_by_number(friend_number) old_name = friend.name new_name = str(name, 'utf-8') - 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') + print('User #{} has new status message'.format(friend_number)) invoke_in_main_thread(messenger.send_messages, friend_number) return wrapped @@ -183,20 +112,16 @@ def friend_message(messenger, contacts_manager, profile, settings, window, tray) """ New message from friend """ - LOG_DEBUG(f"friend_message #{friend_number}") message = str(message, 'utf-8') invoke_in_main_thread(messenger.new_message, friend_number, message_type, message) if not window.isActiveWindow(): friend = contacts_manager.get_friend_by_number(friend_number) - if settings['notifications'] \ - and profile.status != TOX_USER_STATUS['BUSY'] \ - and not settings.locked: + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: 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)) + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped @@ -206,7 +131,7 @@ def friend_request(contacts_manager): """ Called when user get new friend request """ - LOG_DEBUG(f'Friend request') + print('Friend request') key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) invoke_in_main_thread(contacts_manager.process_friend_request, tox_id, str(message, 'utf-8')) @@ -215,12 +140,9 @@ def friend_request(contacts_manager): def friend_typing(messenger): - sSlot = "friend_typing" def wrapped(tox, friend_number, typing, user_data): - key = f"friend_number={friend_number}" - if bTooSoon(key, sSlot, 10): return - LOG_DEBUG(f"friend_typing #{friend_number}") invoke_in_main_thread(messenger.friend_typing, friend_number, typing) + return wrapped @@ -231,7 +153,9 @@ def friend_read_receipt(messenger): return wrapped +# ----------------------------------------------------------------------------------------------------------------- # Callbacks - file transfers +# ----------------------------------------------------------------------------------------------------------------- def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager, settings): @@ -240,7 +164,7 @@ def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager """ def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data): if file_type == TOX_FILE_KIND['DATA']: - LOG_INFO(f'file_transfer_handler File friend_number={friend_number}') + print('File') try: file_name = str(file_name[:file_name_size], 'utf-8') except: @@ -252,18 +176,15 @@ def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager 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: + 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)) + 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') + print('Avatar') invoke_in_main_thread(file_transfer_handler.incoming_avatar, friend_number, file_number, @@ -306,7 +227,9 @@ def file_recv_control(file_transfer_handler): return wrapped +# ----------------------------------------------------------------------------------------------------------------- # Callbacks - custom packets +# ----------------------------------------------------------------------------------------------------------------- def lossless_packet(plugin_loader): @@ -331,20 +254,20 @@ def lossy_packet(plugin_loader): return wrapped +# ----------------------------------------------------------------------------------------------------------------- # Callbacks - audio +# ----------------------------------------------------------------------------------------------------------------- def call_state(calls_manager): - def wrapped(iToxav, friend_number, mask, user_data): + def wrapped(toxav, friend_number, mask, user_data): """ New call state """ - LOG_DEBUG(f"call_state #{friend_number}") + print(friend_number, mask) if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: invoke_in_main_thread(calls_manager.stop_call, friend_number, True) else: - # guessing was calls_manager. - #? incoming_call - calls_manager._call.toxav_call_state_cb(friend_number, mask) + calls_manager.toxav_call_state_cb(friend_number, mask) return wrapped @@ -354,7 +277,7 @@ def call(calls_manager): """ Incoming call from friend """ - LOG_DEBUG(f"Incoming call from {friend_number} {audio} {video}") + print(friend_number, audio, video) invoke_in_main_thread(calls_manager.incoming_call, audio, video, friend_number) return wrapped @@ -365,16 +288,16 @@ def callback_audio(calls_manager): """ New audio chunk """ -#trace LOG_DEBUG(f"callback_audio #{friend_number}") - # dunno was .call - calls_manager._call.audio_chunk( + calls_manager.call.audio_chunk( bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), audio_channels_count, rate) return wrapped +# ----------------------------------------------------------------------------------------------------------------- # Callbacks - video +# ----------------------------------------------------------------------------------------------------------------- def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data): @@ -401,9 +324,6 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u It can be created from initial y, u, v using slices """ - LOG_DEBUG(f"video_receive_frame from 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))) @@ -425,14 +345,15 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u 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 + frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + invoke_in_main_thread(cv2.imshow, str(friend_number), frame) + except Exception as ex: + print(ex) + +# ----------------------------------------------------------------------------------------------------------------- # Callbacks - groups +# ----------------------------------------------------------------------------------------------------------------- def group_message(window, tray, tox, messenger, settings, profile): @@ -440,24 +361,18 @@ 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']: + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['MESSAGE']) - 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)) + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped @@ -467,87 +382,54 @@ 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: + name = tox.group_peer_get_name(group_number, peer_id) + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and (not settings.locked) and bl: invoke_in_main_thread(tray_notification, name, message, tray, window) if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['MESSAGE']) icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - if tray and hasattr(tray, 'setIcon'): - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + 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: + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: friend = contacts_provider.get_friend_by_number(friend_number) title = util_ui.tr('New invite to group chat') text = util_ui.tr('{} invites you to group "{}"').format(friend.name, group_name) invoke_in_main_thread(tray_notification, title, text, tray, window) - if tray: - icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + 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) @@ -556,49 +438,29 @@ def group_peer_join(contacts_provider, groups_service): def group_peer_exit(contacts_provider, groups_service, contacts_manager): - def wrapped(tox, - group_number, peer_id, - exit_type, name, name_length, - message, length, - user_data): + def wrapped(tox, group_number, peer_id, 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}") + group.remove_peer(peer_id) + invoke_in_main_thread(groups_service.generate_peers_list) return wrapped + def group_peer_name(contacts_provider, groups_service): def wrapped(tox, group_number, peer_id, name, length, user_data): - LOG_DEBUG(f"group_peer_name #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - 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 + peer.name = str(name[:length], 'utf-8') + invoke_in_main_thread(groups_service.generate_peers_list) return wrapped def group_peer_status(contacts_provider, groups_service): def wrapped(tox, group_number, peer_id, peer_status, user_data): - 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 + peer.status = peer_status invoke_in_main_thread(groups_service.generate_peers_list) return wrapped @@ -606,62 +468,32 @@ def group_peer_status(contacts_provider, groups_service): def group_topic(contacts_provider): def wrapped(tox, group_number, peer_id, topic, length, user_data): - LOG_DEBUG(f"group_topic #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) - 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 + topic = str(topic[:length], 'utf-8') + invoke_in_main_thread(group.set_status_message, topic) return wrapped + def group_moderation(groups_service, contacts_provider, contacts_manager, messenger): + def update_peer_role(group, mod_peer_id, peer_id, new_role): peer = group.get_peer_by_id(peer_id) - 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") + peer.role = new_role # 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") + contacts_manager.remove_group_peer_by_id(group, peer_id) + group.remove_peer(peer_id) # 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['BAN']: + remove_peer(group, mod_peer_id, peer_id, True) elif event_type == TOX_GROUP_MOD_EVENT['OBSERVER']: update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['OBSERVER']) elif event_type == TOX_GROUP_MOD_EVENT['USER']: @@ -677,7 +509,6 @@ def group_moderation(groups_service, contacts_provider, contacts_manager, messen def group_password(contacts_provider): def wrapped(tox_link, group_number, password, length, user_data): - LOG_DEBUG(f"group_password #{group_number}") password = str(password[:length], 'utf-8') group = contacts_provider.get_group_by_number(group_number) group.password = password @@ -688,7 +519,6 @@ def group_password(contacts_provider): def group_peer_limit(contacts_provider): def wrapped(tox_link, group_number, peer_limit, user_data): - LOG_DEBUG(f"group_peer_limit #{group_number}") group = contacts_provider.get_group_by_number(group_number) group.peer_limit = peer_limit @@ -698,18 +528,19 @@ def group_peer_limit(contacts_provider): def group_privacy_state(contacts_provider): def wrapped(tox_link, group_number, privacy_state, user_data): - LOG_DEBUG(f"group_privacy_state #{group_number}") group = contacts_provider.get_group_by_number(group_number) group.is_private = privacy_state == TOX_GROUP_PRIVACY_STATE['PRIVATE'] 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): + contacts_provider): """ Initialization of all callbacks. :param tox: Tox instance @@ -726,7 +557,6 @@ def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, :param groups_service: GroupsService instance :param contacts_provider: ContactsProvider instance """ - # self callbacks tox.callback_self_connection_status(self_connection_status(tox, profile)) diff --git a/toxygen/middleware/threads.py b/toxygen/middleware/threads.py index 75e3fc9..5f9404b 100644 --- a/toxygen/middleware/threads.py +++ b/toxygen/middleware/threads.py @@ -1,170 +1,115 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import sys +from bootstrap.bootstrap import * 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 +from PyQt5 import QtCore -# 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): + def __init__(self): + super().__init__() self._stop_thread = False - if name: - super().__init__(name=name, target=target) - else: - super().__init__(target=target) - def stop_thread(self, timeout=-1): + def stop_thread(self): 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}") + self.join() + class BaseQThread(QtCore.QThread): - def __init__(self, name=None): - # NO name=name + def __init__(self): super().__init__() self._stop_thread = False - self.name = str(id(self)) - def stop_thread(self, timeout=-1): + def stop_thread(self): 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") + self.wait() + +# ----------------------------------------------------------------------------------------------------------------- # 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 + def __init__(self, tox, plugin_loader, settings, is_first_start): + super().__init__() + self._tox, self._plugin_loader, self._settings = tox, plugin_loader, settings self._is_first_start = is_first_start def run(self): - # DBUG+ InitThread run: ERROR name 'ts' is not defined - import toxygen_wrapper.tests.support_testing as ts - LOG_DEBUG('InitThread run: ') + if self._is_first_start: + # download list of nodes if needed + download_nodes_list(self._settings) + # start plugins + self._plugin_loader.load() + + # bootstrap 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}") + for data in generate_nodes(): + if self._stop_thread: + return + self._tox.bootstrap(*data) + self._tox.add_tcp_relay(*data) + except: pass - for _ in range(ts.iTHREAD_JOINS): + for _ in range(10): if self._stop_thread: return - sleep(ts.iTHREAD_SLEEP) - return + time.sleep(1) + + while not self._tox.self_get_connection_status(): + try: + for data in generate_nodes(None): + if self._stop_thread: + return + self._tox.bootstrap(*data) + self._tox.add_tcp_relay(*data) + except: + pass + finally: + time.sleep(5) + class ToxIterateThread(BaseQThread): - def __init__(self, tox, app=None): + def __init__(self, tox): 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) + self._tox.iterate() + time.sleep(self._tox.iteration_interval() / 1000) class ToxAVIterateThread(BaseQThread): + def __init__(self, toxav): super().__init__() self._toxav = toxav def run(self): - LOG_DEBUG('ToxAVIterateThread run: ') while not self._stop_thread: self._toxav.iterate() - sleep(self._toxav.iteration_interval() / 1000) + time.sleep(self._toxav.iteration_interval() / 1000) +# ----------------------------------------------------------------------------------------------------------------- # File transfers thread +# ----------------------------------------------------------------------------------------------------------------- class FileTransfersThread(BaseQThread): def __init__(self): - super().__init__('FileTransfers') + super().__init__() self._queue = queue.Queue() self._timeout = 0.01 @@ -179,12 +124,14 @@ class FileTransfersThread(BaseQThread): except queue.Empty: pass except queue.Full: - LOG_WARN('Queue is full in _thread') + util.log('Queue is full in _thread') except Exception as ex: - LOG_ERROR('in _thread: ' + str(ex)) + util.log('Exception in _thread: ' + str(ex)) _thread = FileTransfersThread() + + def start_file_transfer_thread(): _thread.start() @@ -197,7 +144,9 @@ 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()) diff --git a/toxygen/middleware/tox_factory.py b/toxygen/middleware/tox_factory.py index 1216dd8..9ee5c01 100644 --- a/toxygen/middleware/tox_factory.py +++ b/toxygen/middleware/tox_factory.py @@ -1,89 +1,34 @@ -# -*- 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 +import wrapper.tox +import wrapper.toxcore_enums_and_consts as enums +import ctypes -def tox_factory(data=None, settings=None, args=None, app=None): +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 not settings: - LOG_WARN("tox_factory using get_default_settings") + if settings is None: 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 + tox_options = wrapper.tox.Tox.options_new() + tox_options.contents.udp_enabled = settings['udp_enabled'] + tox_options.contents.proxy_type = settings['proxy_type'] + tox_options.contents.proxy_host = bytes(settings['proxy_host'], 'UTF-8') + tox_options.contents.proxy_port = settings['proxy_port'] + tox_options.contents.start_port = settings['start_port'] + tox_options.contents.end_port = settings['end_port'] + tox_options.contents.tcp_port = settings['tcp_port'] + tox_options.contents.local_discovery_enabled = settings['lan_discovery'] + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = ctypes.c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 - # 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 + return wrapper.tox.Tox(tox_options) diff --git a/toxygen/network/tox_dns.py b/toxygen/network/tox_dns.py index 58c9da1..02e97f5 100644 --- a/toxygen/network/tox_dns.py +++ b/toxygen/network/tox_dns.py @@ -1,44 +1,20 @@ -# -*- 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 +from PyQt5 import QtNetwork, QtCore -global LOG - -iTIMEOUT=60 -LOG = logging.getLogger('app.'+__name__) class ToxDns: - def __init__(self, settings, log=None): + def __init__(self, settings): 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')) + 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: @@ -53,25 +29,12 @@ class ToxDns: 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 + if 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)) + util.log('TOX DNS ERROR: ' + str(ex)) else: # proxy netman = QtNetwork.QNetworkAccessManager() proxy = QtNetwork.QNetworkProxy() @@ -97,6 +60,6 @@ class ToxDns: if not result['c']: return result['tox_id'] except Exception as ex: - LOG.error('ERROR: TOX DNS ' + str(ex)) + util.log('TOX DNS ERROR: ' + str(ex)) return None # error diff --git a/toxygen/notifications/sound.py b/toxygen/notifications/sound.py index 9df46b2..361cd05 100644 --- a/toxygen/notifications/sound.py +++ b/toxygen/notifications/sound.py @@ -1,17 +1,8 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os.path -import wave - import utils.util +import wave +import pyaudio +import os.path -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, @@ -34,19 +25,9 @@ class AudioFile: 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 + while data: + self.stream.write(data) + data = self.wf.readframes(self.chunk) def close(self): self.stream.close() diff --git a/toxygen/notifications/tray.py b/toxygen/notifications/tray.py index 0a6bca3..4232253 100644 --- a/toxygen/notifications/tray.py +++ b/toxygen/notifications/tray.py @@ -1,6 +1,5 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from PyQt5 import QtCore, QtWidgets -from qtpy import QtCore, QtWidgets def tray_notification(title, text, tray, window): """ @@ -11,7 +10,7 @@ def tray_notification(title, text, tray, window): :param tray: ref to tray icon :param window: main window """ - if tray and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + if QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): if len(text) > 30: text = text[:27] + '...' tray.showMessage(title, text, QtWidgets.QSystemTrayIcon.NoIcon, 3000) diff --git a/toxygen/plugin_support/plugin_support.py b/toxygen/plugin_support/plugin_support.py index f180e4d..ed45910 100644 --- a/toxygen/plugin_support/plugin_support.py +++ b/toxygen/plugin_support/plugin_support.py @@ -1,20 +1,10 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import utils.util as util import os import importlib import inspect -import sys -import logging - -import utils.util as util import plugins.plugin_super_class as pl +import sys -# 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: @@ -43,65 +33,54 @@ class PluginLoader: self._app = app self._plugins = {} # dict. key - plugin unique short name, value - Plugin instance - def set_tox(self, tox) -> None: + def set_tox(self, tox): """ New tox instance """ for plugin in self._plugins.values(): plugin.instance.set_tox(tox) - def load(self) -> None: + def load(self): """ 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) + util.log('Plugin dir not found') return - - sys.path.append(path) + 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 - base_name = fl[:-3] # module name without .py + 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) + module = importlib.import_module(name) # import plugin + except ImportError: + util.log('Import error in module ' + name) continue except Exception as ex: - LOG.error('importing ' + base_name + ' Exception: ' + str(ex)) + util.log('Exception in module ' + name + ' Exception: ' + str(ex)) continue for elem in dir(module): obj = getattr(module, elem) # looking for plugin class in module if not inspect.isclass(obj) or not hasattr(obj, 'is_plugin') or not obj.is_plugin: continue + print('Plugin', elem) try: # create instance of plugin class - instance = obj(self._app) # 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'] + instance = obj(self._app) + is_active = instance.get_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) + instance.start() except Exception as ex: - LOG.error('in module ' + short_name + ' Exception: ' + str(ex)) + util.log('Exception in module ' + 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) + self._plugins[instance.get_short_name()] = Plugin(instance, is_active) break - def callback_lossless(self, friend_number, data) -> None: + def callback_lossless(self, friend_number, data): """ New incoming custom lossless packet (callback) """ @@ -119,7 +98,7 @@ class PluginLoader: 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: + def friend_online(self, friend_number): """ Friend with specified number is online """ @@ -127,7 +106,7 @@ class PluginLoader: if plugin.is_active: plugin.instance.friend_connected(friend_number) - def get_plugins_list(self) -> list: + def get_plugins_list(self): """ Returns list of all plugins """ @@ -135,7 +114,7 @@ class PluginLoader: for plugin in self._plugins.values(): try: result.append([plugin.instance.get_name(), # plugin full name - plugin.is_active, # is enabled + plugin.is_active, # is enabled plugin.instance.get_description(), # plugin description plugin.instance.get_short_name()]) # key - short unique name except: @@ -147,15 +126,9 @@ class PluginLoader: """ 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 self._plugins[key].instance.get_window() - return None - - def toggle_plugin(self, key) -> None: + def toggle_plugin(self, key): """ Enable/disable plugin :param key: plugin short name @@ -172,7 +145,7 @@ class PluginLoader: self._settings['plugins'].remove(key) self._settings.save() - def command(self, text) -> None: + def command(self, text): """ New command for plugin """ @@ -189,7 +162,6 @@ class PluginLoader: for plugin in self._plugins.values(): if not plugin.is_active: continue - try: result.extend(plugin.instance.get_menu(num)) except: @@ -201,17 +173,13 @@ class PluginLoader: for plugin in self._plugins.values(): if not plugin.is_active: continue - if not hasattr(plugin.instance, 'get_message_menu'): - name = plugin.instance.get_short_name() - self._app._log('WARN: get_message_menu not found: ' + name) - continue try: result.extend(plugin.instance.get_message_menu(menu, selected_text)) except: pass return result - def stop(self) -> None: + def stop(self): """ App is closing, stop all plugins """ @@ -220,12 +188,7 @@ class PluginLoader: 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 - + def reload(self): + print('Reloading plugins') 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..0056d36 100644 --- a/toxygen/plugins/plugin_super_class.py +++ b/toxygen/plugins/plugin_super_class.py @@ -1,17 +1,16 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import os - -from qtpy import QtCore, QtWidgets - +from PyQt5 import QtCore, QtWidgets import utils.ui as util_ui import common.tox_save as tox_save + MAX_SHORT_NAME_LENGTH = 5 LOSSY_FIRST_BYTE = 200 LOSSLESS_FIRST_BYTE = 160 + def path_to_data(name): """ :param name: plugin unique name @@ -20,7 +19,7 @@ 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 @@ -48,12 +47,14 @@ class PluginSuperClass(tox_save.ToxSave): 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 + # ----------------------------------------------------------------------------------------------------------------- # Get methods + # ----------------------------------------------------------------------------------------------------------------- def get_name(self): """ @@ -73,7 +74,7 @@ class PluginSuperClass(tox_save.ToxSave): """ return self.__doc__ - def get_menu(self, menu, row_number=None): + def get_menu(self, row_number): """ This method creates items for menu which called on right click in list of friends :param row_number: number of selected row in list of contacts @@ -96,7 +97,9 @@ class PluginSuperClass(tox_save.ToxSave): """ return None + # ----------------------------------------------------------------------------------------------------------------- # Plugin was stopped, started or new command received + # ----------------------------------------------------------------------------------------------------------------- def start(self): """ @@ -126,7 +129,9 @@ class PluginSuperClass(tox_save.ToxSave): title = util_ui.tr('List of commands for plugin {}').format(self._name) util_ui.message_box(text, title) + # ----------------------------------------------------------------------------------------------------------------- # Translations support + # ----------------------------------------------------------------------------------------------------------------- def load_translator(self): """ @@ -143,7 +148,9 @@ 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): """ @@ -162,7 +169,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 +189,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/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/smileys.py b/toxygen/smileys/smileys.py index 604e681..0391856 100644 --- a/toxygen/smileys/smileys.py +++ b/toxygen/smileys/smileys.py @@ -1,18 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - +from utils import util import json -import logging import os from collections import OrderedDict +from PyQt5 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: """ @@ -40,7 +31,7 @@ class SmileyLoader: 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) @@ -51,7 +42,7 @@ 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 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/stickers.py b/toxygen/stickers/stickers.py index 56a0a29..14142c7 100644 --- a/toxygen/stickers/stickers.py +++ b/toxygen/stickers/stickers.py @@ -1,8 +1,7 @@ -# -*- 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 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/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..6e05c3e 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 PyQt5 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/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/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/ui/av_widgets.py b/toxygen/ui/av_widgets.py index e750231..e5773a8 100644 --- a/toxygen/ui/av_widgets.py +++ b/toxygen/ui/av_widgets.py @@ -1,18 +1,9 @@ -# -*- 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 PyQt5 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 +import pyaudio +import wave -global LOG -LOG = logging.getLogger('app.'+__name__) class IncomingCallWidget(widgets.CenteredWidget): @@ -20,7 +11,7 @@ class IncomingCallWidget(widgets.CenteredWidget): super().__init__() self._settings = settings self._calls_manager = calls_manager - self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint) # | QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint) self.resize(QtCore.QSize(500, 270)) self.avatar_label = QtWidgets.QLabel(self) self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) @@ -54,9 +45,9 @@ class IncomingCallWidget(widgets.CenteredWidget): self.accept_audio.setIconSize(QtCore.QSize(150, 150)) self.accept_video.setIconSize(QtCore.QSize(140, 140)) self.decline.setIconSize(QtCore.QSize(140, 140)) - #self.accept_audio.setStyleSheet("QPushButton { border: none }") - #self.accept_video.setStyleSheet("QPushButton { border: none }") - #self.decline.setStyleSheet("QPushButton { border: none }") + self.accept_audio.setStyleSheet("QPushButton { border: none }") + self.accept_video.setStyleSheet("QPushButton { border: none }") + self.decline.setStyleSheet("QPushButton { border: none }") self.setWindowTitle(text) self.name.setText(name) self.call_type.setText(text) @@ -65,118 +56,75 @@ class IncomingCallWidget(widgets.CenteredWidget): 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'] + 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(), + 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(util.join_path(util.get_sounds_directory(), 'call.wav')) + self.a.play() + self.a.close() 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 + self.thread.wait() + self.close() 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 + self._calls_manager.accept_call(self._friend_number, True, False) + self.stop() 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() + self._calls_manager.accept_call(self._friend_number, True, True) + 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() + self._calls_manager.stop_call(self._friend_number, False) + 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 index bdff447..7a32284 100644 --- a/toxygen/ui/contact_items.py +++ b/toxygen/ui/contact_items.py @@ -1,7 +1,5 @@ -# -*- 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 wrapper.toxcore_enums_and_consts import * +from PyQt5 import QtCore, QtGui, QtWidgets from utils.util import * from ui.widgets import DataLabel @@ -11,7 +9,7 @@ class ContactItem(QtWidgets.QWidget): Contact in friends list """ - def __init__(self, settings, parent=None, kind='friend'): + def __init__(self, settings, parent=None): QtWidgets.QWidget.__init__(self, parent) mode = settings['compact_mode'] self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) @@ -32,11 +30,6 @@ class ContactItem(QtWidgets.QWidget): 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) diff --git a/toxygen/ui/create_profile_screen.py b/toxygen/ui/create_profile_screen.py index f507a1d..512c141 100644 --- a/toxygen/ui/create_profile_screen.py +++ b/toxygen/ui/create_profile_screen.py @@ -1,11 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - from ui.widgets import * +from PyQt5 import uic import utils.util as util import utils.ui as util_ui + class CreateProfileScreenResult: def __init__(self, save_into_default_folder, password): diff --git a/toxygen/ui/group_bans_widgets.py b/toxygen/ui/group_bans_widgets.py index 60f1e9e..b2758c7 100644 --- a/toxygen/ui/group_bans_widgets.py +++ b/toxygen/ui/group_bans_widgets.py @@ -1,11 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic, QtWidgets, QtCore - from ui.widgets import CenteredWidget +from PyQt5 import uic, QtWidgets, QtCore import utils.util as util import utils.ui as util_ui + class GroupBanItem(QtWidgets.QWidget): def __init__(self, ban, cancel_ban, can_cancel_ban, parent=None): @@ -24,20 +22,15 @@ class GroupBanItem(QtWidgets.QWidget): 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) + 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 _cancel_ban(self): + self._cancel_ban(self._ban.ban_id) - def can_cancel_ban(self): # pylint: disable=method-hidden - # FixMe missing - pass class GroupBansScreen(CenteredWidget): @@ -55,8 +48,7 @@ class GroupBansScreen(CenteredWidget): self._refresh_bans_list() def _retranslate_ui(self): -# self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) - pass + self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) def _refresh_bans_list(self): self.bansListWidget.clear() diff --git a/toxygen/ui/group_invites_widgets.py b/toxygen/ui/group_invites_widgets.py index 6b9fa9e..d35aca1 100644 --- a/toxygen/ui/group_invites_widgets.py +++ b/toxygen/ui/group_invites_widgets.py @@ -1,13 +1,7 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import logging -from qtpy import uic, QtWidgets - +from PyQt5 import uic, QtWidgets import utils.util as util from ui.widgets import * -global LOG -LOG = logging.getLogger('app') class GroupInviteItem(QtWidgets.QWidget): @@ -33,7 +27,6 @@ class GroupInvitesScreen(CenteredWidget): 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) @@ -75,11 +68,8 @@ class GroupInvitesScreen(CenteredWidget): 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() @@ -88,7 +78,6 @@ class GroupInvitesScreen(CenteredWidget): 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() @@ -101,7 +90,7 @@ class GroupInvitesScreen(CenteredWidget): 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(): + if item_widget.is_selected(): selected.append(all_invites[index]) return selected diff --git a/toxygen/ui/group_peers_list.py b/toxygen/ui/group_peers_list.py index ec8f95d..9d2632d 100644 --- a/toxygen/ui/group_peers_list.py +++ b/toxygen/ui/group_peers_list.py @@ -1,7 +1,6 @@ -# -*- 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 * +from wrapper.toxcore_enums_and_consts import * + class PeerItem(QtWidgets.QWidget): @@ -12,15 +11,12 @@ class PeerItem(QtWidgets.QWidget): self.nameLabel.setGeometry(5, 0, width - 5, 34) name = peer.name if peer.is_current_user: - name += util_ui.tr(' *') + name += util_ui.tr(' (You)') 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}' + style = 'QLabel {color: green}' elif peer.status == TOX_USER_STATUS['AWAY']: - style = 'QLabel {color: blue}' + style = 'QLabel {color: yellow}' else: style = 'QLabel {color: red}' self.nameLabel.setStyleSheet(style) diff --git a/toxygen/ui/group_settings_widgets.py b/toxygen/ui/group_settings_widgets.py index 5fd04d4..c32168b 100644 --- a/toxygen/ui/group_settings_widgets.py +++ b/toxygen/ui/group_settings_widgets.py @@ -1,8 +1,9 @@ from ui.widgets import CenteredWidget -from qtpy import uic +from PyQt5 import uic import utils.util as util import utils.ui as util_ui + class GroupManagementScreen(CenteredWidget): def __init__(self, groups_service, group): @@ -20,7 +21,6 @@ class GroupManagementScreen(CenteredWidget): 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): @@ -28,21 +28,12 @@ class GroupManagementScreen(CenteredWidget): 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() diff --git a/toxygen/ui/groups_widgets.py b/toxygen/ui/groups_widgets.py index ceda0ef..ad4b703 100644 --- a/toxygen/ui/groups_widgets.py +++ b/toxygen/ui/groups_widgets.py @@ -1,10 +1,8 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - +from PyQt5 import uic import utils.util as util from ui.widgets import * -from toxygen_wrapper.toxcore_enums_and_consts import * +from wrapper.toxcore_enums_and_consts import * + class BaseGroupScreen(CenteredWidget): diff --git a/toxygen/ui/items_factories.py b/toxygen/ui/items_factories.py index 530839c..7346f8f 100644 --- a/toxygen/ui/items_factories.py +++ b/toxygen/ui/items_factories.py @@ -1,8 +1,7 @@ -# -*- 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): @@ -46,29 +45,9 @@ class MessagesItemsFactory: 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) + item = InlineImageItem(message.data, self._messages.width(), elem, self._messages) elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) if append: self._messages.addItem(elem) @@ -102,7 +81,9 @@ class MessagesItemsFactory: 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, diff --git a/toxygen/ui/login_screen.py b/toxygen/ui/login_screen.py index 93362fd..35e33b5 100644 --- a/toxygen/ui/login_screen.py +++ b/toxygen/ui/login_screen.py @@ -1,12 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import os.path - -from qtpy import uic - from ui.widgets import * +from PyQt5 import uic import utils.util as util import utils.ui as util_ui +import os.path + class LoginScreenResult: diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py index f65a0af..5a510a5 100644 --- a/toxygen/ui/main_screen.py +++ b/toxygen/ui/main_screen.py @@ -1,187 +1,31 @@ -# -*- 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 +from PyQt5 import uic -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): + def __init__(self, settings, tray): 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._profile = self._toxes = self._messenger = None self._file_transfer_handler = self._history_loader = self._groups_service = self._calls_manager = None self._should_show_group_peers_list = False self.initUI() - 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): + file_transfer_handler, history_loader, calls_manager, groups_service, toxes): self._widget_factory = widget_factory self._tray = tray self._contacts_manager = contacts_manager @@ -192,7 +36,6 @@ class MainWindow(QtWidgets.QMainWindow): self._calls_manager = calls_manager self._groups_service = groups_service self._toxes = toxes - self._app = app self._messenger = messenger self._contacts_manager.active_contact_changed.add_callback(self._new_contact_selected) self.messageEdit.set_dependencies(messenger, contacts_manager, file_transfer_handler) @@ -208,19 +51,11 @@ class MainWindow(QtWidgets.QMainWindow): 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.menubar.setNativeMenuBar(False) + self.menubar.setMinimumSize(self.width(), 25) + self.menubar.setMaximumSize(self.width(), 25) + self.menubar.setBaseSize(self.width(), 25) + self.menuProfile = QtWidgets.QMenu(self.menubar) self.menuProfile = QtWidgets.QMenu(self.menubar) self.menuProfile.setObjectName("menuProfile") @@ -229,14 +64,13 @@ class MainWindow(QtWidgets.QMainWindow): 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 = QtWidgets.QMenu(self.menubar) 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.actionprofilesettings = QtWidgets.QAction(window) + self.actionprofilesettings.setObjectName("actionprofilesettings") self.actionPrivacy_settings = QtWidgets.QAction(window) self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") self.actionInterface_settings = QtWidgets.QAction(window) @@ -247,13 +81,6 @@ class MainWindow(QtWidgets.QMainWindow): self.actionNetwork.setObjectName("actionNetwork") self.actionAbout_program = QtWidgets.QAction(window) self.actionAbout_program.setObjectName("actionAbout_program") - - self.actionLog_console = QtWidgets.QAction(window) - self.actionLog_console.setObjectName("actionLog_console") - self.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") @@ -262,8 +89,6 @@ class MainWindow(QtWidgets.QMainWindow): self.pluginData = QtWidgets.QAction(window) self.importPlugin = QtWidgets.QAction(window) self.reloadPlugins = QtWidgets.QAction(window) - self.reloadToxchat = QtWidgets.QAction(window) - self.lockApp = QtWidgets.QAction(window) self.createGC = QtWidgets.QAction(window) self.joinGC = QtWidgets.QAction(window) @@ -272,30 +97,19 @@ class MainWindow(QtWidgets.QMainWindow): self.menuProfile.addAction(self.actionAdd_friend) self.menuProfile.addAction(self.actionSettings) self.menuProfile.addAction(self.lockApp) - self.menuProfile.addAction(self.actionTest_tox) - self.menuProfile.addAction(self.actionTest_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.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()) @@ -304,31 +118,22 @@ class MainWindow(QtWidgets.QMainWindow): 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.actionSettings.triggered.connect(self.profile_settings) self.actionPrivacy_settings.triggered.connect(self.privacy_settings) self.actionInterface_settings.triggered.connect(self.interface_settings) self.actionNotifications.triggered.connect(self.notification_settings) self.audioSettings.triggered.connect(self.audio_settings) self.videoSettings.triggered.connect(self.video_settings) -## self.updateSettings.triggered.connect(self.update_settings) + self.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): @@ -336,17 +141,10 @@ class MainWindow(QtWidgets.QMainWindow): 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._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")) @@ -359,26 +157,18 @@ class MainWindow(QtWidgets.QMainWindow): 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.actionprofilesettings.setText(util_ui.tr("Profile")) self.actionPrivacy_settings.setText(util_ui.tr("Privacy")) self.actionInterface_settings.setText(util_ui.tr("Interface")) self.actionNotifications.setText(util_ui.tr("Notifications")) self.actionNetwork.setText(util_ui.tr("Network")) self.actionAbout_program.setText(util_ui.tr("About program")) - self.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")) @@ -390,7 +180,6 @@ class MainWindow(QtWidgets.QMainWindow): 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) @@ -398,7 +187,6 @@ class MainWindow(QtWidgets.QMainWindow): 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) @@ -621,7 +409,6 @@ class MainWindow(QtWidgets.QMainWindow): self.peers_list.setGeometry(width * 3 // 4, 0, width - width * 3 // 4, self.height() - 155) invites_button_visible = self.groupInvitesPushButton.isVisible() -# LOG.debug(f"invites_button_visible={invites_button_visible}") self.friends_list.setGeometry(0, 125 if invites_button_visible else 100, 270, self.height() - 150 if invites_button_visible else self.height() - 125) @@ -654,133 +441,14 @@ class MainWindow(QtWidgets.QMainWindow): 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' + text = util_ui.tr('Toxygen is Tox client written on Python.\nVersion: ') + text += '' + '\nGitHub: https://github.com/toxygen-project/toxygen/' title = util_ui.tr('About') util_ui.message_box(text, title) @@ -836,16 +504,13 @@ class MainWindow(QtWidgets.QMainWindow): self._modal_window.show() def reload_plugins(self): - if hasattr(self, '_plugin_loader') and self._plugin_loader is not None: + if 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): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugin')) + if directory: src = directory + '/' dest = util.get_plugins_directory() util.copy(src, dest) @@ -858,25 +523,6 @@ class MainWindow(QtWidgets.QMainWindow): else: util_ui.message_box(util_ui.tr('Error. Profile password is not set.'), util_ui.tr("Cannot lock app")) - def test_tox(self): - self._app._test_tox() - - def test_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) @@ -886,7 +532,9 @@ class MainWindow(QtWidgets.QMainWindow): 120)) self.menu.show() + # ----------------------------------------------------------------------------------------------------------------- # Messages, calls and file transfers + # ----------------------------------------------------------------------------------------------------------------- def send_message(self): self._messenger.send_message() @@ -949,7 +597,9 @@ class MainWindow(QtWidgets.QMainWindow): 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) @@ -1006,7 +656,9 @@ class MainWindow(QtWidgets.QMainWindow): 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() @@ -1029,13 +681,9 @@ class MainWindow(QtWidgets.QMainWindow): 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 + if self._contacts_manager.get_curr_friend() is None: + return self.search_field = self._widget_factory.create_search_screen(self.messages) x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40 self.search_field.setGeometry(x, y, self.messages.width(), 40) @@ -1064,10 +712,7 @@ class MainWindow(QtWidgets.QMainWindow): 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.setVisible(invites_count > 0) + text = util_ui.tr('{} new invites to group chats').format(invites_count) self.groupInvitesPushButton.setText(text) self.resizeEvent() diff --git a/toxygen/ui/main_screen_widgets.py b/toxygen/ui/main_screen_widgets.py index 064cb09..122561b 100644 --- a/toxygen/ui/main_screen_widgets.py +++ b/toxygen/ui/main_screen_widgets.py @@ -1,26 +1,18 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - +from PyQt5 import QtCore, QtGui, QtWidgets +from ui.widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit 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._messenger = self._contacts_manager = self._file_transfer_handler = None self.parent = form self.setAcceptDrops(True) self._timer = QtCore.QTimer(self) @@ -39,7 +31,6 @@ class MessageArea(QtWidgets.QPlainTextEdit): 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: @@ -47,22 +38,15 @@ class MessageArea(QtWidgets.QPlainTextEdit): 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}")) - + self._messenger.send_typing(False) + self._messenger.send_message() 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] + current_word = re.split("\s+", text[:pos])[-1] start_index = text.rindex(current_word, 0, pos) peer_name = self._contacts_manager.get_gc_peer_name(current_word) self.setPlainText(text[:start_index] + peer_name + text[pos:]) @@ -396,8 +380,8 @@ class MainMenuButton(QtWidgets.QPushButton): class ClickableLabel(QtWidgets.QLabel): - # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' - clicked = Signal() + + clicked = QtCore.pyqtSignal() def __init__(self, *args): super().__init__(*args) diff --git a/toxygen/ui/menu.py b/toxygen/ui/menu.py index 9f0fc05..8aec578 100644 --- a/toxygen/ui/menu.py +++ b/toxygen/ui/menu.py @@ -1,35 +1,23 @@ -# -*- 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 PyQt5 import QtCore, QtGui, QtWidgets, uic from user_data.settings import * from utils.util import * from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow +import pyaudio import updater.updater as updater import utils.ui as util_ui -from user_data import settings +import cv2 -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=''): + def __init__(self, settings, contacts_manager, tox_id=''): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings + self._settings = settings self._contacts_manager = contacts_manager uic.loadUi(get_views_path('add_contact_screen'), self) self._update_ui(tox_id) self._adding = False - self._bootstrap = False def _update_ui(self, tox_id): self.toxIdLineEdit = LineEdit(self) @@ -38,15 +26,8 @@ class AddContact(CenteredWidget): 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 @@ -59,10 +40,9 @@ class AddContact(CenteredWidget): self._adding = False if send is True: # request was successful - pass - elif send and type(send) == str: # print error data + self.close() + else: # print error data self.errorLabel.setText(send) - self.close() def _retranslate_ui(self): self.setWindowTitle(util_ui.tr('Add contact')) @@ -71,61 +51,12 @@ class AddContact(CenteredWidget): 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): + def __init__(self, settings, reset): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings + self._settings = settings self._reset = reset uic.loadUi(get_views_path('network_settings_screen'), self) self._update_ui() @@ -137,20 +68,16 @@ class NetworkSettings(CenteredWidget): self.portLineEdit = LineEdit(self) self.portLineEdit.setGeometry(100, 325, 270, 30) - self.urlLineEdit = LineEdit(self) - self.urlLineEdit.setGeometry(100, 370, 270, 30) - self.restartCorePushButton.clicked.connect(self._restart_core) self.ipv6CheckBox.setChecked(self._settings['ipv6_enabled']) self.udpCheckBox.setChecked(self._settings['udp_enabled']) self.proxyCheckBox.setChecked(self._settings['proxy_type']) self.ipLineEdit.setText(self._settings['proxy_host']) self.portLineEdit.setText(str(self._settings['proxy_port'])) - self.urlLineEdit.setText(str(self._settings['download_nodes_url'])) self.httpProxyRadioButton.setChecked(self._settings['proxy_type'] == 1) self.socksProxyRadioButton.setChecked(self._settings['proxy_type'] != 1) self.downloadNodesCheckBox.setChecked(self._settings['download_nodes_list']) - self.lanCheckBox.setChecked(self._settings['local_discovery_enabled']) + self.lanCheckBox.setChecked(self._settings['lan_discovery']) self._retranslate_ui() self.proxyCheckBox.stateChanged.connect(lambda x: self._activate_proxy()) self._activate_proxy() @@ -163,13 +90,11 @@ class NetworkSettings(CenteredWidget): self.proxyCheckBox.setText(util_ui.tr("Proxy")) self.ipLabel.setText(util_ui.tr("IP:")) self.portLabel.setText(util_ui.tr("Port:")) - self.urlLabel.setText(util_ui.tr("ChatUrl:")) self.restartCorePushButton.setText(util_ui.tr("Restart TOX core")) self.httpProxyRadioButton.setText(util_ui.tr("HTTP")) self.socksProxyRadioButton.setText(util_ui.tr("Socks 5")) self.downloadNodesCheckBox.setText(util_ui.tr("Download nodes list from tox.chat")) -# self.warningLabel.setText(util_ui.tr("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) - self.warningLabel.setText(util_ui.tr("Changing settings require 'Restart TOX core'")) + self.warningLabel.setText(util_ui.tr("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) def _activate_proxy(self): bl = self.proxyCheckBox.isChecked() @@ -188,28 +113,26 @@ class NetworkSettings(CenteredWidget): self._settings['proxy_type'] = 2 - int(self.httpProxyRadioButton.isChecked()) if proxy_enabled else 0 self._settings['proxy_host'] = str(self.ipLineEdit.text()) self._settings['proxy_port'] = int(self.portLineEdit.text()) - self._settings['download_nodes_url'] = str(self.urlLineEdit.text()) self._settings['download_nodes_list'] = self.downloadNodesCheckBox.isChecked() - self._settings['local_discovery_enabled'] = self.lanCheckBox.isChecked() + self._settings['lan_discovery'] = 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)) + log('Exception in restart: ' + str(ex)) class PrivacySettings(CenteredWidget): """Privacy settings form: history, typing notifications""" - def __init__(self, contacts_manager, dsettings): + def __init__(self, contacts_manager, settings): """ :type contacts_manager: ContactsManager """ super().__init__() - self._app = QtWidgets.QApplication.instance() self._contacts_manager = contacts_manager - self._settings = dsettings + self._settings = settings self.initUI() self.center() @@ -324,10 +247,9 @@ class PrivacySettings(CenteredWidget): class NotificationsSettings(CenteredWidget): """Notifications settings form""" - def __init__(self, dsettings): + def __init__(self, setttings): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings # pylint: disable=undefined-variable + self._settings = setttings uic.loadUi(get_views_path('notifications_settings_screen'), self) self._update_ui() self.center() @@ -357,10 +279,9 @@ class NotificationsSettings(CenteredWidget): class InterfaceSettings(CenteredWidget): """Interface settings form""" - def __init__(self, dsettings, smiley_loader): + def __init__(self, settings, smiley_loader): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings + self._settings = settings self._smiley_loader = smiley_loader uic.loadUi(get_views_path('interface_settings_screen'), self) @@ -368,10 +289,10 @@ class InterfaceSettings(CenteredWidget): self.center() def _update_ui(self): - themes = list(settings.built_in_themes().keys()) + themes = list(self._settings.built_in_themes().keys()) self.themeComboBox.addItems(themes) theme = self._settings['theme'] - if theme in settings.built_in_themes().keys(): + if theme in self._settings.built_in_themes().keys(): index = themes.index(theme) else: index = 0 @@ -391,10 +312,10 @@ class InterfaceSettings(CenteredWidget): 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) + app_closing_setting = self._settings['close_app'] + self.closeRadioButton.setChecked(app_closing_setting == 0) + self.hideRadioButton.setChecked(app_closing_setting == 1) + self.closeToTrayRadioButton.setChecked(app_closing_setting == 2) self.compactModeCheckBox.setChecked(self._settings['compact_mode']) self.showAvatarsCheckBox.setChecked(self._settings['show_avatars']) @@ -416,7 +337,7 @@ class InterfaceSettings(CenteredWidget): self.closeRadioButton.setText(util_ui.tr("Close app")) self.hideRadioButton.setText(util_ui.tr("Hide app")) self.closeToTrayRadioButton.setText(util_ui.tr("Close to tray")) -# self.mirrorModeCheckBox.setText(util_ui.tr("Mirror mode")) + self.mirrorModeCheckBox.setText(util_ui.tr("Mirror mode")) self.compactModeCheckBox.setText(util_ui.tr("Compact contact list")) self.importSmileysPushButton.setText(util_ui.tr("Import smiley pack")) self.importStickersPushButton.setText(util_ui.tr("Import sticker pack")) @@ -439,23 +360,24 @@ class InterfaceSettings(CenteredWidget): copy(src, dest) def closeEvent(self, event): + app = QtWidgets.QApplication.instance() self._settings['theme'] = str(self.themeComboBox.currentText()) try: theme = self._settings['theme'] - styles_path = join_path(get_styles_directory(), settings.built_in_themes()[theme]) + styles_path = join_path(get_styles_directory(), self._settings.built_in_themes()[theme]) with open(styles_path) as fl: style = fl.read() - self._app.setStyleSheet(style) + app.setStyleSheet(style) except IsADirectoryError: pass self._settings['smileys'] = self.smileysCheckBox.isChecked() restart = False -# if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): -# self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() -# restart = True + if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): + self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() + restart = True if self._settings['compact_mode'] != self.compactModeCheckBox.isChecked(): self._settings['compact_mode'] = self.compactModeCheckBox.isChecked() @@ -472,9 +394,9 @@ class InterfaceSettings(CenteredWidget): 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.removeTranslator(app.translator) + app.translator.load(join_path(get_translations_directory(), path)) + app.installTranslator(app.translator) app_closing_setting = 0 if self.hideRadioButton.isChecked(): @@ -493,28 +415,21 @@ class AudioSettings(CenteredWidget): Audio calls settings form """ - def __init__(self, dsettings): + def __init__(self, settings): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings + self._settings = settings self._in_indexes = self._out_indexes = None uic.loadUi(get_views_path('audio_settings_screen'), self) self._update_ui() self.center() def closeEvent(self, event): - 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() + 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 + 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) @@ -524,11 +439,8 @@ class AudioSettings(CenteredWidget): 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.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings.audio['input'])) + self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings.audio['output'])) self._retranslate_ui() def _retranslate_ui(self): @@ -551,13 +463,12 @@ class DesktopAreaSelectionWindow(RubberBandWindow): class VideoSettings(CenteredWidget): """ - Video calls settings form + Audio calls settings form """ - def __init__(self, dsettings): + def __init__(self, settings): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings + self._settings = settings uic.loadUi(get_views_path('video_settings_screen'), self) self._devices = self._frame_max_sizes = None self._update_ui() @@ -568,35 +479,24 @@ class VideoSettings(CenteredWidget): 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}") + self._settings.video['device'] = self.devices[self.input.currentIndex()] 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.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)) + print('Saving video settings error: ' + str(ex)) def save(self, x, y, width, height): self.desktopAreaSelection = None - self._settings['video']['device'] = -1 - self._settings['video']['width'] = width - self._settings['video']['height'] = height - self._settings['video']['x'] = x - self._settings['video']['y'] = y + self._settings.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] @@ -605,35 +505,23 @@ class VideoSettings(CenteredWidget): 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 + for i in range(10): + v = cv2.VideoCapture(i) + if v.isOpened(): + v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) + v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) - width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) # 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'] + width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT)) + del v + self._devices.append(i) + self._frame_max_sizes.append((width, height)) + self.deviceComboBox.addItem(util_ui.tr('Device #') + str(i)) try: - index = self._devices.index(iIndex) + index = self._devices.index(self._settings.video['device']) 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] - + except: + print('Video devices error!') self._retranslate_ui() def _retranslate_ui(self): @@ -647,7 +535,7 @@ class VideoSettings(CenteredWidget): def _device_changed(self): index = self.deviceComboBox.currentIndex() self.selectRegionPushButton.setVisible(index == 0) - self.resolutionComboBox.setVisible(True) # index != 0 + self.resolutionComboBox.setVisible(index != 0) width, height = self._frame_max_sizes[index] self.resolutionComboBox.clear() dims = [ @@ -671,7 +559,6 @@ class PluginsSettings(CenteredWidget): def __init__(self, plugin_loader): super().__init__() - self._app = QtWidgets.QApplication.instance() self._plugin_loader = plugin_loader self._window = None self.initUI() @@ -702,24 +589,14 @@ class PluginsSettings(CenteredWidget): 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')) + plugin = self.data[ind] + window = self.pl_loader.plugin_window(plugin[-1]) + if window is not None: + self._window = window + self._window.show() + else: + util_ui.message_box(util_ui.tr('No GUI found for this plugin'), util_ui.tr('Error')) def update_list(self): self.comboBox.clear() @@ -758,10 +635,9 @@ class UpdateSettings(CenteredWidget): Updates settings form """ - def __init__(self, dsettings, version): + def __init__(self, settings, version): super().__init__() - self._app = QtWidgets.QApplication.instance() - self._settings = dsettings + self._settings = settings self._version = version uic.loadUi(get_views_path('update_settings_screen'), self) self._update_ui() diff --git a/toxygen/ui/messages_widgets.py b/toxygen/ui/messages_widgets.py index 2c0ddfb..8a46fd0 100644 --- a/toxygen/ui/messages_widgets.py +++ b/toxygen/ui/messages_widgets.py @@ -1,18 +1,14 @@ -# -*- 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 * +from wrapper.toxcore_enums_and_consts import * import ui.widgets as widgets import utils.util as util import ui.menu as menu +import html as h +import re 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): @@ -43,16 +39,7 @@ class MessageBrowser(QtWidgets.QTextBrowser): 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.resize(width, self.document().size().height()) self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) self.anchorClicked.connect(self.on_anchor_clicked) @@ -94,8 +81,7 @@ class MessageBrowser(QtWidgets.QTextBrowser): 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.frameChanged[int].connect(lambda x: self.animate(movie)) movie.start() def animate(self, movie): diff --git a/toxygen/ui/password_screen.py b/toxygen/ui/password_screen.py index 57f7b95..bbae7ff 100644 --- a/toxygen/ui/password_screen.py +++ b/toxygen/ui/password_screen.py @@ -1,13 +1,7 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import logging -from qtpy import QtCore, QtWidgets - from ui.widgets import CenteredWidget, LineEdit, DialogWithResult +from PyQt5 import QtCore, QtWidgets import utils.ui as util_ui -global LOG -LOG = logging.getLogger('app.'+__name__) class PasswordArea(LineEdit): @@ -84,7 +78,6 @@ class PasswordScreen(PasswordScreenBase): new_data = self._encrypt.pass_decrypt(self._data) except Exception as ex: self.warning.setVisible(True) - LOG.error(f"Decryption error: {ex}") print('Decryption error:', ex) else: self.close_with_result(new_data) diff --git a/toxygen/ui/peer_screen.py b/toxygen/ui/peer_screen.py index 6bca903..8f2d5ba 100644 --- a/toxygen/ui/peer_screen.py +++ b/toxygen/ui/peer_screen.py @@ -1,12 +1,9 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - from ui.widgets import CenteredWidget +from PyQt5 import uic import utils.util as util import utils.ui as util_ui from ui.contact_items import * -import toxygen_wrapper.toxcore_enums_and_consts as consts +import wrapper.toxcore_enums_and_consts as consts class PeerScreen(CenteredWidget): @@ -32,11 +29,9 @@ class PeerScreen(CenteredWidget): 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.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) @@ -45,7 +40,7 @@ class PeerScreen(CenteredWidget): self.rolesComboBox.setVisible(can_change_role_or_ban) self.roleNameLabel.setVisible(not can_change_role_or_ban) self.banGroupBox.setEnabled(can_change_role_or_ban) -# self.banPushButton.clicked.connect(self._ban_peer) + self.banPushButton.clicked.connect(self._ban_peer) self.kickPushButton.clicked.connect(self._kick_peer) self._retranslate_ui() @@ -58,7 +53,7 @@ class PeerScreen(CenteredWidget): self.roleLabel.setText(util_ui.tr('Role:')) self.copyPublicKeyPushButton.setText(util_ui.tr('Copy public key')) self.sendPrivateMessagePushButton.setText(util_ui.tr('Send private message')) -# self.banPushButton.setText(util_ui.tr('Ban peer')) + self.banPushButton.setText(util_ui.tr('Ban peer')) self.kickPushButton.setText(util_ui.tr('Kick peer')) self.banGroupBox.setTitle(util_ui.tr('Ban peer')) self.ipBanRadioButton.setText(util_ui.tr('IP')) diff --git a/toxygen/ui/profile_settings_screen.py b/toxygen/ui/profile_settings_screen.py index 5b4658f..2e55d3d 100644 --- a/toxygen/ui/profile_settings_screen.py +++ b/toxygen/ui/profile_settings_screen.py @@ -1,11 +1,9 @@ -# -*- 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 +from PyQt5 import QtGui, QtCore, uic + class ProfileSettings(CenteredWidget): """Form with profile settings such as name, status, TOX ID""" @@ -31,7 +29,7 @@ class ProfileSettings(CenteredWidget): self._auto = Settings.get_auto_profile() == self._profile_manager.get_path() self.toxIdLabel.setText(self._profile.tox_id) self.nameLineEdit.setText(self._profile.name) - self.statusMessageLineEdit.setText(str(self._profile.status_message)) + self.statusMessageLineEdit.setText(self._profile.status_message) self.defaultProfilePushButton.clicked.connect(self._toggle_auto_profile) self.copyToxIdPushButton.clicked.connect(self._copy_tox_id) self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) @@ -71,7 +69,7 @@ class ProfileSettings(CenteredWidget): self.statusComboBox.addItem(util_ui.tr("Online")) self.statusComboBox.addItem(util_ui.tr("Away")) self.statusComboBox.addItem(util_ui.tr("Busy")) - self.copyPublicKeyPushButton.setText(util_ui.tr("Copy public key" +' (64)')) + self.copyPublicKeyPushButton.setText(util_ui.tr("Copy public key")) self._set_default_profile_button_text() @@ -146,7 +144,7 @@ class ProfileSettings(CenteredWidget): reply = util_ui.question(util_ui.tr('Do you want to move your profile to this location?'), util_ui.tr('Use new path')) - + self._settings.export(directory) self._profile.export_db(directory) self._profile_manager.export_profile(self._settings, directory, reply) diff --git a/toxygen/ui/self_peer_screen.py b/toxygen/ui/self_peer_screen.py index 7f30653..cf252d3 100644 --- a/toxygen/ui/self_peer_screen.py +++ b/toxygen/ui/self_peer_screen.py @@ -1,8 +1,5 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import uic - from ui.widgets import CenteredWidget, LineEdit +from PyQt5 import uic import utils.util as util import utils.ui as util_ui from ui.contact_items import * diff --git a/toxygen/ui/tray.py b/toxygen/ui/tray.py index c838a0d..3bfc7f3 100644 --- a/toxygen/ui/tray.py +++ b/toxygen/ui/tray.py @@ -1,17 +1,13 @@ -# -*- 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 PyQt5 import QtWidgets, QtGui, QtCore from utils.ui import tr from utils.util import * from ui.password_screen import UnlockAppScreen import os.path + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): - # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' - leftClicked = Signal() + + leftClicked = QtCore.pyqtSignal() def __init__(self, icon, parent=None): super().__init__(icon, parent) 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/group_management_screen.ui b/toxygen/ui/views/group_management_screen.ui index de7c21e..859754b 100644 --- a/toxygen/ui/views/group_management_screen.ui +++ b/toxygen/ui/views/group_management_screen.ui @@ -7,7 +7,7 @@ 0 0 658 - 283 + 238 @@ -91,24 +91,11 @@ - - - - 20 - 180 - 300 - 41 - - - - PushButton - - 20 - 220 + 180 611 41 diff --git a/toxygen/ui/views/interface_settings_screen.ui b/toxygen/ui/views/interface_settings_screen.ui index b762903..fb0bcf1 100644 --- a/toxygen/ui/views/interface_settings_screen.ui +++ b/toxygen/ui/views/interface_settings_screen.ui @@ -77,7 +77,6 @@ TextLabel - diff --git a/toxygen/ui/views/login_screen.ui b/toxygen/ui/views/login_screen.ui index d100803..50ca1e0 100644 --- a/toxygen/ui/views/login_screen.ui +++ b/toxygen/ui/views/login_screen.ui @@ -36,6 +36,7 @@ + Garuda 16 75 true diff --git a/toxygen/ui/views/network_settings_screen.ui b/toxygen/ui/views/network_settings_screen.ui index f6e2960..aacf1e0 100644 --- a/toxygen/ui/views/network_settings_screen.ui +++ b/toxygen/ui/views/network_settings_screen.ui @@ -7,19 +7,19 @@ 0 0 400 - 545 + 500 400 - 545 + 500 400 - 545 + 500 @@ -135,27 +135,11 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - 30 - 380 - 60 - 20 - - - - Chat Url - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - 30 - 430 + 370 340 40 @@ -181,7 +165,7 @@ 30 - 480 + 420 340 65 diff --git a/toxygen/ui/views/peer_screen.ui b/toxygen/ui/views/peer_screen.ui index 086dd18..e8e9e31 100644 --- a/toxygen/ui/views/peer_screen.ui +++ b/toxygen/ui/views/peer_screen.ui @@ -76,7 +76,6 @@ GroupBox - diff --git a/toxygen/ui/views/profile_settings_screen.ui b/toxygen/ui/views/profile_settings_screen.ui index 1c899ab..ece0083 100644 --- a/toxygen/ui/views/profile_settings_screen.ui +++ b/toxygen/ui/views/profile_settings_screen.ui @@ -7,7 +7,7 @@ 0 0 900 - 680 + 702 @@ -19,7 +19,7 @@ 30 10 161 - 30 + 31 @@ -32,7 +32,7 @@ 30 90 161 - 30 + 31 @@ -45,7 +45,7 @@ 30 50 421 - 30 + 31 @@ -55,7 +55,7 @@ 30 130 421 - 30 + 31 @@ -65,7 +65,7 @@ 520 30 311 - 30 + 31 @@ -75,7 +75,7 @@ 40 180 131 - 20 + 21 @@ -88,7 +88,7 @@ 40 210 831 - 60 + 61 @@ -104,7 +104,7 @@ 40 280 371 - 30 + 31 @@ -117,7 +117,7 @@ 440 280 371 - 30 + 31 @@ -130,7 +130,7 @@ 520 80 321 - 34 + 35 @@ -143,7 +143,7 @@ 520 130 321 - 34 + 35 @@ -156,7 +156,7 @@ 60 380 161 - 30 + 31 @@ -169,7 +169,7 @@ 50 420 421 - 30 + 31 @@ -179,7 +179,7 @@ 50 470 421 - 30 + 31 @@ -189,7 +189,7 @@ 500 420 381 - 20 + 21 @@ -202,7 +202,7 @@ 60 580 381 - 20 + 21 @@ -215,7 +215,7 @@ 40 630 831 - 34 + 35 @@ -228,7 +228,7 @@ 50 520 421 - 34 + 35 @@ -241,7 +241,7 @@ 500 470 381 - 20 + 21 @@ -254,7 +254,7 @@ 40 330 371 - 34 + 35 @@ -267,7 +267,7 @@ 440 330 371 - 34 + 35 diff --git a/toxygen/ui/widgets.py b/toxygen/ui/widgets.py index 78e9a0a..e7fe623 100644 --- a/toxygen/ui/widgets.py +++ b/toxygen/ui/widgets.py @@ -1,34 +1,18 @@ -# -*- 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 - +from PyQt5 import QtCore, QtGui, QtWidgets 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) - + text = ''.join('\u25AF' if len(bytes(c, 'utf-8')) >= 4 else c for c in text) + metrics = QtGui.QFontMetrics(self.font()) + text = metrics.elidedText(text, QtCore.Qt.ElideRight, self.width()) super().setText(text) + class ComboBox(QtWidgets.QComboBox): def __init__(self, *args): @@ -81,8 +65,8 @@ class QRightClickButton(QtWidgets.QPushButton): """ Button with right click support """ - # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' - rightClicked = Signal() + + rightClicked = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__(parent) diff --git a/toxygen/ui/widgets_factory.py b/toxygen/ui/widgets_factory.py index 08861a4..128e85e 100644 --- a/toxygen/ui/widgets_factory.py +++ b/toxygen/ui/widgets_factory.py @@ -1,4 +1,3 @@ -# -*- 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 * @@ -9,6 +8,7 @@ from ui.group_settings_widgets import * from ui.group_bans_widgets import * from ui.profile_settings_screen import ProfileSettings + class WidgetsFactory: def __init__(self, settings, profile, profile_manager, contacts_manager, file_transfer_handler, smiley_loader, @@ -42,11 +42,6 @@ class WidgetsFactory: 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): diff --git a/toxygen/updater/updater.py b/toxygen/updater/updater.py index 0eb81f3..329353c 100644 --- a/toxygen/updater/updater.py +++ b/toxygen/updater/updater.py @@ -1,26 +1,20 @@ -# -*- 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 +from PyQt5 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 + urllib.request.urlopen('http://216.58.192.142', timeout=1) # google.com return True except: return False + def updater_available(): if is_from_sources(): return os.path.exists(util.curr_directory() + '/toxygen_updater.py') @@ -76,11 +70,12 @@ def download(version): os.chdir(util.curr_directory()) url = get_url(version) params = get_params(url, version) - LOG.info('Updating Toxygen') + print('Updating Toxygen') + util.log('Updating Toxygen') try: subprocess.Popen(params) except Exception as ex: - LOG.error('running updater failed with ' + str(ex)) + util.log('Exception: running updater failed with ' + str(ex)) def send_request(version, settings): @@ -102,7 +97,7 @@ def send_request(version, settings): attr = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute) return attr is not None and 200 <= attr < 300 except Exception as ex: - LOG.error('TOXYGEN UPDATER ' + str(ex)) + util.log('TOXYGEN UPDATER ERROR: ' + str(ex)) return False diff --git a/toxygen/user_data/backup_service.py b/toxygen/user_data/backup_service.py index 9f3a051..bb0cef9 100644 --- a/toxygen/user_data/backup_service.py +++ b/toxygen/user_data/backup_service.py @@ -1,9 +1,7 @@ -# -*- 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): diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py index a6f5df0..05e2f2d 100644 --- a/toxygen/user_data/profile_manager.py +++ b/toxygen/user_data/profile_manager.py @@ -1,26 +1,16 @@ -# -*- 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 + def __init__(self, toxes, 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: @@ -28,14 +18,18 @@ class ProfileManager: 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: @@ -54,33 +48,22 @@ class ProfileManager: 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}") + with open(self._path, 'wb') as fl: + fl.write(data) + print('Profile saved successfully') self._profile_saved_event(data) def export_profile(self, settings, new_path, use_new_path): - profile_path = self._path.replace('.json', '.tox') - with open(profile_path, 'rb') as fin: + path = new_path + os.path.basename(self._path) + with open(self._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')) + print('Profile exported successfully') + util.copy(self._directory + 'avatars', new_path + 'avatars') if use_new_path: - profile_path = os.path.join(new_path, os.path.basename(profile_path)) + self._path = new_path + os.path.basename(self._path) self._directory = new_path settings.update_path(new_path) @@ -89,7 +72,7 @@ class ProfileManager: """ Find available tox profiles """ - path = get_user_config_path() + path = Settings.get_default_path() result = [] # check default path if not os.path.exists(path): diff --git a/toxygen/user_data/settings.py b/toxygen/user_data/settings.py index c87eec3..71422c2 100644 --- a/toxygen/user_data/settings.py +++ b/toxygen/user_data/settings.py @@ -1,208 +1,62 @@ -# -*- 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 utils.util import * +import pyaudio 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): + def __init__(self, toxes, path): + self._path = path + self._profile_path = path.replace('.json', '.tox') 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): + if os.path.isfile(path): + with open(path, 'rb') as fl: + data = fl.read() try: - with open(path, 'rb') as fl: - data = fl.read() - if self._toxes.is_data_encrypted(data): - data = self._toxes.pass_decrypt(data) + if toxes.is_data_encrypted(data): + data = toxes.pass_decrypt(data) info = json.loads(str(data, 'utf-8')) - LOG.debug('Parsed settings from: ' + str(path)) except Exception as ex: - 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) + info = Settings.get_default_settings() + log('Parsing settings error: ' + str(ex)) + super().__init__(info) + self._upgrade() 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) - + super().__init__(Settings.get_default_settings()) self.save() self.locked = False self.closing = False self.unlockScreen = False + p = pyaudio.PyAudio() + input_devices = output_devices = 0 + for i in range(p.get_device_count()): + device = p.get_device_info_by_index(i) + if device["maxInputChannels"]: + input_devices += 1 + if device["maxOutputChannels"]: + output_devices += 1 + self.audio = {'input': p.get_default_input_device_info()['index'] if input_devices else -1, + 'output': p.get_default_output_device_info()['index'] if output_devices else -1, + 'enabled': input_devices and output_devices} + self.video = {'device': -1, 'width': 640, 'height': 480, 'x': 0, 'y': 0} + # ----------------------------------------------------------------------------------------------------------------- # Properties + # ----------------------------------------------------------------------------------------------------------------- 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) @@ -210,43 +64,23 @@ class Settings(dict): 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) + with open(self._path, 'wb') as fl: + fl.write(text) + + self._settings_saved_event(text) def close(self): path = self._profile_path + '.lock' if os.path.isfile(path): os.remove(path) - def set_active_profile(self, profile_path): + def set_active_profile(self): """ 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')) + path = self._profile_path + '.lock' + with open(path, 'w') as fl: + fl.write('active') def export(self, path): text = json.dumps(self) @@ -258,45 +92,32 @@ class Settings(dict): 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') + def get_auto_profile(): + p = Settings.get_global_settings_path() if not os.path.isfile(p): return None + with open(p) as fl: + data = fl.read() try: - with open(p, 'rb') as fl: - data = fl.read() - if self._toxes.is_data_encrypted(data): - data = self._toxes.pass_decrypt(data) + auto = json.loads(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}") + log(str(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__))) + path = join_path(path, curr_directory(__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') + p = Settings.get_global_settings_path() if os.path.isfile(p): with open(p) as fl: data = fl.read() @@ -309,8 +130,7 @@ class Settings(dict): @staticmethod def reset_auto_profile(): - appdir = ts.get_user_config_path() - p = os.path.join(appdir, 'toxygen.json') + p = Settings.get_global_settings_path() if os.path.isfile(p): with open(p) as fl: data = fl.read() @@ -323,37 +143,28 @@ class Settings(dict): fl.write(json.dumps(data)) @staticmethod - def get_default_settings(args=None): + def is_active_profile(profile_path): + return os.path.isfile(profile_path + '.lock') + + @staticmethod + def get_default_settings(): """ Default profile settings """ - retval = { - # FixMe: match? /var/local/src/c-toxcore/toxcore/tox.h - 'ipv6_enabled': True, + return { + 'theme': 'dark', + 'ipv6_enabled': False, 'udp_enabled': True, - 'trace_enabled': False, - 'local_discovery_enabled': True, - 'dht_announcements_enabled': True, 'proxy_type': 0, - 'proxy_host': '', - 'proxy_port': 0, + 'proxy_host': '127.0.0.1', + 'proxy_port': 9050, '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, + 'notifications': True, 'sound_notifications': False, 'language': 'English', - 'calls_sound': False, # was True - - 'save_history': True, - 'save_unsent_only': False, + 'save_history': False, 'allow_inline': True, 'allow_auto_accept': True, 'auto_accept_path': None, @@ -364,6 +175,7 @@ class Settings(dict): 'friends_aliases': [], 'show_avatars': False, 'typing_notifications': False, + 'calls_sound': True, 'blocked': [], 'plugins': [], 'notes': {}, @@ -376,33 +188,53 @@ class Settings(dict): 'y': 400, 'message_font_size': 14, 'unread_color': 'red', + 'save_unsent_only': False, 'compact_mode': False, 'identicons': True, - 'show_welcome_screen': False, + 'show_welcome_screen': True, 'close_app': 0, 'font': 'Times New Roman', - 'update': 0, + 'update': 1, 'group_notifications': True, - 'download_nodes_list': False, # - 'download_nodes_url': 'https://nodes.tox.chat/json', + 'download_nodes_list': False, '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, + 'lan_discovery': True, + 'backup_directory': None } - return retval + @staticmethod + def supported_languages(): + return { + 'English': 'en_EN', + 'French': 'fr_FR', + 'Russian': 'ru_RU', + 'Ukrainian': 'uk_UA' + } + + @staticmethod + def built_in_themes(): + return { + 'dark': 'dark_style.qss', + 'default': 'style.qss' + } + + @staticmethod + def get_global_settings_path(): + return os.path.join(get_base_directory(), 'toxygen.json') + + @staticmethod + def get_default_path(): + system = get_platform() + if system == 'Windows': + return os.getenv('APPDATA') + '/Tox/' + elif system == 'Darwin': + return os.getenv('HOME') + '/Library/Application Support/Tox/' + else: + return os.getenv('HOME') + '/.config/tox/' + + # ----------------------------------------------------------------------------------------------------------------- # Private methods + # ----------------------------------------------------------------------------------------------------------------- def _upgrade(self): default = Settings.get_default_settings() @@ -410,19 +242,3 @@ class Settings(dict): if key not in self: print(key) self[key] = default[key] - - def _changed(self, aArgs, info): - aRet = dict() - default = Settings.get_default_settings() - for key in default: - if key in ['audio', 'video']: continue - if key not in aArgs.__dict__: continue - val = aArgs.__dict__[key] - if val in ['0.0.0.0']: continue - if key in aArgs.__dict__ and 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 index 84b8636..982f287 100644 --- a/toxygen/user_data/toxes.py +++ b/toxygen/user_data/toxes.py @@ -1,4 +1,3 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- class ToxES: diff --git a/toxygen/utils/ui.py b/toxygen/utils/ui.py index f7d20e8..d2d7122 100644 --- a/toxygen/utils/ui.py +++ b/toxygen/utils/ui.py @@ -1,9 +1,7 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -from qtpy import QtWidgets - +from PyQt5 import QtWidgets import utils.util as util + def tr(s): return QtWidgets.QApplication.translate('Toxygen', s) @@ -38,9 +36,9 @@ def file_dialog(caption, file_filter=None): options=QtWidgets.QFileDialog.DontUseNativeDialog) -def save_file_dialog(caption, file_filter=None): +def save_file_dialog(caption, filter=None): return QtWidgets.QFileDialog.getSaveFileName(None, caption, util.curr_directory(), - filter=file_filter, + filter=filter, options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) diff --git a/toxygen/utils/util.py b/toxygen/utils/util.py index 70cc7ff..5bd5c3a 100644 --- a/toxygen/utils/util.py +++ b/toxygen/utils/util.py @@ -1,11 +1,10 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import datetime import os -import platform -import re +import time import shutil import sys -import time +import re +import platform +import datetime def cached(func): @@ -20,26 +19,14 @@ def cached(func): 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 + +def log(data): try: - oFD.write(str(data) +'\n') + with open(join_path(curr_directory(), 'logs.log'), 'a') as fl: + fl.write(str(data) + '\n') except Exception as ex: - print(f"ERROR: writing to toxygen.log: {ex}") - return data + print(ex) + def curr_directory(current_file=None): return os.path.dirname(os.path.realpath(current_file or __file__)) @@ -157,6 +144,7 @@ def time_offset(): result = hours * 60 + minutes - h * 60 - m return result + def unix_time_to_long_str(unix_time): date_time = datetime.datetime.utcfromtimestamp(unix_time) @@ -180,11 +168,3 @@ def is_re_valid(regex): @cached def get_platform(): return platform.system() - -def get_user_config_path(): - if get_platform() == 'Windows': - return os.getenv('APPDATA') + '/Tox/' - elif get_platform() == 'Darwin': - return os.getenv('HOME') + '/Library/Application Support/Tox/' - else: - return os.getenv('HOME') + '/.config/tox/' diff --git a/toxygen/tests/__init__.py b/toxygen/wrapper/__init__.py similarity index 100% rename from toxygen/tests/__init__.py rename to toxygen/wrapper/__init__.py diff --git a/toxygen/wrapper/libtox.py b/toxygen/wrapper/libtox.py new file mode 100644 index 0000000..01d41f1 --- /dev/null +++ b/toxygen/wrapper/libtox.py @@ -0,0 +1,61 @@ +from ctypes import CDLL +import utils.util as util + + +class LibToxCore: + + def __init__(self): + platform = util.get_platform() + if platform == 'Windows': + self._libtoxcore = CDLL(util.join_path(util.get_libs_directory(), 'libtox.dll')) + elif platform == 'Darwin': + self._libtoxcore = CDLL('libtoxcore.dylib') + else: + # libtoxcore and libsodium must be installed in your os + try: + self._libtoxcore = CDLL('libtoxcore.so') + except: + self._libtoxcore = CDLL(util.join_path(util.get_libs_directory(), 'libtoxcore.so')) + + def __getattr__(self, item): + return self._libtoxcore.__getattr__(item) + + +class LibToxAV: + + def __init__(self): + platform = util.get_platform() + if platform == 'Windows': + # on Windows av api is in libtox.dll + self._libtoxav = CDLL(util.join_path(util.get_libs_directory(), 'libtox.dll')) + elif platform == 'Darwin': + self._libtoxav = CDLL('libtoxcore.dylib') + else: + # /usr/lib/libtoxcore.so must exists + try: + self._libtoxav = CDLL('libtoxcore.so') + except: + self._libtoxav = CDLL(util.join_path(util.get_libs_directory(), 'libtoxcore.so')) + + def __getattr__(self, item): + return self._libtoxav.__getattr__(item) + + +class LibToxEncryptSave: + + def __init__(self): + platform = util.get_platform() + if platform == 'Windows': + # on Windows profile encryption api is in libtox.dll + self._lib_tox_encrypt_save = CDLL(util.join_path(util.get_libs_directory(), 'libtox.dll')) + elif platform == 'Darwin': + self._lib_tox_encrypt_save = CDLL('libtoxcore.dylib') + else: + # /usr/lib/libtoxcore.so must exists + try: + self._lib_tox_encrypt_save = CDLL('libtoxcore.so') + except: + self._lib_tox_encrypt_save = CDLL(util.join_path(util.get_libs_directory(), 'libtoxcore.so')) + + def __getattr__(self, item): + return self._lib_tox_encrypt_save.__getattr__(item) diff --git a/toxygen/wrapper/tox.py b/toxygen/wrapper/tox.py new file mode 100644 index 0000000..21b0ebc --- /dev/null +++ b/toxygen/wrapper/tox.py @@ -0,0 +1,2532 @@ +# -*- coding: utf-8 -*- +from ctypes import * +from wrapper.toxcore_enums_and_consts import * +from wrapper.toxav import ToxAV +from wrapper.libtox import LibToxCore + + +class ToxOptions(Structure): + _fields_ = [ + ('ipv6_enabled', c_bool), + ('udp_enabled', c_bool), + ('local_discovery_enabled', c_bool), + ('proxy_type', c_int), + ('proxy_host', c_char_p), + ('proxy_port', c_uint16), + ('start_port', c_uint16), + ('end_port', c_uint16), + ('tcp_port', c_uint16), + ('hole_punching_enabled', c_bool), + ('savedata_type', c_int), + ('savedata_data', c_char_p), + ('savedata_length', c_size_t), + ('log_callback', c_void_p), + ('log_user_data', c_void_p) + ] + + +class GroupChatSelfPeerInfo(Structure): + _fields_ = [ + ('nick', c_char_p), + ('nick_length', c_uint8), + ('user_status', c_int) + ] + + +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() + f = Tox.libtoxcore.tox_new + f.restype = POINTER(c_void_p) + self._tox_pointer = f(tox_options, byref(tox_err_new)) + tox_err_new = tox_err_new.value + if tox_err_new == TOX_ERR_NEW['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + 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.group_moderation_cb = None + self.group_join_fail_cb = None + self.group_self_join_cb = None + self.group_invite_cb = None + self.group_custom_packet_cb = None + self.group_private_message_cb = None + self.group_message_cb = None + self.group_password_cb = None + self.group_peer_limit_cb = None + self.group_privacy_state_cb = None + self.group_topic_cb = None + self.group_peer_status_cb = None + self.group_peer_name_cb = None + self.group_peer_exit_cb = None + self.group_peer_join_cb = None + + self.AV = ToxAV(self._tox_pointer) + + def kill(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. + """ + address = bytes(address, 'utf-8') + tox_err_bootstrap = c_int() + result = Tox.libtoxcore.tox_bootstrap(self._tox_pointer, c_char_p(address), c_uint16(port), + string_to_bin(public_key), byref(tox_err_bootstrap)) + 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. + """ + address = bytes(address, 'utf-8') + tox_err_bootstrap = c_int() + result = Tox.libtoxcore.tox_add_tcp_relay(self._tox_pointer, c_char_p(address), c_uint16(port), + string_to_bin(public_key), byref(tox_err_bootstrap)) + 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): + """ + 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 + """ + 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) + + 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, user_data=None): + """ + The main loop that needs to be run in intervals of tox_iteration_interval() milliseconds. + """ + if user_data is not None: + user_data = c_char_p(user_data) + Tox.libtoxcore.tox_iterate(self._tox_pointer, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Internal client information (Tox address/id) + # ----------------------------------------------------------------------------------------------------------------- + + 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() + name = bytes(name, 'utf-8') + result = Tox.libtoxcore.tox_self_set_name(self._tox_pointer, c_char_p(name), + c_size_t(len(name)), byref(tox_err_set_info)) + tox_err_set_info = tox_err_set_info.value + 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() + status_message = bytes(status_message, 'utf-8') + result = Tox.libtoxcore.tox_self_set_status_message(self._tox_pointer, c_char_p(status_message), + c_size_t(len(status_message)), byref(tox_err_set_info)) + tox_err_set_info = tox_err_set_info.value + 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): + """ + 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 + """ + 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) + + 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): + """ + 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 + """ + 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) + + 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): + """ + 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) + + 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): + """ + 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 + """ + 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) + + 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): + """ + 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 + """ + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # 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): + """ + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # Receiving private messages and friend requests + # ----------------------------------------------------------------------------------------------------------------- + + def callback_friend_request(self, callback): + """ + 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) + + def callback_friend_message(self, callback): + """ + 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 + """ + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # 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): + """ + 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 + """ + 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) + + 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): + """ + 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 + """ + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # File transmission: receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_file_recv(self, callback): + """ + 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 + """ + 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) + + def callback_file_recv_chunk(self, callback): + """ + 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 + """ + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # 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): + """ + 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 + """ + 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) + + def callback_friend_lossless_packet(self, callback): + """ + 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 + """ + 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) + + # ----------------------------------------------------------------------------------------------------------------- + # 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.') + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat instance management + # ----------------------------------------------------------------------------------------------------------------- + + def group_new(self, privacy_state, group_name, nick, status): + """ + Creates a new group chat. + + This function creates a new group chat object and adds it to the chats array. + + The client should initiate its peer list with self info after calling this function, as + the peer_join callback will not be triggered. + + :param privacy_state: The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC, + the group will attempt to announce itself to the DHT and anyone with the Chat ID may join. + Otherwise a friend invite will be required to join the group. + :param group_name: The name of the group. The name must be non-NULL. + + :return group number on success, UINT32_MAX on failure. + """ + + error = c_int() + peer_info = self.group_self_peer_info_new() + nick = bytes(nick, 'utf-8') + group_name = group_name.encode('utf-8') + peer_info.contents.nick = c_char_p(nick) + peer_info.contents.nick_length = len(nick) + peer_info.contents.user_status = status + result = Tox.libtoxcore.tox_group_new(self._tox_pointer, privacy_state, group_name, + len(group_name), peer_info, byref(error)) + return result + + def group_join(self, chat_id, password, nick, status): + """ + Joins a group chat with specified Chat ID. + + This function creates a new group chat object, adds it to the chats array, and sends + a DHT announcement to find peers in the group associated with chat_id. Once a peer has been + found a join attempt will be initiated. + + :param chat_id: The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes. + :param password: The password required to join the group. Set to NULL if no password is required. + + :return group_number on success, UINT32_MAX on failure. + """ + + error = c_int() + peer_info = self.group_self_peer_info_new() + nick = bytes(nick, 'utf-8') + peer_info.contents.nick = c_char_p(nick) + peer_info.contents.nick_length = len(nick) + peer_info.contents.user_status = status + result = Tox.libtoxcore.tox_group_join(self._tox_pointer, string_to_bin(chat_id), + password, + len(password) if password is not None else 0, + peer_info, + byref(error)) + return result + + def group_reconnect(self, group_number): + """ + Reconnects to a group. + + This function disconnects from all peers in the group, then attempts to reconnect with the group. + The caller's state is not changed (i.e. name, status, role, chat public key etc.) + + :param group_number: The group number of the group we wish to reconnect to. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_reconnect(self._tox_pointer, group_number, byref(error)) + return result + + def group_is_connected(self, group_number): + error = c_int() + result = Tox.libtoxcore.tox_group_is_connected(self._tox_pointer, group_number, byref(error)) + return result + + def group_disconnect(self, group_number): + error = c_int() + result = Tox.libtoxcore.tox_group_disconnect(self._tox_pointer, group_number, byref(error)) + return result + + def group_leave(self, group_number, message=''): + """ + Leaves a group. + + This function sends a parting packet containing a custom (non-obligatory) message to all + peers in a group, and deletes the group from the chat array. All group state information is permanently + lost, including keys and role credentials. + + :param group_number: The group number of the group we wish to leave. + :param message: The parting message to be sent to all the peers. Set to NULL if we do not wish to + send a parting message. + + :return True if the group chat instance was successfully deleted. + """ + + error = c_int() + f = Tox.libtoxcore.tox_group_leave + f.restype = c_bool + result = f(self._tox_pointer, group_number, message, + len(message) if message is not None else 0, byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group user-visible client information (nickname/status/role/public key) + # ----------------------------------------------------------------------------------------------------------------- + + def group_self_set_name(self, group_number, name): + """ + Set the client's nickname for the group instance designated by the given group number. + + Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is equal to zero or name is a NULL + pointer, the function call will fail. + + :param name: A byte array containing the new nickname. + + :return True on success. + """ + + error = c_int() + name = bytes(name, 'utf-8') + result = Tox.libtoxcore.tox_group_self_set_name(self._tox_pointer, group_number, name, len(name), byref(error)) + return result + + def group_self_get_name_size(self, group_number): + """ + Return the length of the client's current nickname for the group instance designated + by group_number as passed to tox_group_self_set_name. + + If no nickname was set before calling this function, the name is empty, + and this function returns 0. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_name_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_name(self, group_number): + """ + Write the nickname set by tox_group_self_set_name to a byte array. + + If no nickname was set before calling this function, the name is empty, + and this function has no effect. + + Call tox_group_self_get_name_size to find out how much memory to allocate for the result. + :return nickname + """ + + error = c_int() + size = self.group_self_get_name_size(group_number) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_self_get_name(self._tox_pointer, group_number, name, byref(error)) + return str(name[:size], 'utf-8') + + def group_self_set_status(self, group_number, status): + + """ + Set the client's status for the group instance. Status must be a TOX_USER_STATUS. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_set_status(self._tox_pointer, group_number, status, byref(error)) + return result + + def group_self_get_status(self, group_number): + """ + returns the client's status for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_status(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_role(self, group_number): + """ + returns the client's role for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_role(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_peer_id(self, group_number): + """ + returns the client's peer id for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_peer_id(self._tox_pointer, group_number, byref(error)) + return result + + def group_self_get_public_key(self, group_number): + """ + Write the client's group public key designated by the given group number to a byte array. + + This key will be permanently tied to the client's identity for this particular group until + the client explicitly leaves the group or gets kicked/banned. This key is the only way for + other peers to reliably identify the client across client restarts. + + `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + + :return public key + """ + + error = c_int() + key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + result = Tox.libtoxcore.tox_group_self_get_public_key(self._tox_pointer, group_number, + key, byref(error)) + return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + + # ----------------------------------------------------------------------------------------------------------------- + # Peer-specific group state queries. + # ----------------------------------------------------------------------------------------------------------------- + + def group_peer_get_name_size(self, group_number, peer_id): + """ + Return the length of the peer's name. If the group number or ID is invalid, the + return value is unspecified. + + The return value is equal to the `length` argument received by the last + `group_peer_name` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_name_size(self._tox_pointer, group_number, peer_id, byref(error)) + return result + + def group_peer_get_name(self, group_number, peer_id): + """ + Write the name of the peer designated by the given ID to a byte + array. + + Call tox_group_peer_get_name_size to determine the allocation size for the `name` parameter. + + The data written to `name` is equal to the data received by the last + `group_peer_name` callback. + + :param group_number: The group number of the group we wish to query. + :param peer_id: The ID of the peer whose name we want to retrieve. + + :return name. + """ + error = c_int() + size = self.group_peer_get_name_size(group_number, peer_id) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_peer_get_name(self._tox_pointer, group_number, peer_id, name, byref(error)) + return str(name[:], 'utf-8') + + def group_peer_get_status(self, group_number, peer_id): + """ + Return the peer's user status (away/busy/...). If the ID or group number is + invalid, the return value is unspecified. + + The status returned is equal to the last status received through the + `group_peer_status` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_status(self._tox_pointer, group_number, peer_id, byref(error)) + return result + + def group_peer_get_role(self, group_number, peer_id): + """ + Return the peer's role (user/moderator/founder...). If the ID or group number is + invalid, the return value is unspecified. + + The role returned is equal to the last role received through the + `group_moderation` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_role(self._tox_pointer, group_number, peer_id, byref(error)) + return result + + def group_peer_get_public_key(self, group_number, peer_id): + """ + Write the group public key with the designated peer_id for the designated group number to public_key. + + This key will be permanently tied to a particular peer until they explicitly leave the group or + get kicked/banned, and is the only way to reliably identify the same peer across client restarts. + + `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + + :return public key + """ + + error = c_int() + key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + result = Tox.libtoxcore.tox_group_peer_get_public_key(self._tox_pointer, group_number, peer_id, + key, byref(error)) + return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + + def callback_group_peer_name(self, callback, user_data): + """ + Set the callback for the `group_peer_name` event. Pass NULL to unset. + This event is triggered when a peer changes their nickname. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_peer_name_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_name(self._tox_pointer, self.group_peer_name_cb, user_data) + + def callback_group_peer_status(self, callback, user_data): + """ + Set the callback for the `group_peer_status` event. Pass NULL to unset. + This event is triggered when a peer changes their status. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p) + self.group_peer_status_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_status(self._tox_pointer, self.group_peer_status_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat state queries and events. + # ----------------------------------------------------------------------------------------------------------------- + + def group_set_topic(self, group_number, topic): + """ + Set the group topic and broadcast it to the rest of the group. + + topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH. If length is equal to zero or + topic is set to NULL, the topic will be unset. + + :return True on success. + """ + + error = c_int() + topic = bytes(topic, 'utf-8') + result = Tox.libtoxcore.tox_group_set_topic(self._tox_pointer, group_number, topic, len(topic), byref(error)) + return result + + def group_get_topic_size(self, group_number): + """ + Return the length of the group topic. If the group number is invalid, the + return value is unspecified. + + The return value is equal to the `length` argument received by the last + `group_topic` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_topic(self, group_number): + """ + Write the topic designated by the given group number to a byte array. + Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter. + The data written to `topic` is equal to the data received by the last + `group_topic` callback. + + :return topic + """ + + error = c_int() + size = self.group_get_topic_size(group_number) + topic = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_topic(self._tox_pointer, group_number, topic, byref(error)) + return str(topic[:size], 'utf-8') + + def group_get_name_size(self, group_number): + """ + Return the length of the group name. If the group number is invalid, the + return value is unspecified. + """ + error = c_int() + result = Tox.libtoxcore.tox_group_get_name_size(self._tox_pointer, group_number, byref(error)) + return int(result) + + def group_get_name(self, group_number): + """ + Write the name of the group designated by the given group number to a byte array. + Call tox_group_get_name_size to determine the allocation size for the `name` parameter. + :return true on success. + """ + + error = c_int() + size = self.group_get_name_size(group_number) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_name(self._tox_pointer, group_number, + name, byref(error)) + return str(name[:size], 'utf-8') + + def group_get_chat_id(self, group_number): + """ + Write the Chat ID designated by the given group number to a byte array. + `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes. + :return chat id. + """ + + error = c_int() + buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE) + result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer, group_number, + buff, byref(error)) + return bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE) + + def group_get_number_groups(self): + """ + Return the number of groups in the Tox chats array. + """ + + result = Tox.libtoxcore.tox_group_get_number_groups(self._tox_pointer) + return result + + def groups_get_list(self): + groups_list_size = self.group_get_number_groups() + groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size) + groups_list = POINTER(c_uint32)(groups_list) + Tox.libtoxcore.tox_groups_get_list(self._tox_pointer, groups_list) + return groups_list[0:groups_list_size] + + def group_get_privacy_state(self, group_number): + """ + Return the privacy state of the group designated by the given group number. If group number + is invalid, the return value is unspecified. + + The value returned is equal to the data received by the last + `group_privacy_state` callback. + + see the `Group chat founder controls` section for the respective set function. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_privacy_state(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_peer_limit(self, group_number): + """ + Return the maximum number of peers allowed for the group designated by the given group number. + If the group number is invalid, the return value is unspecified. + + The value returned is equal to the data received by the last + `group_peer_limit` callback. + + see the `Group chat founder controls` section for the respective set function. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_peer_limit(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_password_size(self, group_number): + """ + Return the length of the group password. If the group number is invalid, the + return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_password_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_get_password(self, group_number): + """ + Write the password for the group designated by the given group number to a byte array. + + Call tox_group_get_password_size to determine the allocation size for the `password` parameter. + + The data received is equal to the data received by the last + `group_password` callback. + + see the `Group chat founder controls` section for the respective set function. + + :return password + """ + + error = c_int() + size = self.group_get_password_size(group_number) + password = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_password(self._tox_pointer, group_number, + password, byref(error)) + return str(password[:size], 'utf-8') + + def callback_group_topic(self, callback, user_data): + """ + Set the callback for the `group_topic` event. Pass NULL to unset. + This event is triggered when a peer changes the group topic. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_topic_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_topic(self._tox_pointer, self.group_topic_cb, user_data) + + def callback_group_privacy_state(self, callback, user_data): + """ + Set the callback for the `group_privacy_state` event. Pass NULL to unset. + This event is triggered when the group founder changes the privacy state. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) + self.group_privacy_state_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_privacy_state(self._tox_pointer, self.group_privacy_state_cb, user_data) + + def callback_group_peer_limit(self, callback, user_data): + """ + Set the callback for the `group_peer_limit` event. Pass NULL to unset. + This event is triggered when the group founder changes the maximum peer limit. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.group_peer_limit_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_limit(self._tox_pointer, self.group_peer_limit_cb, user_data) + + def callback_group_password(self, callback, user_data): + """ + Set the callback for the `group_password` event. Pass NULL to unset. + This event is triggered when the group founder changes the group password. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_password_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_password(self._tox_pointer, self.group_password_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group message sending + # ----------------------------------------------------------------------------------------------------------------- + + def group_send_custom_packet(self, group_number, lossless, data): + """ + Send a custom packet to the group. + + If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + to TCP (reliability, arrive in order) but with packets instead of a stream. + + If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + meaning they might never reach the other side or might arrive more than once (if someone + is messing with the connection) or might arrive in the wrong order. + + Unless latency is an issue or message reliability is not important, it is recommended that you use + lossless custom packets. + + :param group_number: The group number of the group the message is intended for. + :param lossless: True if the packet should be lossless. + :param data A byte array containing the packet data. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_custom_packet(self._tox_pointer, group_number, lossless, data, + len(data), byref(error)) + return result + + def group_send_private_message(self, group_number, peer_id, message_type, message): + """ + Send a text chat message to the specified peer in the specified group. + + This function creates a group private message packet and pushes it into the send + queue. + + The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages + must be split by the client and sent as separate messages. Other clients can + then reassemble the fragments. Messages may not be empty. + + :param group_number: The group number of the group the message is intended for. + :param peer_id: The ID of the peer the message is intended for. + :param message: A non-NULL pointer to the first element of a byte array containing the message text. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_private_message(self._tox_pointer, group_number, peer_id, + message_type, message, + len(message), byref(error)) + return result + + def group_send_message(self, group_number, type, message): + """ + Send a text chat message to the group. + + This function creates a group message packet and pushes it into the send + queue. + + The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages + must be split by the client and sent as separate messages. Other clients can + then reassemble the fragments. Messages may not be empty. + + :param group_number: The group number of the group the message is intended for. + :param type: Message type (normal, action, ...). + :param message: A non-NULL pointer to the first element of a byte array containing the message text. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_message(self._tox_pointer, group_number, type, message, len(message), + byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group message receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_group_message(self, callback, user_data): + """ + Set the callback for the `group_message` event. Pass NULL to unset. + This event is triggered when the client receives a group message. + + Callback: python function with params: + tox Tox* instance + group_number The group number of the group the message is intended for. + peer_id The ID of the peer who sent the message. + type The type of message (normal, action, ...). + message The message data. + length The length of the message. + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_char_p, c_size_t, c_void_p) + self.group_message_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_message(self._tox_pointer, self.group_message_cb, user_data) + + def callback_group_private_message(self, callback, user_data): + """ + Set the callback for the `group_private_message` event. Pass NULL to unset. + This event is triggered when the client receives a private message. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint8, c_char_p, c_size_t, c_void_p) + self.group_private_message_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_private_message(self._tox_pointer, self.group_private_message_cb, user_data) + + def callback_group_custom_packet(self, callback, user_data): + """ + Set the callback for the `group_custom_packet` event. Pass NULL to unset. + + This event is triggered when the client receives a custom packet. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, POINTER(c_uint8), c_void_p) + self.group_custom_packet_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_custom_packet(self._tox_pointer, self.group_custom_packet_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat inviting and join/part events + # ----------------------------------------------------------------------------------------------------------------- + + def group_invite_friend(self, group_number, friend_number): + """ + Invite a friend to a group. + + This function creates an invite request packet and pushes it to the send queue. + + :param group_number: The group number of the group the message is intended for. + :param friend_number: The friend number of the friend the invite is intended for. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_invite_friend(self._tox_pointer, group_number, friend_number, byref(error)) + return result + + @staticmethod + def group_self_peer_info_new(): + error = c_int() + f = Tox.libtoxcore.tox_group_self_peer_info_new + f.restype = POINTER(GroupChatSelfPeerInfo) + result = f(byref(error)) + + return result + + def group_invite_accept(self, invite_data, friend_number, nick, status, password=None): + """ + Accept an invite to a group chat that the client previously received from a friend. The invite + is only valid while the inviter is present in the group. + + :param invite_data: The invite data received from the `group_invite` event. + :param password: The password required to join the group. Set to NULL if no password is required. + :return the group_number on success, UINT32_MAX on failure. + """ + + error = c_int() + f = Tox.libtoxcore.tox_group_invite_accept + f.restype = c_uint32 + peer_info = self.group_self_peer_info_new() + nick = bytes(nick, 'utf-8') + peer_info.contents.nick = c_char_p(nick) + peer_info.contents.nick_length = len(nick) + peer_info.contents.user_status = status + result = f(self._tox_pointer, friend_number, invite_data, len(invite_data), password, + len(password) if password is not None else 0, peer_info, byref(error)) + print('Invite accept. Result:', result, 'Error:', error.value) + return result + + def callback_group_invite(self, callback, user_data): + """ + Set the callback for the `group_invite` event. Pass NULL to unset. + + This event is triggered when the client receives a group invite from a friend. The client must store + invite_data which is used to join the group via tox_group_invite_accept. + + Callback: python function with params: + tox - Tox* + friend_number The friend number of the contact who sent the invite. + invite_data The invite data. + length The length of invite_data. + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, + POINTER(c_uint8), c_size_t, c_void_p) + self.group_invite_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, self.group_invite_cb, user_data) + + def callback_group_peer_join(self, callback, user_data): + """ + Set the callback for the `group_peer_join` event. Pass NULL to unset. + + This event is triggered when a peer other than self joins the group. + Callback: python function with params: + tox - Tox* + group_number - group number + peer_id - peer id + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.group_peer_join_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_join(self._tox_pointer, self.group_peer_join_cb, user_data) + + def callback_group_peer_exit(self, callback, user_data): + """ + Set the callback for the `group_peer_exit` event. Pass NULL to unset. + + This event is triggered when a peer other than self exits the group. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_peer_exit_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_exit(self._tox_pointer, self.group_peer_exit_cb, user_data) + + def callback_group_self_join(self, callback, user_data): + """ + Set the callback for the `group_self_join` event. Pass NULL to unset. + + This event is triggered when the client has successfully joined a group. Use this to initialize + any group information the client may need. + Callback: python fucntion with params: + tox - *Tox + group_number - group number + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_void_p) + self.group_self_join_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_self_join(self._tox_pointer, self.group_self_join_cb, user_data) + + def callback_group_join_fail(self, callback, user_data): + """ + Set the callback for the `group_join_fail` event. Pass NULL to unset. + + This event is triggered when the client fails to join a group. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) + self.group_join_fail_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_join_fail(self._tox_pointer, self.group_join_fail_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat founder controls (these only work for the group founder) + # ----------------------------------------------------------------------------------------------------------------- + + def group_founder_set_password(self, group_number, password): + """ + Set or unset the group password. + + This function sets the groups password, creates a new group shared state including the change, + and distributes it to the rest of the group. + + :param group_number: The group number of the group for which we wish to set the password. + :param password: The password we want to set. Set password to NULL to unset the password. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_password(self._tox_pointer, group_number, password, + len(password), byref(error)) + return result + + def group_founder_set_privacy_state(self, group_number, privacy_state): + """ + Set the group privacy state. + + This function sets the group's privacy state, creates a new group shared state + including the change, and distributes it to the rest of the group. + + If an attempt is made to set the privacy state to the same state that the group is already + in, the function call will be successful and no action will be taken. + + :param group_number: The group number of the group for which we wish to change the privacy state. + :param privacy_state: The privacy state we wish to set the group to. + + :return true on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_privacy_state(self._tox_pointer, group_number, privacy_state, + byref(error)) + return result + + def group_founder_set_peer_limit(self, group_number, max_peers): + """ + Set the group peer limit. + + This function sets a limit for the number of peers who may be in the group, creates a new + group shared state including the change, and distributes it to the rest of the group. + + :param group_number: The group number of the group for which we wish to set the peer limit. + :param max_peers: The maximum number of peers to allow in the group. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_peer_limit(self._tox_pointer, group_number, + max_peers, byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat moderation + # ----------------------------------------------------------------------------------------------------------------- + + def group_toggle_ignore(self, group_number, peer_id, ignore): + """ + Ignore or unignore a peer. + + :param group_number: The group number of the group the in which you wish to ignore a peer. + :param peer_id: The ID of the peer who shall be ignored or unignored. + :param ignore: True to ignore the peer, false to unignore the peer. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_toggle_ignore(self._tox_pointer, group_number, peer_id, ignore, byref(error)) + return result + + def group_mod_set_role(self, group_number, peer_id, role): + """ + Set a peer's role. + + This function will first remove the peer's previous role and then assign them a new role. + It will also send a packet to the rest of the group, requesting that they perform + the role reassignment. Note: peers cannot be set to the founder role. + + :param group_number: The group number of the group the in which you wish set the peer's role. + :param peer_id: The ID of the peer whose role you wish to set. + :param role: The role you wish to set the peer to. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_set_role(self._tox_pointer, group_number, peer_id, role, byref(error)) + return result + + def group_mod_remove_peer(self, group_number, peer_id): + """ + Kick/ban a peer. + + This function will remove a peer from the caller's peer list and optionally add their IP address + to the ban list. It will also send a packet to all group members requesting them + to do the same. + + :param group_number: The group number of the group the ban is intended for. + :param peer_id: The ID of the peer who will be kicked and/or added to the ban list. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_remove_peer(self._tox_pointer, group_number, peer_id, + byref(error)) + return result + + def group_mod_ban_peer(self, group_number, peer_id, ban_type): + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_ban_peer(self._tox_pointer, group_number, peer_id, + ban_type, byref(error)) + return result + + def group_mod_remove_ban(self, group_number, ban_id): + """ + Removes a ban. + + This function removes a ban entry from the ban list, and sends a packet to the rest of + the group requesting that they do the same. + + :param group_number: The group number of the group in which the ban is to be removed. + :param ban_id: The ID of the ban entry that shall be removed. + + :return True on success + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_remove_ban(self._tox_pointer, group_number, ban_id, byref(error)) + return result + + def callback_group_moderation(self, callback, user_data): + """ + Set the callback for the `group_moderation` event. Pass NULL to unset. + + This event is triggered when a moderator or founder executes a moderation event. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_int, c_void_p) + self.group_moderation_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_moderation(self._tox_pointer, self.group_moderation_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat ban list queries + # ----------------------------------------------------------------------------------------------------------------- + + def group_ban_get_list_size(self, group_number): + """ + Return the number of entries in the ban list for the group designated by + the given group number. If the group number is invalid, the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_list_size(self._tox_pointer, group_number, byref(error)) + return result + + def group_ban_get_list(self, group_number): + """ + Copy a list of valid ban list ID's into an array. + + Call tox_group_ban_get_list_size to determine the number of elements to allocate. + return true on success. + """ + + error = c_int() + bans_list_size = self.group_ban_get_list_size(group_number) + bans_list = create_string_buffer(sizeof(c_uint32) * bans_list_size) + bans_list = POINTER(c_uint32)(bans_list) + result = Tox.libtoxcore.tox_group_ban_get_list(self._tox_pointer, group_number, bans_list, byref(error)) + return bans_list[:bans_list_size] + + def group_ban_get_type(self, group_number, ban_id): + """ + Return the type for the ban list entry designated by ban_id, in the + group designated by the given group number. If either group_number or ban_id is invalid, + the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_type(self._tox_pointer, group_number, ban_id, byref(error)) + return result + + def group_ban_get_target_size(self, group_number, ban_id): + """ + Return the length of the name for the ban list entry designated by ban_id, in the + group designated by the given group number. If either group_number or ban_id is invalid, + the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_target_size(self._tox_pointer, group_number, ban_id, byref(error)) + return result + + def group_ban_get_target(self, group_number, ban_id): + """ + Write the name of the ban entry designated by ban_id in the group designated by the + given group number to a byte array. + + Call tox_group_ban_get_name_size to find out how much memory to allocate for the result. + + :return name + """ + + error = c_int() + size = self.group_ban_get_target_size(group_number, ban_id) + target = create_string_buffer(size) + target_type = self.group_ban_get_type(group_number, ban_id) + + result = Tox.libtoxcore.tox_group_ban_get_target(self._tox_pointer, group_number, ban_id, + target, byref(error)) + if target_type == TOX_GROUP_BAN_TYPE['PUBLIC_KEY']: + return bin_to_string(target, size) + return str(target[:size], 'utf-8') + + def group_ban_get_time_set(self, group_number, ban_id): + """ + Return a time stamp indicating the time the ban was set, for the ban list entry + designated by ban_id, in the group designated by the given group number. + If either group_number or ban_id is invalid, the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_time_set(self._tox_pointer, group_number, ban_id, byref(error)) + return result diff --git a/toxygen/wrapper/toxav.py b/toxygen/wrapper/toxav.py new file mode 100644 index 0000000..98e1c73 --- /dev/null +++ b/toxygen/wrapper/toxav.py @@ -0,0 +1,363 @@ +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 wrapper.libtox import LibToxAV +from wrapper.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() + f = self.libtoxav.toxav_new + f.restype = POINTER(c_void_p) + self._toxav_pointer = f(tox_pointer, byref(toxav_err_new)) + toxav_err_new = toxav_err_new.value + if toxav_err_new == TOXAV_ERR_NEW['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + 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 kill(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/wrapper/toxav_enums.py b/toxygen/wrapper/toxav_enums.py new file mode 100644 index 0000000..3f3977a --- /dev/null +++ b/toxygen/wrapper/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/wrapper/toxcore_enums_and_consts.py b/toxygen/wrapper/toxcore_enums_and_consts.py new file mode 100644 index 0000000..b34e272 --- /dev/null +++ b/toxygen/wrapper/toxcore_enums_and_consts.py @@ -0,0 +1,954 @@ +TOX_USER_STATUS = { + 'NONE': 0, + 'AWAY': 1, + 'BUSY': 2, +} + +TOX_MESSAGE_TYPE = { + 'NORMAL': 0, + 'ACTION': 1, +} + +TOX_PROXY_TYPE = { + 'NONE': 0, + 'HTTP': 1, + 'SOCKS5': 2, +} + +TOX_SAVEDATA_TYPE = { + 'NONE': 0, + 'TOX_SAVE': 1, + 'SECRET_KEY': 2, +} + +TOX_ERR_OPTIONS_NEW = { + 'OK': 0, + 'MALLOC': 1, +} + +TOX_ERR_NEW = { + 'OK': 0, + 'NULL': 1, + 'MALLOC': 2, + 'PORT_ALLOC': 3, + 'PROXY_BAD_TYPE': 4, + 'PROXY_BAD_HOST': 5, + 'PROXY_BAD_PORT': 6, + 'PROXY_NOT_FOUND': 7, + 'LOAD_ENCRYPTED': 8, + 'LOAD_BAD_FORMAT': 9, +} + +TOX_ERR_BOOTSTRAP = { + 'OK': 0, + 'NULL': 1, + 'BAD_HOST': 2, + 'BAD_PORT': 3, +} + +TOX_CONNECTION = { + 'NONE': 0, + 'TCP': 1, + 'UDP': 2, +} + +TOX_ERR_SET_INFO = { + 'OK': 0, + 'NULL': 1, + 'TOO_LONG': 2, +} + +TOX_ERR_FRIEND_ADD = { + 'OK': 0, + 'NULL': 1, + 'TOO_LONG': 2, + 'NO_MESSAGE': 3, + 'OWN_KEY': 4, + 'ALREADY_SENT': 5, + 'BAD_CHECKSUM': 6, + 'SET_NEW_NOSPAM': 7, + 'MALLOC': 8, +} + +TOX_ERR_FRIEND_DELETE = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_BY_PUBLIC_KEY = { + 'OK': 0, + 'NULL': 1, + 'NOT_FOUND': 2, +} + +TOX_ERR_FRIEND_GET_PUBLIC_KEY = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_GET_LAST_ONLINE = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_QUERY = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, +} + +TOX_ERR_SET_TYPING = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, +} + +TOX_ERR_FRIEND_SEND_MESSAGE = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'SENDQ': 4, + 'TOO_LONG': 5, + 'EMPTY': 6, +} + +TOX_FILE_KIND = { + 'DATA': 0, + 'AVATAR': 1, +} + +TOX_FILE_CONTROL = { + 'RESUME': 0, + 'PAUSE': 1, + 'CANCEL': 2, +} + +TOX_ERR_FILE_CONTROL = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, + 'FRIEND_NOT_CONNECTED': 2, + 'NOT_FOUND': 3, + 'NOT_PAUSED': 4, + 'DENIED': 5, + 'ALREADY_PAUSED': 6, + 'SENDQ': 7, +} + +TOX_ERR_FILE_SEEK = { + 'OK': 0, + 'FRIEND_NOT_FOUND': 1, + 'FRIEND_NOT_CONNECTED': 2, + 'NOT_FOUND': 3, + 'DENIED': 4, + 'INVALID_POSITION': 5, + 'SENDQ': 6, +} + +TOX_ERR_FILE_GET = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'NOT_FOUND': 3, +} + +TOX_ERR_FILE_SEND = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'NAME_TOO_LONG': 4, + 'TOO_MANY': 5, +} + +TOX_ERR_FILE_SEND_CHUNK = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'NOT_FOUND': 4, + 'NOT_TRANSFERRING': 5, + 'INVALID_LENGTH': 6, + 'SENDQ': 7, + 'WRONG_POSITION': 8, +} + +TOX_ERR_FRIEND_CUSTOM_PACKET = { + 'OK': 0, + 'NULL': 1, + 'FRIEND_NOT_FOUND': 2, + 'FRIEND_NOT_CONNECTED': 3, + 'INVALID': 4, + 'EMPTY': 5, + 'TOO_LONG': 6, + 'SENDQ': 7, +} + +TOX_ERR_GET_PORT = { + 'OK': 0, + 'NOT_BOUND': 1, +} + +TOX_GROUP_PRIVACY_STATE = { + + # + # The group is considered to be public. Anyone may join the group using the Chat ID. + # + # If the group is in this state, even if the Chat ID is never explicitly shared + # with someone outside of the group, information including the Chat ID, IP addresses, + # and peer ID's (but not Tox ID's) is visible to anyone with access to a node + # storing a DHT entry for the given group. + # + 'PUBLIC': 0, + + # + # The group is considered to be private. The only way to join the group is by having + # someone in your contact list send you an invite. + # + # If the group is in this state, no group information (mentioned above) is present in the DHT; + # the DHT is not used for any purpose at all. If a public group is set to private, + # all DHT information related to the group will expire shortly. + # + 'PRIVATE': 1 +} + +TOX_GROUP_ROLE = { + + # + # May kick and ban all other peers as well as set their role to anything (except founder). + # Founders may also set the group password, toggle the privacy state, and set the peer limit. + # + 'FOUNDER': 0, + + # + # May kick, ban and set the user and observer roles for peers below this role. + # May also set the group topic. + # + 'MODERATOR': 1, + + # + # May communicate with other peers normally. + # + 'USER': 2, + + # + # May observe the group and ignore peers; may not communicate with other peers or with the group. + # + 'OBSERVER': 3 +} + +TOX_ERR_GROUP_NEW = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_NEW_OK': 0, + + # + # The group name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH. + # + 'TOX_ERR_GROUP_NEW_TOO_LONG': 1, + + # + # group_name is NULL or length is zero. + # + 'TOX_ERR_GROUP_NEW_EMPTY': 2, + + # + # TOX_GROUP_PRIVACY_STATE is an invalid type. + # + 'TOX_ERR_GROUP_NEW_PRIVACY': 3, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_NEW_INIT': 4, + + # + # The group state failed to initialize. This usually indicates that something went wrong + # related to cryptographic signing. + # + 'TOX_ERR_GROUP_NEW_STATE': 5, + + # + # The group failed to announce to the DHT. This indicates a network related error. + # + 'TOX_ERR_GROUP_NEW_ANNOUNCE': 6, +} + +TOX_ERR_GROUP_JOIN = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_JOIN_OK': 0, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_JOIN_INIT': 1, + + # + # The chat_id pointer is set to NULL or a group with chat_id already exists. This usually + # happens if the client attempts to create multiple sessions for the same group. + # + 'TOX_ERR_GROUP_JOIN_BAD_CHAT_ID': 2, + + # + # Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_JOIN_TOO_LONG': 3, +} + +TOX_ERR_GROUP_RECONNECT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_RECONNECT_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND': 1, +} + +TOX_ERR_GROUP_LEAVE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_LEAVE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_GROUP_MAX_PART_LENGTH. + # + 'TOX_ERR_GROUP_LEAVE_TOO_LONG': 2, + + # + # The parting packet failed to send. + # + 'TOX_ERR_GROUP_LEAVE_FAIL_SEND': 3, + + # + # The group chat instance failed to be deleted. This may occur due to memory related errors. + # + 'TOX_ERR_GROUP_LEAVE_DELETE_FAIL': 4, +} + +TOX_ERR_GROUP_SELF_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND': 1, +} + + +TOX_ERR_GROUP_SELF_NAME_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND': 1, + + # + # Name length exceeded 'TOX_MAX_NAME_LENGTH. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG': 2, + + # + # The length given to the set function is zero or name is a NULL pointer. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_INVALID': 3, + + # + # The name is already taken by another peer in the group. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_TAKEN': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_SELF_STATUS_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND': 1, + + # + # An invalid type was passed to the set function. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_INVALID': 2, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND': 3 +} + +TOX_ERR_GROUP_PEER_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_PEER_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND': 2 +} + +TOX_ERR_GROUP_STATE_QUERIES = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_STATE_QUERIES_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND': 1 +} + + +TOX_ERR_GROUP_TOPIC_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_TOPIC_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND': 1, + + # + # Topic length exceeded 'TOX_GROUP_MAX_TOPIC_LENGTH. + # + 'TOX_ERR_GROUP_TOPIC_SET_TOO_LONG': 2, + + # + # The caller does not have the required permissions to set the topic. + # + 'TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS': 3, + + # + # The packet could not be created. This error is usually related to cryptographic signing. + # + 'TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_SEND_MESSAGE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG': 2, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_EMPTY': 3, + + # + # The message type is invalid. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE': 4, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS': 5, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND': 6 +} + +TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND': 2, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG': 3, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY': 4, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS': 5, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND': 6 +} + +TOX_ERR_GROUP_SEND_CUSTOM_PACKET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG': 2, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY': 3, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS': 4 +} + +TOX_ERR_GROUP_INVITE_FRIEND = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND': 1, + + # + # The friend number passed did not designate a valid friend. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND': 2, + + # + # Creation of the invite packet failed. This indicates a network related error. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL': 3, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_INVITE_ACCEPT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_OK': 0, + + # + # The invite data is not in the expected format. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE': 1, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED': 2, + + # + # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG': 3 +} + +TOX_GROUP_JOIN_FAIL = { + + # + # You are using the same nickname as someone who is already in the group. + # + 'TOX_GROUP_JOIN_FAIL_NAME_TAKEN': 0, + + # + # The group peer limit has been reached. + # + 'TOX_GROUP_JOIN_FAIL_PEER_LIMIT': 1, + + # + # You have supplied an invalid password. + # + 'TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD': 2, + + # + # The join attempt failed due to an unspecified error. This often occurs when the group is + # not found in the DHT. + # + 'TOX_GROUP_JOIN_FAIL_UNKNOWN': 3 +} + +TOX_ERR_GROUP_FOUNDER_SET_PASSWORD = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions to set the password. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS': 2, + + # + # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND': 1, + + # + # 'TOX_GROUP_PRIVACY_STATE is an invalid type. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_INVALID': 2, + + # + # The caller does not have the required permissions to set the privacy state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS': 3, + + # + # The privacy state could not be set. This may occur due to an error related to + # cryptographic signing of the new shared state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions to set the peer limit. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS': 2, + + # + # The peer limit could not be set. This may occur due to an error related to + # cryptographic signing of the new shared state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_TOGGLE_IGNORE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_PEER_NOT_FOUND': 2 +} + +TOX_ERR_GROUP_MOD_SET_ROLE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. Note: you cannot set your own role. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND': 2, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS': 3, + + # + # The role assignment is invalid. This will occur if you try to set a peer's role to + # the role they already have. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT': 4, + + # + # The role was not successfully set. This may occur if something goes wrong with role setting': , + # or if the packet fails to send. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION': 5 +} + +TOX_ERR_GROUP_MOD_REMOVE_PEER = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PEER_NOT_FOUND': 2, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PERMISSIONS': 3, + + # + # The peer could not be removed from the group. + # + # If a ban was set': , this error indicates that the ban entry could not be created. + # This is usually due to the peer's IP address already occurring in the ban list. It may also + # be due to the entry containing invalid peer information': , or a failure to cryptographically + # authenticate the entry. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_ACTION': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_MOD_REMOVE_BAN = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_PERMISSIONS': 2, + + # + # The ban entry could not be removed. This may occur if ban_id does not designate + # a valid ban entry. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_ACTION': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_SEND': 4 +} + +TOX_GROUP_MOD_EVENT = { + + # + # A peer has been kicked from the group. + # + 'KICK': 0, + + # + # A peer has been banned from the group. + # + 'BAN': 1, + + # + # A peer as been given the observer role. + # + 'OBSERVER': 2, + + # + # A peer has been given the user role. + # + 'USER': 3, + + # + # A peer has been given the moderator role. + # + 'MODERATOR': 4, +} + +TOX_ERR_GROUP_BAN_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_BAN_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_BAN_QUERY_GROUP_NOT_FOUND': 1, + + # + # The ban_id does not designate a valid ban list entry. + # + 'TOX_ERR_GROUP_BAN_QUERY_BAD_ID': 2, +} + + +TOX_GROUP_BAN_TYPE = { + + 'IP_PORT': 0, + + 'PUBLIC_KEY': 1, + + 'NICK': 2 +} + +TOX_PUBLIC_KEY_SIZE = 32 + +TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6 + +TOX_MAX_FRIEND_REQUEST_LENGTH = 1016 + +TOX_MAX_MESSAGE_LENGTH = 1372 + +TOX_GROUP_MAX_TOPIC_LENGTH = 512 + +TOX_GROUP_MAX_PART_LENGTH = 128 + +TOX_GROUP_MAX_GROUP_NAME_LENGTH = 48 + +TOX_GROUP_MAX_PASSWORD_SIZE = 32 + +TOX_GROUP_CHAT_ID_SIZE = 32 + +TOX_GROUP_PEER_PUBLIC_KEY_SIZE = 32 + +TOX_MAX_NAME_LENGTH = 128 + +TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 + +TOX_SECRET_KEY_SIZE = 32 + +TOX_FILE_ID_LENGTH = 32 + +TOX_HASH_LENGTH = 32 + +TOX_MAX_CUSTOM_PACKET_SIZE = 1373 diff --git a/toxygen/wrapper/toxencryptsave.py b/toxygen/wrapper/toxencryptsave.py new file mode 100644 index 0000000..31de085 --- /dev/null +++ b/toxygen/wrapper/toxencryptsave.py @@ -0,0 +1,74 @@ +from wrapper import libtox +from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool +from wrapper.toxencryptsave_enums_and_consts import * + + +class ToxEncryptSave: + + def __init__(self): + self.libtoxencryptsave = libtox.LibToxEncryptSave() + + def is_data_encrypted(self, data): + """ + Checks if given data is encrypted + """ + 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, password): + """ + Encrypts the given data with the given password. + + :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(password, 'utf-8')), + c_size_t(len(password)), + 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, password): + """ + Decrypts the given data with the given password. + + :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(password, 'utf-8')), + c_size_t(len(password)), + 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/wrapper/toxencryptsave_enums_and_consts.py b/toxygen/wrapper/toxencryptsave_enums_and_consts.py new file mode 100644 index 0000000..cf795f8 --- /dev/null +++ b/toxygen/wrapper/toxencryptsave_enums_and_consts.py @@ -0,0 +1,29 @@ +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