diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..65f74dc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +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 0a8182a..5e4d717 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,27 @@ +.pylint.err +.pylint.out *.pyc *.pyo + +*.zip +*.bak +*.lis +*.dst +*.so + toxygen/toxcore tests/tests -tests/libs +toxygen/libs tests/.cache tests/__pycache__ tests/avatars toxygen/libs .idea *~ +#* *.iml +*.junk + *.so *.log toxygen/build @@ -25,4 +37,5 @@ Toxygen.egg-info *.tox .cache *.db - +*~ +Makefile diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..524302e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +# -*- 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 new file mode 100644 index 0000000..e79a8ae --- /dev/null +++ b/.pylintrc @@ -0,0 +1,4 @@ +[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/toxygen/wrapper/__init__.py b/.rsync similarity index 100% rename from toxygen/wrapper/__init__.py rename to .rsync diff --git a/.rsync.sh b/.rsync.sh new file mode 100644 index 0000000..06ad4db --- /dev/null +++ b/.rsync.sh @@ -0,0 +1,8 @@ +#!/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 914fdfe..8a15a09 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,17 @@ # Toxygen -Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pure Python3. +Toxygen is powerful cross-platform [Tox](https://tox.chat/) client +for Tox and IRC/weechat written in pure Python3. -[![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) +### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) -### [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 +### Supported OS: Linux and Windows (only Linux is tested at the moment) ### Features: +- PyQt5, PyQt6, and maybe PySide2, PySide6 via qtpy +- IRC via weechat /relay +- NGC groups - 1v1 messages - File transfers - Audio calls @@ -25,14 +23,13 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu - 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 +- Proxy support - runs over tor, without DNS leaks - Avatars - Multiprofile - Multilingual @@ -43,22 +40,108 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu - Changing nospam - File resuming - Read receipts - -### 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) +- uses gevent ### Screenshots *Toxygen on Ubuntu and Windows* ![Ubuntu](/docs/ubuntu.png) ![Windows](/docs/windows.png) -### Docs -[Check /docs/ for more info](/docs/) +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 -Also visit [pythonhosted.org/Toxygen/](http://pythonhosted.org/Toxygen/) +## Forked -[Wiki](https://wiki.tox.chat/clients/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! diff --git a/ToDo.md b/ToDo.md new file mode 100644 index 0000000..9b4266f --- /dev/null +++ b/ToDo.md @@ -0,0 +1,70 @@ +# 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 new file mode 100644 index 0000000..6f1294e --- /dev/null +++ b/_Bugs/segv.err @@ -0,0 +1,130 @@ +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 new file mode 100644 index 0000000..920f606 --- /dev/null +++ b/_Bugs/tox.abilinski.com.ping @@ -0,0 +1,11 @@ + 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 deleted file mode 100644 index 0b45358..0000000 --- a/build/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index fb6c4b2..0000000 --- a/build/build.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/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 new file mode 100644 index 0000000..1694669 --- /dev/null +++ b/docs/ToxygenWeechat.md @@ -0,0 +1,171 @@ +## 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/install.md b/docs/install.md index b3c5457..7d2b773 100644 --- a/docs/install.md +++ b/docs/install.md @@ -21,8 +21,8 @@ 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. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip) -7. Unpack archive +6. git clone --depth=1 https://git.plastiras.org/emdee/toxygen/ +7. I don't know 8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\ 9. Run \toxygen\main.py. @@ -30,15 +30,22 @@ Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly r 1. Install latest Python3: ``sudo apt-get install python3`` -2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` or ``sudo pip3 install pyqt5`` +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`` 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://git.plastiras.org/emdee/toxygen/) -8. Unpack archive +4. Install PyAudio: ``sudo apt-get install portaudio19-dev python3-pyaudio`` (or ``sudo pip3 install pyaudio``) +5. Install toxygen_wrapper https://git.plastiras.org/emdee/toxygen_wrapper +6. Install the rest of the requirements: ``sudo pip3 install -m requirements.txt`` +7. git clone --depth=1 [toxygen](https://git.plastiras.org/emdee/toxygen/) +8. Look in the Makefile for the install target and type +`` +make install +`` +You should set the PIP_EXE_MSYS and PYTHON_EXE_MSYS variables and it does +`` + ${PIP_EXE_MSYS} --python ${PYTHON_EXE_MSYS} install \ + --target ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/ \ + --upgrade . +`` 9. Run app: -``python3 main.py`` +``python3 ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/bin/toxygen`` -Optional: install toxygen using setup.py: ``python3 setup.py install`` diff --git a/docs/plugin_api.md b/docs/plugin_api.md index 9eb30a4..32a27f8 100644 --- a/docs/plugin_api.md +++ b/docs/plugin_api.md @@ -53,5 +53,5 @@ Plugin's methods MUST NOT raise exceptions. # Examples -You can find examples in [official repo](https://github.com/toxygen-project/toxygen_plugins) +You can find examples in [official repo](https://git.plastiras.org/emdee/toxygen_plugins) diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..9b4266f --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,70 @@ +# 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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..37d424a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[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 new file mode 100644 index 0000000..216e1a4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +# 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 new file mode 100644 index 0000000..d7ffc22 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,54 @@ +[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 deleted file mode 100644 index fb80363..0000000 --- a/setup.py +++ /dev/null @@ -1,93 +0,0 @@ -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 new file mode 100644 index 0000000..a3f543d --- /dev/null +++ b/setup.py.dst @@ -0,0 +1,53 @@ +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 30d5edd..af8f83f 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 new file mode 100755 index 0000000..c2e645c --- /dev/null +++ b/toxygen/.pylint.sh @@ -0,0 +1,14 @@ +#!/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 70180be..4671c45 100644 --- a/toxygen/__init__.py +++ b/toxygen/__init__.py @@ -1,8 +1,3 @@ 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 new file mode 100644 index 0000000..406726f --- /dev/null +++ b/toxygen/__main__.py @@ -0,0 +1,378 @@ +# -*- 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 a23816d..4326849 100644 --- a/toxygen/app.py +++ b/toxygen/app.py @@ -1,141 +1,387 @@ +# -*- 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 -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 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 ui.login_screen import LoginScreen from user_data.profile_manager import ProfileManager + from plugin_support.plugin_support import PluginLoader + +import ui.password_screen as password_screen +from ui.login_screen import LoginScreen from ui.main_screen import MainWindow from ui import tray + import utils.ui as util_ui import utils.util as util -from contacts.profile import Profile -from file_transfers.file_transfers_handler import FileTransfersHandler +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.contacts_manager import ContactsManager -from av.calls_manager import CallsManager -from history.database import Database -from ui.widgets_factory import WidgetsFactory -from smileys.smileys import SmileyLoader -from ui.items_factories import MessagesItemsFactory, ContactItemsFactory -from messenger.messenger import Messenger -from network.tox_dns import ToxDns -from history.history import History +from contacts.group_peer_factory import GroupPeerFactory +from contacts.profile import Profile +from file_transfers.file_transfers_handler import FileTransfersHandler from file_transfers.file_transfers_messages_service import FileTransfersMessagesService from groups.groups_service import GroupsService +from history.database import Database +from history.history import History +from messenger.messenger import Messenger +from network.tox_dns import ToxDns +from smileys.smileys import SmileyLoader from ui.create_profile_screen import CreateProfileScreen -from common.provider import Provider -from contacts.group_peer_factory import GroupPeerFactory +from ui.items_factories import MessagesItemsFactory, ContactItemsFactory +from ui.widgets_factory import WidgetsFactory from user_data.backup_service import BackupService import styles.style # TODO: dynamic loading +import toxygen_wrapper.tests.support_testing as ts +global LOG +LOG = logging.getLogger('app') + +IDLE_PERIOD = 0.10 +iNODES=8 +bSHOW_TRAY=False + +def setup_logging(oArgs) -> None: + global LOG + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S', + fmt='%(levelname)s:%(name)s %(message)s') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + if coloredlogs: + aKw = dict(level=oArgs.loglevel, + logger=LOG, + fmt='%(name)s %(levelname)s %(message)s') + aKw['stream'] = sys.stdout + coloredlogs.install(**aKw) + + else: + aKw = dict(level=oArgs.loglevel, + format='%(name)s %(levelname)-4s %(message)s') + aKw['stream'] = sys.stdout + logging.basicConfig(**aKw) + + if oArgs.logfile: + oFd = open(oArgs.logfile, 'wt') + setattr(oArgs, 'log_oFd', oFd) + oHandler = logging.StreamHandler(stream=oFd) + LOG.addHandler(oHandler) + + LOG.setLevel(oArgs.loglevel) + LOG.trace = lambda l: LOG.log(0, repr(l)) + LOG.info(f"Setting loglevel to {oArgs.loglevel}") + + if oArgs.loglevel < 20: + # opencv debug + sys.OpenCV_LOADER_DEBUG = True + +#? with ignoreStderr(): for png +# silence logging PyQt5.uic.uiparser +logging.getLogger('PyQt5.uic').setLevel(logging.ERROR) +logging.getLogger('PyQt5.uic.uiparser').setLevel(logging.ERROR) +logging.getLogger('PyQt5.uic.properties').setLevel(logging.ERROR) + +global iI +iI = 0 + +sSTYLE = """ +.QWidget {font-family Helvetica;} +.QCheckBox { font-family Helvetica;} +.QComboBox { font-family Helvetica;} +.QGroupBox { font-family Helvetica;} +.QLabel {font-family Helvetica;} +.QLineEdit { font-family Helvetica;} +.QListWidget { font-family Helvetica;} +.QListWidgetItem { font-family Helvetica;} +.QMainWindow {font-family Helvetica;} +.QMenu {font-family Helvetica;} +.QMenuBar {font-family Helvetica;} +.QPlainText {font-family Courier; weight: 75;} +.QPlainTextEdit {font-family Courier;} +.QPushButton {font-family Helvetica;} +.QRadioButton { font-family Helvetica; } +.QText {font-family Courier; weight: 75; } +.QTextBrowser {font-family Courier; weight: 75; } +.QTextSingleLine {font-family Courier; weight: 75; } +.QToolBar { font-weight: bold; } +""" class App: - def __init__(self, version, path_to_profile=None, uri=None): + def __init__(self, version, oArgs): + global LOG + self._args = oArgs + self.oArgs = oArgs + self._path = path_to_profile = oArgs.profile + uri = oArgs.uri + logfile = oArgs.logfile + loglevel = oArgs.loglevel + + setup_logging(oArgs) + # sys.stderr.write( 'Command line args: ' +repr(oArgs) +'\n') + LOG.info("Command line: " +' '.join(sys.argv[1:])) + LOG.debug(f'oArgs = {oArgs}') + LOG.info("Starting toxygen version " +version) + self._version = version - self._app = self._settings = self._profile_manager = self._plugin_loader = self._messenger = None + self._tox = None + self._app = self._settings = self._profile_manager = None + self._plugin_loader = self._messenger = None self._tox = self._ms = self._init = self._main_loop = self._av_loop = None - self._uri = self._toxes = self._tray = self._file_transfer_handler = self._contacts_provider = None - self._friend_factory = self._calls_manager = self._contacts_manager = self._smiley_loader = None + self._uri = self._toxes = self._tray = None + self._file_transfer_handler = self._contacts_provider = None + self._friend_factory = self._calls_manager = None + self._contacts_manager = self._smiley_loader = None self._group_peer_factory = self._tox_dns = self._backup_service = None self._group_factory = self._groups_service = self._profile = None if uri is not None and uri.startswith('tox:'): self._uri = uri[4:] - self._path = path_to_profile + self._history = None + self.bAppExiting = False - # ----------------------------------------------------------------------------------------------------------------- # Public methods - # ----------------------------------------------------------------------------------------------------------------- - def main(self): + def set_trace(self) -> None: + """unused""" + LOG.debug('pdb.set_trace ') + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + import pdb; pdb.set_trace() + + def ten(self, i=0) -> None: + """unused""" + global iI + iI += 1 + if logging.getLogger('app').getEffectiveLevel() != 10: + sys.stderr.write('CHANGED '+str(logging.getLogger().level+'\n')) + LOG.setLevel(10) + LOG.root.setLevel(10) + logging.getLogger('app').setLevel(10) + #sys.stderr.write(f"ten '+str(iI)+' {i}"+' '+repr(LOG) +'\n') + #LOG.debug('ten '+str(iI)) + + def iMain(self) -> int: """ Main function of app. loads login screen if needed and starts main screen """ - self._app = QtWidgets.QApplication([]) + self._app = QApplication([]) self._load_icon() - if util.get_platform() == 'Linux': + # is this still needed? + if util.get_platform() == 'Linux' and \ + hasattr(QtCore.Qt, 'AA_X11InitThreads'): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) self._load_base_style() - if not self._select_and_load_profile(): - return + 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 self._try_to_update(): - return + self._load_app_styles() + if self._args.language != 'English': + # (Pdb) Fatal Python error: Segmentation fault + self._load_app_translations() + self._create_dependencies() - self._load_app_styles() - self._load_app_translations() + self._start_threads(True) - self._create_dependencies() - self._start_threads() - - if self._uri is not None: - self._ms.add_contact(self._uri) + if self._uri is not None: + self._ms.add_contact(self._uri) + except Exception as e: + LOG.error(f"Error loading profile: {e}") + sys.stderr.write(' iMain(): ' +f"Error loading profile: {e}" \ + +'\n' + traceback.format_exc()+'\n') + util_ui.message_box(str(e), + util_ui.tr('Error loading profile')) + return 4 self._app.lastWindowClosed.connect(self._app.quit) + try: + self._execute_app() + self.quit() + retval = 0 + except KeyboardInterrupt: + retval = 0 + except Exception: + retval = 1 - self._execute_app() + return retval - self._stop_app() - - # ----------------------------------------------------------------------------------------------------------------- # App executing - # ----------------------------------------------------------------------------------------------------------------- - def _execute_app(self): + def _execute_app(self) -> None: + LOG.debug("_execute_app") + while True: try: self._app.exec_() except Exception as ex: - util.log('Unhandled exception: ' + str(ex)) + LOG.error('Unhandled exception: ' + str(ex)) else: break - def _stop_app(self): - self._plugin_loader.stop() - self._stop_threads() - self._file_transfer_handler.stop() - self._tray.hide() + def quit(self, retval=0) -> None: + LOG.debug("quit") + self._stop_app() + + # failsafe: segfaults on exit - maybe it's Qt + if hasattr(self, '_tox'): + if self._tox and hasattr(self._tox, 'kill'): + LOG.debug(f"quit: Killing {self._tox}") + self._tox.kill() + del self._tox + + if hasattr(self, '_app'): + self._app.quit() + del self._app.quit + del self._app + + sys.stderr.write('quit raising SystemExit' +'\n') + # hanging on gevents + # Thread 1 "python3.9" received signal SIGSEGV, Segmentation fault. + #44 0x00007ffff7fb2f93 in () at /usr/lib/python3.9/site-packages/greenlet/_greenlet.cpython-39-x86_64-linux-gnu.so + #45 0x00007ffff7fb31ef in () at /usr/lib/python3.9/site-packages/greenlet/_greenlet.cpython-39-x86_64-linux-gnu.so + #46 0x00007ffff452165c in hb_shape_plan_create_cached2 () at /usr/lib64/libharfbuzz.so.0 + + raise SystemExit(retval) + + def _stop_app(self) -> None: + LOG.debug("_stop_app") self._save_profile() + self._history.save_history() + + self._plugin_loader.stop() + try: + self._stop_threads(is_app_closing=True) + except (Exception, RuntimeError): + # RuntimeError: cannot join current thread + pass + # I think there are threads still running here leading to a SEGV + # File "/usr/lib/python3.11/threading.py", line 1401 in run + # File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner + # File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap + + if hasattr(self, '_tray') and self._tray: + self._tray.hide() self._settings.close() + + self.bAppExiting = True + LOG.debug(f"stop_app: Killing {self._tox}") self._kill_toxav() self._kill_tox() + del self._tox + + oArgs = self._args + if hasattr(oArgs, 'log_oFd'): + LOG.debug(f"Closing {oArgs.log_oFd}") + oArgs.log_oFd.close() + delattr(oArgs, 'log_oFd') - # ----------------------------------------------------------------------------------------------------------------- # App loading - # ----------------------------------------------------------------------------------------------------------------- - def _load_base_style(self): - with open(util.join_path(util.get_styles_directory(), 'dark_style.qss')) as fl: - style = fl.read() + def _load_base_style(self) -> None: + if self._args.theme in ['', 'default']: return + + if qdarkstyle: + LOG.debug("_load_base_style qdarkstyle " +self._args.theme) + # QDarkStyleSheet + if self._args.theme == 'light': + from qdarkstyle.light.palette import LightPalette + style = qdarkstyle.load_stylesheet(palette=LightPalette) + else: + from qdarkstyle.dark.palette import DarkPalette + style = qdarkstyle.load_stylesheet(palette=DarkPalette) + else: + LOG.debug("_load_base_style qss " +self._args.theme) + name = self._args.theme + '.qss' + with open(util.join_path(util.get_styles_directory(), name)) as fl: + style = fl.read() + style += '\n' +sSTYLE self._app.setStyleSheet(style) - def _load_app_styles(self): + def _load_app_styles(self) -> None: + LOG.debug(f"_load_app_styles {list(settings.built_in_themes().keys())}") # application color scheme - if self._settings['theme'] == 'dark': - return - for theme in self._settings.built_in_themes().keys(): + if self._settings['theme'] in ['', 'default']: return + for theme in settings.built_in_themes().keys(): if self._settings['theme'] != theme: continue - theme_path = self._settings.built_in_themes()[theme] - file_path = util.join_path(util.get_styles_directory(), theme_path) - with open(file_path) as fl: - style = fl.read() + if qdarkstyle: + LOG.debug("_load_base_style qdarkstyle " +self._args.theme) + # QDarkStyleSheet + if self._args.theme == 'light': + from qdarkstyle.light.palette import LightPalette + style = qdarkstyle.load_stylesheet(palette=LightPalette) + else: + from qdarkstyle.dark.palette import DarkPalette + style = qdarkstyle.load_stylesheet(palette=DarkPalette) + else: + theme_path = settings.built_in_themes()[theme] + file_path = util.join_path(util.get_styles_directory(), theme_path) + if not os.path.isfile(file_path): + LOG.warn('_load_app_styles: no theme file ' + file_path) + continue + with open(file_path) as fl: + style = fl.read() + LOG.debug('_load_app_styles: loading theme file ' + file_path) + style += '\n' +sSTYLE self._app.setStyleSheet(style) + LOG.info('_load_app_styles: loaded theme ' +self._args.theme) break - def _load_login_screen_translations(self): + def _load_login_screen_translations(self) -> None: + LOG.debug("_load_login_screen_translations") current_language, supported_languages = self._get_languages() if current_language not in supported_languages: return @@ -145,48 +391,78 @@ class App: self._app.installTranslator(translator) self._app.translator = translator - def _load_icon(self): + def _load_icon(self) -> None: + LOG.debug("_load_icon") icon_file = os.path.join(util.get_images_directory(), 'icon.png') self._app.setWindowIcon(QtGui.QIcon(icon_file)) @staticmethod - def _get_languages(): + def _get_languages() -> tuple: + LOG.debug("_get_languages") current_locale = QtCore.QLocale() curr_language = current_locale.languageToString(current_locale.language()) - supported_languages = Settings.supported_languages() + supported_languages = settings.supported_languages() return curr_language, supported_languages - def _load_app_translations(self): - lang = Settings.supported_languages()[self._settings['language']] + def _load_app_translations(self) -> None: + LOG.debug("_load_app_translations") + lang = settings.supported_languages()[self._settings['language']] translator = QtCore.QTranslator() translator.load(os.path.join(util.get_translations_directory(), lang)) self._app.installTranslator(translator) self._app.translator = translator - def _select_and_load_profile(self): - encrypt_save = tox_encrypt_save.ToxEncryptSave() - self._toxes = user_data.toxes.ToxES(encrypt_save) + def _select_and_load_profile(self) -> bool: + LOG.debug("_select_and_load_profile: " +repr(self._path)) - if self._path is not None: # toxygen was started with path to profile - self._load_existing_profile(self._path) + if self._path is not None: + # toxygen was started with path to profile + try: + assert os.path.exists(self._path), f"FNF {self._path}" + self._load_existing_profile(self._path) + except Exception as e: + LOG.error('_load_existing_profile failed: ' + str(e)) + title = 'Loading the profile failed ' + if self._path: + title += os.path.basename(self._path) + text = 'Loading the profile failed - \n' +str(e) + if 'Dis' == 'Abled': + text += '\nLoading the profile failed - \n' \ + +str(e) +'\nContinue with a default profile?' + reply = util_ui.question(text, title) + if not reply: + LOG.debug('_load_existing_profile not continuing ') + raise + LOG.debug('_load_existing_profile continuing ') + # drop through + else: + util_ui.message_box(text, title) + raise else: auto_profile = Settings.get_auto_profile() if auto_profile is None: # no default profile + LOG.debug('_select_and_load_profile no default profile ') result = self._select_profile() if result is None: + LOG.debug('no selected profile ') return False if result.is_new_profile(): # create new profile if not self._create_new_profile(result.profile_path): + LOG.warn('no new profile ') return False + LOG.debug('created new profile ') else: # load existing profile self._load_existing_profile(result.profile_path) + # drop through self._path = result.profile_path else: # default profile + LOG.debug('loading default profile ') self._path = auto_profile self._load_existing_profile(auto_profile) - if Settings.is_active_profile(self._path): # profile is in use + if settings.is_active_profile(self._path): # profile is in use + LOG.warn(f"_select_and_load_profile active: {self._path}") profile_name = util.get_profile_name_from_path(self._path) title = util_ui.tr('Profile {}').format(profile_name) text = util_ui.tr( @@ -195,30 +471,39 @@ class App: if not reply: return False - self._settings.set_active_profile() + # is self._path right - was pathless + self._settings.set_active_profile(self._path) return True - # ----------------------------------------------------------------------------------------------------------------- # Threads - # ----------------------------------------------------------------------------------------------------------------- - def _start_threads(self, initial_start=True): + def _start_threads(self, initial_start=True) -> None: + LOG.debug(f"_start_threads before: {threading.enumerate()}") # init thread - self._init = threads.InitThread(self._tox, self._plugin_loader, self._settings, initial_start) + self._init = threads.InitThread(self._tox, + self._plugin_loader, + self._settings, + self, + initial_start) self._init.start() + def te(): return [t.name for t in threading.enumerate()] + LOG.debug(f"_start_threads init: {te()}") # starting threads for tox iterate and toxav iterate - self._main_loop = threads.ToxIterateThread(self._tox) + self._main_loop = threads.ToxIterateThread(self._tox, app=self) self._main_loop.start() + self._av_loop = threads.ToxAVIterateThread(self._tox.AV) self._av_loop.start() if initial_start: threads.start_file_transfer_thread() + LOG.debug(f"_start_threads after: {[t.name for t in threading.enumerate()]}") - def _stop_threads(self, is_app_closing=True): - self._init.stop_thread() + def _stop_threads(self, is_app_closing=True) -> None: + LOG.debug("_stop_threads") + self._init.stop_thread(1.0) self._av_loop.stop_thread() self._main_loop.stop_thread() @@ -226,34 +511,54 @@ 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): - self._load_login_screen_translations() + LOG.debug("_select_profile") + if self._args.language != 'English': + self._load_login_screen_translations() ls = LoginScreen() profiles = ProfileManager.find_profiles() ls.update_select(profiles) ls.show() self._app.exec_() - return ls.result - def _load_existing_profile(self, profile_path): - self._profile_manager = ProfileManager(self._toxes, profile_path) + def _load_existing_profile(self, profile_path) -> None: + profile_path = profile_path.replace('.json', '.tox') + LOG.info("_load_existing_profile " +repr(profile_path)) + assert os.path.exists(profile_path), profile_path + self._profile_manager = ProfileManager(self._toxes, profile_path, app=self) data = self._profile_manager.open_profile() if self._toxes.is_data_encrypted(data): + LOG.debug("_entering password") data = self._enter_password(data) - self._settings = Settings(self._toxes, profile_path.replace('.tox', '.json')) - self._tox = self._create_tox(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() - def _create_new_profile(self, profile_name): + self._tox = self._create_tox(data, self._settings) + LOG.debug("created _tox") + + def _create_new_profile(self, profile_name) -> bool: + LOG.info("_create_new_profile " + profile_name) result = self._get_create_profile_screen_result() if result is None: return False if result.save_into_default_folder: - profile_path = util.join_path(Settings.get_default_path(), profile_name + '.tox') + profile_path = util.join_path(get_user_config_path(), profile_name + '.tox') else: profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') if os.path.isfile(profile_path): @@ -261,19 +566,24 @@ class App: util_ui.tr('Error')) return False name = profile_name or 'toxygen_user' - self._tox = tox_factory() - self._tox.self_set_name(name if name else 'Toxygen User') - self._tox.self_set_status_message('Toxing on Toxygen') + assert self._args self._path = profile_path if result.password: self._toxes.set_password(result.password) - self._settings = Settings(self._toxes, self._path.replace('.tox', '.json')) + self._settings = Settings(self._toxes, + self._path.replace('.tox', '.json'), + app=self) + self._tox = self._create_tox(None, + self._settings) + self._tox.self_set_name(name if name else 'Toxygen User') + self._tox.self_set_status_message('Toxing on Toxygen') + self._profile_manager = ProfileManager(self._toxes, profile_path) try: self._save_profile() except Exception as ex: - print(ex) - util.log('Profile creation exception: ' + str(ex)) + #? print(ex) + LOG.error('Profile creation exception: ' + str(ex)) text = util_ui.tr('Profile saving error! Does Toxygen have permission to write to this directory?') util_ui.message_box(text, util_ui.tr('Error')) @@ -286,33 +596,36 @@ 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): + def _save_profile(self, data=None) -> None: + LOG.debug("_save_profile") data = data or self._tox.get_savedata() self._profile_manager.save_profile(data) - # ----------------------------------------------------------------------------------------------------------------- # Other private methods - # ----------------------------------------------------------------------------------------------------------------- def _enter_password(self, data): """ Show password screen """ + LOG.debug("_enter_password") p = password_screen.PasswordScreen(self._toxes, data) p.show() self._app.lastWindowClosed.connect(self._app.quit) self._app.exec_() if p.result is not None: return p.result - self._force_exit() + self._force_exit(0) + return None - def _reset(self): + def _reset(self) -> None: + LOG.debug("_reset") """ Create new tox instance (new network settings) :return: tox instance @@ -323,50 +636,102 @@ class App: self._save_profile(data) self._kill_toxav() self._kill_tox() - # create new tox instance - self._tox = self._create_tox(data) - self._start_threads(False) + try: + # create new tox instance + self._tox = self._create_tox(data, self._settings) + assert self._tox + self._start_threads(False) - tox_savers = [self._friend_factory, self._group_factory, self._plugin_loader, self._contacts_manager, - self._contacts_provider, self._messenger, self._file_transfer_handler, self._groups_service, - self._profile] - for tox_saver in tox_savers: - tox_saver.set_tox(self._tox) + 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() + self._init_callbacks() + except BaseException as e: + LOG.error(f"_reset : {e}") + LOG.debug('_reset: ' \ + +'\n' + traceback.format_exc()) + title = util_ui.tr('Reset Error') + text = util_ui.tr('Error:') + str(e) + util_ui.message_box(text, title) - def _create_dependencies(self): - self._backup_service = BackupService(self._settings, self._profile_manager) + def _create_dependencies(self) -> None: + LOG.info(f"_create_dependencies toxygen version {self._version}") + if hasattr(self._args, 'update') and self._args.update: + self._backup_service = BackupService(self._settings, + self._profile_manager) self._smiley_loader = SmileyLoader(self._settings) self._tox_dns = ToxDns(self._settings) - self._ms = MainWindow(self._settings, self._tray) - db = Database(self._path.replace('.tox', '.db'), self._toxes) + self._ms = MainWindow(self._settings, self._tray, self) + + db_path = self._path.replace('.tox', '.db') + db = Database(db_path, self._toxes) + if os.path.exists(db_path) and hasattr(db, 'open'): + db.open() + + assert self._tox contact_items_factory = ContactItemsFactory(self._settings, self._ms) - self._friend_factory = FriendFactory(self._profile_manager, self._settings, - self._tox, db, contact_items_factory) - self._group_factory = GroupFactory(self._profile_manager, self._settings, self._tox, db, contact_items_factory) - self._group_peer_factory = GroupPeerFactory(self._tox, self._profile_manager, db, contact_items_factory) - self._contacts_provider = ContactProvider(self._tox, self._friend_factory, self._group_factory, - self._group_peer_factory) - 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, + app=self) + self._profile = Profile(self._profile_manager, + self._tox, + self._ms, + self._contacts_provider, + self._reset) self._init_profile() self._plugin_loader = PluginLoader(self._settings, self) history = None - messages_items_factory = MessagesItemsFactory(self._settings, self._plugin_loader, self._smiley_loader, - self._ms, lambda m: history.delete_message(m)) - history = History(self._contacts_provider, db, self._settings, self._ms, messages_items_factory) - self._contacts_manager = ContactsManager(self._tox, self._settings, self._ms, self._profile_manager, - self._contacts_provider, history, self._tox_dns, + messages_items_factory = MessagesItemsFactory(self._settings, + self._plugin_loader, + self._smiley_loader, + self._ms, + lambda m: history.delete_message(m)) + history = History(self._contacts_provider, db, + self._settings, self._ms, messages_items_factory) + self._contacts_manager = ContactsManager(self._tox, + self._settings, + self._ms, + self._profile_manager, + self._contacts_provider, + history, + self._tox_dns, messages_items_factory) history.set_contacts_manager(self._contacts_manager) - self._calls_manager = CallsManager(self._tox.AV, self._settings, self._ms, self._contacts_manager) - self._messenger = Messenger(self._tox, self._plugin_loader, self._ms, self._contacts_manager, + self._history = history + self._calls_manager = CallsManager(self._tox.AV, + self._settings, + self._ms, + self._contacts_manager, + self) + self._messenger = Messenger(self._tox, + self._plugin_loader, self._ms, self._contacts_manager, self._contacts_provider, messages_items_factory, self._profile, self._calls_manager) file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, @@ -376,49 +741,310 @@ 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) - 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: + 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.show() + if bSHOW_TRAY: # broken + # the tray icon does not die with the app + self._tray.show() self._ms.show() + # FixMe: + self._log = lambda line: LOG.log(self._args.loglevel, + self._ms.status(line)) + # self._ms._log = self._log # was used in callbacks.py + + if False: + self.status_handler = logging.Handler() + self.status_handler.setLevel(logging.INFO) # self._args.loglevel + self.status_handler.handle = self._ms.status + self._init_callbacks() + LOG.info("_create_dependencies toxygen version " +self._version) def _try_to_update(self): + LOG.debug("_try_to_update") updating = updater.start_update_if_needed(self._version, self._settings) if updating: + LOG.info("Updating toxygen version " +self._version) self._save_profile() self._settings.close() self._kill_toxav() self._kill_tox() return updating - def _create_tox(self, data): - return tox_factory(data, self._settings) + def _create_tox(self, data, settings_): + LOG.info("_create_tox calling tox_factory") + assert self._args + retval = tox_factory(data=data, settings=settings_, + args=self._args, app=self) + LOG.debug("_create_tox succeeded") + self._tox = retval + return retval - def _force_exit(self): - raise SystemExit() + def _force_exit(self, retval=0) -> None: + LOG.debug("_force_exit") + sys.exit(0) - 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_callbacks(self, ms=None) -> None: + LOG.debug("_init_callbacks") + # this will block if you are not connected + callbacks.init_callbacks(self._tox, self._profile, self._settings, + self._plugin_loader, self._contacts_manager, + self._calls_manager, + self._file_transfer_handler, self._ms, + self._tray, + self._messenger, self._groups_service, + self._contacts_provider, self._ms) - def _init_profile(self): + def _init_profile(self) -> None: + LOG.debug("_init_profile") if not self._profile.has_avatar(): self._profile.reset_avatar(self._settings['identicons']) - def _kill_toxav(self): + def _kill_toxav(self) -> None: +# LOG_debug("_kill_toxav") self._calls_manager.set_toxav(None) self._tox.AV.kill() - def _kill_tox(self): + def _kill_tox(self) -> None: +# LOG.debug("_kill_tox") self._tox.kill() + + def loop(self, n) -> None: + """ + Im guessing - there are 4 sleeps - time, tox, and Qt gevent + """ + interval = self._tox.iteration_interval() + for i in range(n): + self._tox.iterate() + #? QtCore.QThread.msleep(interval) + # Cooperative yield, allow gevent to monitor file handles via libevent + gevent.sleep(interval / 1000.0) + # NO? + QtCore.QCoreApplication.processEvents() + + def _test_tox(self) -> None: + self.test_net(iMax=8) + self._ms.log_console() + + def test_net(self, lElts=None, oThread=None, iMax=4) -> None: + + # bootstrap + LOG.debug('test_net: Calling generate_nodes: udp') + lNodes = ts.generate_nodes(oArgs=self._args, + ipv='ipv4', + udp_not_tcp=True) + self._settings['current_nodes_udp'] = lNodes + if not lNodes: + LOG.warn('empty generate_nodes udp') + LOG.debug('test_net: Calling generate_nodes: tcp') + lNodes = ts.generate_nodes(oArgs=self._args, + ipv='ipv4', + udp_not_tcp=False) + self._settings['current_nodes_tcp'] = lNodes + if not lNodes: + LOG.warn('empty generate_nodes tcp') + + # if oThread and oThread._stop_thread: return + LOG.debug("test_net network=" +self._args.network +' iMax=' +str(iMax)) + if self._args.network not in ['local', 'localnew', 'newlocal']: + b = ts.bAreWeConnected() + if b is None: + i = os.system('ip route|grep ^def') + if i > 0: + b = False + else: + b = True + if not b: + LOG.warn("No default route for network " +self._args.network) + text = 'You have no default route - are you connected?' + reply = util_ui.question(text, "Are you connected?") + if not reply: return + iMax = 1 + else: + LOG.debug("Have default route for network " +self._args.network) + + lUdpElts = self._settings['current_nodes_udp'] + if self._args.proxy_type <= 0 and not lUdpElts: + title = 'test_net Error' + text = 'Error: ' + str('No UDP nodes') + util_ui.message_box(text, title) + return + lTcpElts = self._settings['current_nodes_tcp'] + if self._args.proxy_type > 0 and not lTcpElts: + title = 'test_net Error' + text = 'Error: ' + str('No TCP nodes') + util_ui.message_box(text, title) + return + LOG.debug(f"test_net {self._args.network} lenU={len(lUdpElts)} lenT={len(lTcpElts)} iMax={iMax}") + i = 0 + while i < iMax: + # if oThread and oThread._stop_thread: return + i = i + 1 + LOG.debug(f"bootstrapping status proxy={self._args.proxy_type} # {i}") + if self._args.proxy_type == 0: + self._test_bootstrap(lUdpElts) + else: + self._test_bootstrap([lUdpElts[0]]) + LOG.debug(f"relaying status # {i}") + self._test_relays(self._settings['current_nodes_tcp']) + status = self._tox.self_get_connection_status() + if status > 0: + LOG.info(f"Connected # {i}" +' : ' +repr(status)) + break + LOG.trace(f"Connected status #{i}: {status}") + + def _test_env(self) -> None: + _settings = self._settings + if 'proxy_type' not in _settings or _settings['proxy_type'] == 0 or \ + not _settings['proxy_host'] or not _settings['proxy_port']: + env = dict( prot = 'ipv4') + lElts = self._settings['current_nodes_udp'] + elif _settings['proxy_type'] == 2: + env = dict(prot = 'socks5', + https_proxy='', \ + socks_proxy='socks5://' \ + +_settings['proxy_host'] +':' \ + +str(_settings['proxy_port'])) + lElts = self._settings['current_nodes_tcp'] + elif _settings['proxy_type'] == 1: + env = dict(prot = 'https', + socks_proxy='', \ + https_proxy='http://' \ + +_settings['proxy_host'] +':' \ + +str(_settings['proxy_port'])) + lElts = _settings['current_nodes_tcp'] +# LOG.debug(f"test_env {len(lElts)}") + return env + + def _test_bootstrap(self, lElts=None) -> None: + if lElts is None: + lElts = self._settings['current_nodes_udp'] + LOG.debug(f"_test_bootstrap #Elts={len(lElts)}") + if not lElts: + return + shuffle(lElts) + ts.bootstrap_udp(lElts[:iNODES], [self._tox]) + LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) + + def _test_relays(self, lElts=None) -> None: + if lElts is None: + lElts = self._settings['current_nodes_tcp'] + shuffle(lElts) + LOG.debug(f"_test_relays {len(lElts)}") + ts.bootstrap_tcp(lElts[:iNODES], [self._tox]) + + def _test_nmap(self, lElts=None) -> None: + LOG.debug("_test_nmap") + if not self._tox: return + title = 'Extended Test Suite' + text = 'Run the Extended Test Suite?\nThe program may freeze for 1-10 minutes.' + i = os.system('ip route|grep ^def >/dev/null') + if i > 0: + text += '\nYou have no default route - are you connected?' + reply = util_ui.question(text, title) + if not reply: return + + if self._args.proxy_type == 0: + sProt = "udp4" + else: + sProt = "tcp4" + if lElts is None: + if self._args.proxy_type == 0: + lElts = self._settings['current_nodes_udp'] + else: + lElts = self._settings['current_nodes_tcp'] + shuffle(lElts) + try: + ts.bootstrap_iNmapInfo(lElts, self._args, sProt) + except Exception as e: + LOG.error(f"test_nmap ' +' : {e}") + LOG.error('_test_nmap(): ' \ + +'\n' + traceback.format_exc()) + title = 'Test Suite Error' + text = 'Error: ' + str(e) + util_ui.message_box(text, title) + + # LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) + self._ms.log_console() + + def _test_main(self) -> None: + from toxygen_toxygen_wrapper.toxygen_wrapper.tests.tests_wrapper import main as tests_main + LOG.debug("_test_main") + if not self._tox: return + title = 'Extended Test Suite' + text = 'Run the Extended Test Suite?\nThe program may freeze for 20-60 minutes.' + reply = util_ui.question(text, title) + if reply: + if hasattr(self._args, 'proxy_type') and self._args.proxy_type: + lArgs = ['--proxy_host', self._args.proxy_host, + '--proxy_port', str(self._args.proxy_port), + '--proxy_type', str(self._args.proxy_type), ] + else: + lArgs = list() + try: + tests_main(lArgs) + except Exception as e: + LOG.error(f"_test_socks(): {e}") + LOG.error('_test_socks(): ' \ + +'\n' + traceback.format_exc()) + title = 'Extended Test Suite Error' + text = 'Error:' + str(e) + util_ui.message_box(text, title) + self._ms.log_console() + +#? unused +class GEventProcessing: + """Interoperability class between Qt/gevent that allows processing gevent + tasks during Qt idle periods.""" + def __init__(self, idle_period=IDLE_PERIOD): + # Limit the IDLE handler's frequency while still allow for gevent + # to trigger a microthread anytime + self._idle_period = idle_period + # IDLE timer: on_idle is called whenever no Qt events left for + # processing + self._timer = QTimer() + self._timer.timeout.connect(self.process_events) + self._timer.start(0) + def __enter__(self) -> None: + pass + + def __exit__(self, *exc_info) -> None: + self._timer.stop() + + def process_events(self, idle_period=None) -> None: + if idle_period is None: + idle_period = self._idle_period + # Cooperative yield, allow gevent to monitor file handles via libevent + gevent.sleep(idle_period) + #? QtCore.QCoreApplication.processEvents() diff --git a/toxygen/av/call.py b/toxygen/av/call.py index d3e023b..73caa25 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,9 +17,7 @@ class Call: is_active = property(get_is_active, set_is_active) - # ----------------------------------------------------------------------------------------------------------------- # Audio - # ----------------------------------------------------------------------------------------------------------------- def get_in_audio(self): return self._in_audio @@ -37,9 +35,7 @@ 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 d5f2fe7..9b40fc1 100644 --- a/toxygen/av/calls.py +++ b/toxygen/av/calls.py @@ -1,21 +1,80 @@ -import pyaudio +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import time import threading -from wrapper.toxav_enums import * -import cv2 +import logging import itertools -import numpy as np + +from toxygen_wrapper.toxav_enums import * +from toxygen_wrapper.tests import support_testing as ts +from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +with ts.ignoreStderr(): + import pyaudio from av import screen_sharing from av.call import Call import common.tox_save +from middleware.threads import BaseQThread +from utils import ui as util_ui +from middleware.threads import invoke_in_main_thread +# from middleware.threads import BaseThread + +sleep = time.sleep + +global LOG +LOG = logging.getLogger('app.'+__name__) + +TIMER_TIMEOUT = 30.0 +iFPS = 25 + +class AudioThread(BaseQThread): + def __init__(self, av, name=''): + super().__init__() + self.av = av + self._name = name + + def join(self, ito=ts.iTHREAD_TIMEOUT): + LOG_DEBUG(f"AudioThread join {self}") + # dunno + + def run(self) -> None: + LOG_DEBUG('AudioThread run: ') + # maybe not needed + while not self._stop_thread: + self.av.send_audio() + sleep(100.0 / 1000.0) + +class VideoThread(BaseQThread): + def __init__(self, av, name=''): + super().__init__() + self.av = av + self._name = name + + def join(self, ito=ts.iTHREAD_TIMEOUT): + LOG_DEBUG(f"VideoThread join {self}") + # dunno + + def run(self) -> None: + LOG_DEBUG('VideoThread run: ') + # maybe not needed + while not self._stop_thread: + self.av.send_video() + sleep(100.0 / 1000.0) class AV(common.tox_save.ToxAvSave): def __init__(self, toxav, settings): super().__init__(toxav) + self._toxav = toxav self._settings = settings self._running = True + s = settings + if 'video' not in s: + LOG.warn("AV.__init__ 'video' not in s" ) + LOG.debug(f"AV.__init__ {s}" ) + elif 'device' not in s['video']: + LOG.warn("AV.__init__ 'device' not in s.video" ) + LOG.debug(f"AV.__init__ {s['video']}" ) self._calls = {} # dict: key - friend number, value - Call instance @@ -25,65 +84,125 @@ 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_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000 + self._audio_rate_pa = 48000 + self._audio_rate_tox = 48000 + self._audio_rate_pa = 48000 + self._audio_krate_tox_audio = self._audio_rate_tox // 1000 + self._audio_krate_tox_video = 5000 + self._audio_sample_count_pa = self._audio_rate_pa * self._audio_channels * self._audio_duration // 1000 + self._audio_sample_count_tox = self._audio_rate_tox * self._audio_channels * self._audio_duration // 1000 self._video = None self._video_thread = None - self._video_running = False + self._video_running = None - self._video_width = 640 - self._video_height = 480 + self._video_width = 320 + self._video_height = 240 - def stop(self): + # was iOutput = self._settings._args.audio['output'] + iInput = self._settings['audio']['input'] + self.lPaSampleratesI = ts.lSdSamplerates(iInput) + iOutput = self._settings['audio']['output'] + self.lPaSampleratesO = ts.lSdSamplerates(iOutput) + + global oPYA + oPYA = self._audio = pyaudio.PyAudio() + + def stop(self) -> None: + LOG_DEBUG(f"AV.CA stop {self._video_thread}") self._running = False self.stop_audio_thread() self.stop_video_thread() - def __contains__(self, friend_number): + def __contains__(self, friend_number:int) -> bool: return friend_number in self._calls - # ----------------------------------------------------------------------------------------------------------------- # Calls - # ----------------------------------------------------------------------------------------------------------------- def __call__(self, friend_number, audio, video): """Call friend with specified number""" - self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0) + if friend_number in self._calls: + LOG.warn(f"__call__ already has {friend_number}") + return + if self._audio_krate_tox_audio not in ts.lToxSampleratesK: + LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") + + try: + self._toxav.call(friend_number, + self._audio_krate_tox_audio if audio else 0, + self._audio_krate_tox_video if video else 0) + except Exception as e: + LOG.warn(f"_toxav.call already has {friend_number}") + return self._calls[friend_number] = Call(audio, video) - threading.Timer(30.0, lambda: self.finish_not_started_call(friend_number)).start() + threading.Timer(TIMER_TIMEOUT, + lambda: self.finish_not_started_call(friend_number)).start() def accept_call(self, friend_number, audio_enabled, video_enabled): + # obsolete + self.call_accept_call(friend_number, audio_enabled, video_enabled) + + def call_accept_call(self, friend_number, audio_enabled, video_enabled) -> None: + # called from CM.accept_call in a try: + LOG.debug(f"call_accept_call from F={friend_number} R={self._running}" + + f" A={audio_enabled} V={video_enabled}") + # import pdb; pdb.set_trace() - gets into q Qt exec_ problem + # ts.trepan_handler() + + if self._audio_krate_tox_audio not in ts.lToxSampleratesK: + LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") if self._running: self._calls[friend_number] = Call(audio_enabled, video_enabled) - self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0) - if audio_enabled: - self.start_audio_thread() + # audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + # video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + try: + self._toxav.answer(friend_number, + self._audio_krate_tox_audio if audio_enabled else 0, + self._audio_krate_tox_video if video_enabled else 0) + except Exception as e: + LOG.error(f"AV accept_call error from {friend_number} {self._running} {e}") + raise if video_enabled: + # may raise self.start_video_thread() + if audio_enabled: + LOG.debug(f"calls accept_call calling start_audio_thread F={friend_number}") + # may raise + self.start_audio_thread() - def finish_call(self, friend_number, by_friend=False): - if not by_friend: - self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) + def finish_call(self, friend_number, by_friend=False) -> None: + LOG.debug(f"finish_call {friend_number}") if friend_number in self._calls: del self._calls[friend_number] - if not len(list(filter(lambda c: c.out_audio, self._calls))): + try: + # AttributeError: 'int' object has no attribute 'out_audio' + if not len(list(filter(lambda c: c.out_audio, self._calls))): + self.stop_audio_thread() + if not len(list(filter(lambda c: c.out_video, self._calls))): + self.stop_video_thread() + except Exception as e: + LOG.error(f"finish_call FixMe: {e}") + # dunno self.stop_audio_thread() - 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): + def finish_not_started_call(self, friend_number:int) -> None: if friend_number in self: call = self._calls[friend_number] if not call.is_active: self.finish_call(friend_number) - def toxav_call_state_cb(self, friend_number, state): + def toxav_call_state_cb(self, friend_number, state) -> None: """ New call state """ + LOG.debug(f"toxav_call_state_cb {friend_number}") call = self._calls[friend_number] call.is_active = True @@ -96,41 +215,108 @@ 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): + def is_video_call(self, number) -> bool: return number in self and self._calls[number].in_video - # ----------------------------------------------------------------------------------------------------------------- # Threads - # ----------------------------------------------------------------------------------------------------------------- - def start_audio_thread(self): + def start_audio_thread(self, bSTREAM_CALLBACK=False) -> None: """ Start audio sending + from a callback """ + # called from call_accept_call in an try: from CM.accept_call + global oPYA + # was iInput = self._settings._args.audio['input'] + iInput = self._settings['audio']['input'] if self._audio_thread is not None: + LOG_WARN(f"start_audio_thread device={iInput}") return + LOG_DEBUG(f"start_audio_thread device={iInput}") + lPaSamplerates = ts.lSdSamplerates(iInput) + if not(len(lPaSamplerates)): + e = f"No sample rates for device: audio[input]={iInput}" + LOG_WARN(f"start_audio_thread {e}") + #?? dunno - cancel call? - no let the user do it + # return + # just guessing here in case that's a false negative + lPaSamplerates = [round(oPYA.get_device_info_by_index(iInput)['defaultSampleRate'])] + if lPaSamplerates and self._audio_rate_pa in lPaSamplerates: + pass + elif lPaSamplerates: + LOG_WARN(f"Setting audio_rate to: {lPaSamplerates[0]}") + self._audio_rate_pa = lPaSamplerates[0] + elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iInput): + self._audio_rate_pa = oPYA.get_device_info_by_index(iInput)['defaultSampleRate'] + LOG_WARN(f"setting to defaultSampleRate") + else: + LOG_WARN(f"{self._audio_rate_pa} not in {lPaSamplerates}") + # a float is in here - must it be int? + if type(self._audio_rate_pa) == float: + self._audio_rate_pa = round(self._audio_rate_pa) + try: + if self._audio_rate_pa not in lPaSamplerates: + LOG_WARN(f"PAudio sampling rate was {self._audio_rate_pa} changed to {lPaSamplerates[0]}") + LOG_DEBUG(f"lPaSamplerates={lPaSamplerates}") + self._audio_rate_pa = lPaSamplerates[0] + else: + LOG_DEBUG( f"start_audio_thread framerate: {self._audio_rate_pa}" \ + +f" device: {iInput}" + +f" supported: {lPaSamplerates}") - self._audio_running = True + 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 = 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) + 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_thread = threading.Thread(target=self.send_audio) - self._audio_thread.start() - - def stop_audio_thread(self): + def stop_audio_thread(self) -> None: + LOG_DEBUG(f"stop_audio_thread {self._audio_stream}") if self._audio_thread is None: return - self._audio_running = False - - self._audio_thread.join() + self._audio_thread._stop_thread = True self._audio_thread = None self._audio_stream = None @@ -141,99 +327,215 @@ class AV(common.tox_save.ToxAvSave): self._out_stream.close() self._out_stream = None - def start_video_thread(self): + def start_video_thread(self) -> None: if self._video_thread is not None: return + s = self._settings + if 'video' not in s: + LOG.warn("AV.__init__ 'video' not in s" ) + LOG.debug(f"start_video_thread {s}" ) + raise RuntimeError("start_video_thread not 'video' in s)" ) + if 'device' not in s['video']: + LOG.error("start_video_thread not 'device' in s['video']" ) + LOG.debug(f"start_video_thread {s['video']}" ) + raise RuntimeError("start_video_thread not 'device' ins s['video']" ) + self._video_width = s['video']['width'] + self._video_height = s['video']['height'] - self._video_running = True - 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']) + # dunno + if s['video']['device'] == -1: + self._video = screen_sharing.DesktopGrabber(s['video']['x'], + s['video']['y'], + s['video']['width'], + s['video']['height']) else: - self._video = cv2.VideoCapture(self._settings.video['device']) - self._video.set(cv2.CAP_PROP_FPS, 25) + 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_thread = threading.Thread(target=self.send_video) + self._video_running = True + self._video_thread = VideoThread(self, + name='_video_thread') self._video_thread.start() - def stop_video_thread(self): + def stop_video_thread(self) -> None: + LOG_DEBUG(f"stop_video_thread {self._video_thread}") if self._video_thread is None: return + self._video_thread._stop_thread = True self._video_running = False - self._video_thread.join() + i = 0 + while i < ts.iTHREAD_JOINS: + self._video_thread.join(ts.iTHREAD_TIMEOUT) + try: + if not self._video_thread.is_alive(): break + except: + break + i = i + 1 + else: + LOG.warn("self._video_thread.is_alive BLOCKED") self._video_thread = None self._video = None - - # ----------------------------------------------------------------------------------------------------------------- # Incoming chunks - # ----------------------------------------------------------------------------------------------------------------- - def audio_chunk(self, samples, channels_count, rate): + def audio_chunk(self, samples, channels_count, rate) -> None: """ Incoming chunk """ - + # from callback if self._out_stream is None: - 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) + # was iOutput = self._settings._args.audio['output'] + iOutput = self._settings['audio']['output'] + if self.lPaSampleratesO and rate in self.lPaSampleratesO: + LOG_DEBUG(f"Using rate {rate} in self.lPaSampleratesO") + elif self.lPaSampleratesO: + LOG_WARN(f"{rate} not in {self.lPaSampleratesO}") + LOG_WARN(f"Setting audio_rate to: {self.lPaSampleratesO[0]}") + rate = self.lPaSampleratesO[0] + elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iOutput): + rate = round(oPYA.get_device_info_by_index(iOutput)['defaultSampleRate']) + LOG_WARN(f"Setting rate to {rate} empty self.lPaSampleratesO") + else: + LOG_WARN(f"Using rate {rate} empty self.lPaSampleratesO") + if type(rate) == float: + rate = round(rate) + # test output device? + # [Errno -9985] Device unavailable + try: + with ts.ignoreStderr(): + self._out_stream = oPYA.open(format=pyaudio.paInt16, + channels=channels_count, + rate=rate, + output_device_index=iOutput, + output=True) + except Exception as e: + LOG_ERROR(f"Error playing audio_chunk creating self._out_stream output_device_index={iOutput} {e}") + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Error Chunking audio")) + # dunno + self.stop() + return + + iOutput = self._settings['audio']['output'] +#trace LOG_DEBUG(f"audio_chunk output_device_index={iOutput} rate={rate} channels={channels_count}") + try: + self._out_stream.write(samples) + except Exception as e: + # OSError: [Errno -9999] Unanticipated host error + LOG_WARN(f"audio_chunk output_device_index={iOutput} {e}") - # ----------------------------------------------------------------------------------------------------------------- # AV sending - # ----------------------------------------------------------------------------------------------------------------- - def send_audio(self): + def send_audio_data(self, data, count, *largs, **kwargs) -> None: + # callback + pcm = data + # :param sampling_rate: Audio sampling rate used in this frame. + try: + if self._toxav is None: + LOG_ERROR("_toxav not initialized") + return + if self._audio_rate_tox not in ts.lToxSamplerates: + LOG_WARN(f"ToxAudio sampling rate was {self._audio_rate_tox} changed to {ts.lToxSamplerates[0]}") + self._audio_rate_tox = ts.lToxSamplerates[0] + + for friend_num in self._calls: + if self._calls[friend_num].out_audio: + # app.av.calls ERROR Error send_audio audio_send_frame: This client is currently not in a call with the friend. + self._toxav.audio_send_frame(friend_num, + pcm, + count, + self._audio_channels, + self._audio_rate_tox) + + except Exception as e: + LOG.error(f"Error send_audio_data audio_send_frame: {e}") + LOG.debug(f"send_audio_data self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}") + self.stop_audio_thread() + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Error send_audio_data audio_send_frame")) + #? stop ? endcall? + + def send_audio(self) -> None: """ This method sends audio to friends """ - + i=0 + count = self._audio_sample_count_tox + LOG_DEBUG(f"send_audio stream={self._audio_stream}") while self._audio_running: try: - pcm = self._audio_stream.read(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 + pcm = self._audio_stream.read(count, exception_on_overflow=False) + if not pcm: + sleep(0.1) + else: + self.send_audio_data(pcm, count) except: - pass + LOG_DEBUG(f"error send_audio {i}") + else: + LOG_TRACE(f"send_audio {i}") + i += 1 + sleep(0.01) - time.sleep(0.01) - - def send_video(self): + def send_video(self) -> None: """ This method sends video to friends """ +# LOG_DEBUG(f"send_video thread={threading.current_thread().name}" +# +f" self._video_running={self._video_running}" +# +f" device: {self._settings['video']['device']}" ) while self._video_running: try: result, frame = self._video.read() - if 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: + if not result: + LOG_WARN(f"send_video video_send_frame _video.read result={result}") + break + if frame is None: + LOG_WARN(f"send_video video_send_frame _video.read result={result} frame={frame}") + continue + + LOG_TRACE(f"send_video video_send_frame _video.read result={result}") + height, width, channels = frame.shape + friends = [] + for friend_num in self._calls: + if self._calls[friend_num].out_video: + friends.append(friend_num) + if len(friends) == 0: + LOG_WARN(f"send_video video_send_frame no friends") + else: + LOG_TRACE(f"send_video video_send_frame {friends}") + friend_num = friends[0] + try: + y, u, v = self.convert_bgr_to_yuv(frame) + self._toxav.video_send_frame(friend_num, width, height, y, u, v) + except Exception as e: + LOG_WARN(f"send_video video_send_frame ERROR {e}") + pass + + except Exception as e: + LOG_ERROR(f"send_video video_send_frame {e}") pass - time.sleep(0.01) + sleep( 1.0/iFPS) - def convert_bgr_to_yuv(self, frame): + def convert_bgr_to_yuv(self, frame) -> tuple: """ :param frame: input bgr frame :return y, u, v: y, u, v values of frame @@ -264,16 +566,20 @@ 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)) - u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int) + import numpy as np + # was np.int + u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2] u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:] u = list(itertools.chain.from_iterable(u)) - v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int) + v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2] v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:] v = list(itertools.chain.from_iterable(v)) diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py index 5a48672..d0d6683 100644 --- a/toxygen/av/calls_manager.py +++ b/toxygen/av/calls_manager.py @@ -1,29 +1,40 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import sys import threading -import cv2 +import traceback +import logging + +from qtpy import QtCore + import av.calls from messenger.messages import * from ui import av_widgets import common.event as event +import utils.ui as util_ui +from toxygen_wrapper.tests import support_testing as ts +global LOG +LOG = logging.getLogger('app.'+__name__) class CallsManager: - def __init__(self, toxav, settings, screen, contacts_manager): - self._call = av.calls.AV(toxav, settings) # object with data about calls + def __init__(self, toxav, settings, main_screen, contacts_manager, app=None): + self._callav = av.calls.AV(toxav, settings) # object with data about calls + self._call = self._callav self._call_widgets = {} # dict of incoming call widgets self._incoming_calls = set() self._settings = settings - self._screen = screen + self._main_screen = main_screen self._contacts_manager = contacts_manager self._call_started_event = event.Event() # friend_number, audio, video, is_outgoing self._call_finished_event = event.Event() # friend_number, is_declined + self._app = app - def set_toxav(self, toxav): - self._call.set_toxav(toxav) + def set_toxav(self, toxav) -> None: + self._callav.set_toxav(toxav) - # ----------------------------------------------------------------------------------------------------------------- # Events - # ----------------------------------------------------------------------------------------------------------------- def get_call_started_event(self): return self._call_started_event @@ -35,35 +46,33 @@ class CallsManager: call_finished_event = property(get_call_finished_event) - # ----------------------------------------------------------------------------------------------------------------- # AV support - # ----------------------------------------------------------------------------------------------------------------- - def call_click(self, audio=True, video=False): + def call_click(self, audio=True, video=False) -> None: """User clicked audio button in main window""" num = self._contacts_manager.get_active_number() if not self._contacts_manager.is_active_a_friend(): return - if num not in self._call and self._contacts_manager.is_active_online(): # start call - if not self._settings.audio['enabled']: + if num not in self._callav and self._contacts_manager.is_active_online(): # start call + if not self._settings['audio']['enabled']: return - self._call(num, audio, video) - self._screen.active_call() + self._callav(num, audio, video) + self._main_screen.active_call() self._call_started_event(num, audio, video, True) - elif num in self._call: # finish or cancel call if you call with active friend + elif num in self._callav: # finish or cancel call if you call with active friend self.stop_call(num, False) - def incoming_call(self, audio, video, friend_number): + def incoming_call(self, audio, video, friend_number) -> None: """ Incoming call from friend. """ - if not self._settings.audio['enabled']: - return + LOG.debug(f"CM incoming_call {friend_number}") + # if not self._settings['audio']['enabled']: return friend = self._contacts_manager.get_friend_by_number(friend_number) self._call_started_event(friend_number, audio, video, False) self._incoming_calls.add(friend_number) if friend_number == self._contacts_manager.get_active_number(): - self._screen.incoming_call() + self._main_screen.incoming_call() else: friend.actions = True text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") @@ -71,46 +80,105 @@ 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): + def accept_call(self, friend_number, audio, video) -> None: """ 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] - def stop_call(self, friend_number, by_friend): + LOG.debug(f"CM accept_call from friend_number={friend_number} {audio} {video}") + sys.stdout.flush() + + try: + self._main_screen.active_call() + # failsafe added somewhere this was being left up + self.close_call(friend_number) + QtCore.QCoreApplication.processEvents() + + self._callav.call_accept_call(friend_number, audio, video) + LOG.debug(f"accept_call _call.accept_call CALLED f={friend_number}") + except Exception as e: + # + LOG.error(f"accept_call _call.accept_call ERROR for {friend_number} {e}") + LOG.debug(traceback.print_exc()) + self._main_screen.call_finished() + if hasattr(self._main_screen, '_settings') and \ + 'audio' in self._main_screen._settings and \ + 'input' in self._main_screen._settings['audio']: + iInput = self._settings['audio']['input'] + iOutput = self._settings['audio']['output'] + iVideo = self._settings['video']['device'] + LOG.debug(f"iInput={iInput} iOutput={iOutput} iVideo={iVideo}") + elif hasattr(self._main_screen, '_settings') and \ + hasattr(self._main_screen._settings, 'audio') and \ + 'input' not in self._main_screen._settings['audio']: + LOG.warn(f"'audio' not in {self._main_screen._settings}") + elif hasattr(self._main_screen, '_settings') and \ + hasattr(self._main_screen._settings, 'audio') and \ + 'input' not in self._main_screen._settings['audio']: + LOG.warn(f"'audio' not in {self._main_screen._settings}") + else: + LOG.warn(f"_settings not in self._main_screen") + util_ui.message_box(str(e), + util_ui.tr('ERROR Accepting call from {friend_number}')) + finally: + # does not terminate call - just the av_widget + LOG.debug(f"CM.accept_call close av_widget") + self.close_call(friend_number) + LOG.debug(f" closed self._call_widgets[{friend_number}]") + + def close_call(self, friend_number:int) -> None: + # refactored out from above because the accept window not getting + # taken down in some accept audio calls + LOG.debug(f"close_call {friend_number}") + try: + if friend_number in self._call_widgets: + self._call_widgets[friend_number].close() + del self._call_widgets[friend_number] + if friend_number in self._incoming_calls: + self._incoming_calls.remove(friend_number) + except Exception as e: + # RuntimeError: wrapped C/C++ object of type IncomingCallWidget has been deleted + + LOG.warn(f" closed self._call_widgets[{friend_number}] {e}") + # invoke_in_main_thread(QtCore.QCoreApplication.processEvents) + QtCore.QCoreApplication.processEvents() + + + def stop_call(self, friend_number, by_friend) -> None: """ Stop call with friend """ + LOG.debug(f"CM.stop_call friend={friend_number}") if friend_number in self._incoming_calls: self._incoming_calls.remove(friend_number) is_declined = True else: is_declined = False - self._screen.call_finished() - is_video = self._call.is_video_call(friend_number) - self._call.finish_call(friend_number, by_friend) # finish or decline call if friend_number in self._call_widgets: - self._call_widgets[friend_number].close() - del self._call_widgets[friend_number] + LOG.debug(f"CM.stop_call _call_widgets close") + self.close_call(friend_number) - def destroy_window(): - if is_video: + LOG.debug(f"CM.stop_call _main_screen.call_finished") + self._main_screen.call_finished() + self._callav.finish_call(friend_number, by_friend) # finish or decline call + is_video = self._callav.is_video_call(friend_number) + if is_video: + def destroy_window(): + #??? FixMe + with ts.ignoreStdout(): import cv2 cv2.destroyWindow(str(friend_number)) + LOG.debug(f"CM.stop_call destroy_window") + threading.Timer(2.0, destroy_window).start() - threading.Timer(2.0, destroy_window).start() + LOG.debug(f"CM.stop_call _call_finished_event") self._call_finished_event(friend_number, is_declined) - def friend_exit(self, friend_number): - if friend_number in self._call: - self._call.finish_call(friend_number, True) + def friend_exit(self, friend_number:int) -> None: + if friend_number in self._callav: + self._callav.finish_call(friend_number, True) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _get_incoming_call_widget(self, friend_number, text, friend_name): return av_widgets.IncomingCallWidget(self._settings, self, friend_number, text, friend_name) diff --git a/toxygen/av/screen_sharing.py b/toxygen/av/screen_sharing.py index 265658c..e0f783b 100644 --- a/toxygen/av/screen_sharing.py +++ b/toxygen/av/screen_sharing.py @@ -1,6 +1,6 @@ -import numpy as np -from PyQt5 import QtWidgets +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtWidgets class DesktopGrabber: @@ -13,10 +13,11 @@ class DesktopGrabber: self._height -= height % 4 self._screen = QtWidgets.QApplication.primaryScreen() - def read(self): + def read(self) -> tuple: pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height) image = pixmap.toImage() s = image.bits().asstring(self._width * self._height * 4) + import numpy as np arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4)) return True, arr diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py index fad68c4..6d64783 100644 --- a/toxygen/bootstrap/bootstrap.py +++ b/toxygen/bootstrap/bootstrap.py @@ -1,83 +1,48 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import random -import urllib.request +import logging + +from qtpy import QtCore +try: + import certifi + from io import BytesIO +except ImportError: + certifi = None + +from user_data.settings import get_user_config_path from utils.util import * -from 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 -DEFAULT_NODES_COUNT = 4 +global LOG +LOG = logging.getLogger('app.'+'bootstrap') - -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' +def download_nodes_list(settings, oArgs) -> str: if not settings['download_nodes_list']: - return + 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 - 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): +def _save_nodes(nodes, app) -> None: if not nodes: return - print('Saving nodes...') - with open(_get_nodes_path(), 'wb') as fl: + with open(_get_nodes_path(app._args), 'wb') as fl: + LOG.info("Saving nodes to " +_get_nodes_path(app._args)) fl.write(nodes) diff --git a/toxygen/common/event.py b/toxygen/common/event.py index 687a34d..f51a51f 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 d16edb4..687fd9a 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 09c159b..45563b2 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 2058890..b4b33f1 100644 --- a/toxygen/contacts/basecontact.py +++ b/toxygen/contacts/basecontact.py @@ -1,6 +1,7 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- from user_data.settings import * -from PyQt5 import QtCore, QtGui -from wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE +from qtpy import QtCore, QtGui +from toxygen_wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE import utils.util as util import common.event as event import contacts.common as common @@ -14,26 +15,27 @@ class BaseContact: Base class for all contacts. """ - def __init__(self, profile_manager, name, status_message, widget, tox_id): + def __init__(self, profile_manager, name, status_message, widget, tox_id, kind=''): """ :param name: name, example: 'Toxygen user' :param status_message: status message, example: 'Toxing on Toxygen' :param widget: ContactItem instance :param tox_id: tox id of contact + :param kind: one of ['bot', 'friend', 'group', 'invite', 'grouppeer', ''] """ self._profile_manager = profile_manager self._name, self._status_message = name, status_message + self._kind = kind self._status, self._widget = None, widget self._tox_id = tox_id + self._name_changed_event = event.Event() self._status_message_changed_event = event.Event() self._status_changed_event = event.Event() self._avatar_changed_event = event.Event() self.init_widget() - # ----------------------------------------------------------------------------------------------------------------- # Name - current name or alias of user - # ----------------------------------------------------------------------------------------------------------------- def get_name(self): return self._name @@ -53,9 +55,7 @@ class BaseContact: name_changed_event = property(get_name_changed_event) - # ----------------------------------------------------------------------------------------------------------------- # Status message - # ----------------------------------------------------------------------------------------------------------------- def get_status_message(self): return self._status_message @@ -75,9 +75,7 @@ class BaseContact: status_message_changed_event = property(get_status_message_changed_event) - # ----------------------------------------------------------------------------------------------------------------- # Status - # ----------------------------------------------------------------------------------------------------------------- def get_status(self): return self._status @@ -96,30 +94,29 @@ 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 """ - 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) + try: + avatar_path = self.get_avatar_path() + width = self._widget.avatar_label.width() + pixmap = QtGui.QPixmap(avatar_path) + self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation)) + self._widget.avatar_label.repaint() + self._avatar_changed_event(avatar_path) + except Exception as e: + pass def reset_avatar(self, generate_new): avatar_path = self.get_avatar_path() @@ -161,19 +158,23 @@ 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 27750a2..bd46c32 100644 --- a/toxygen/contacts/common.py +++ b/toxygen/contacts/common.py @@ -1,10 +1,10 @@ -from pydenticon import Generator +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import hashlib +from pydenticon import Generator -# ----------------------------------------------------------------------------------------------------------------- # Typing notifications -# ----------------------------------------------------------------------------------------------------------------- class BaseTypingNotificationHandler: @@ -19,7 +19,7 @@ class BaseTypingNotificationHandler: class FriendTypingNotificationHandler(BaseTypingNotificationHandler): - def __init__(self, friend_number): + def __init__(self, friend_number:int): super().__init__() self._friend_number = friend_number @@ -30,9 +30,7 @@ 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 e88acf2..70b9318 100644 --- a/toxygen/contacts/contact.py +++ b/toxygen/contacts/contact.py @@ -1,10 +1,17 @@ -from history.database import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from history.database import TIMEOUT, \ + SAVE_MESSAGES, MESSAGE_AUTHOR + from contacts import basecontact, common from messenger.messages import * from contacts.contact_menu import * from file_transfers import file_transfers as ft import re +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) class Contact(basecontact.BaseContact): """ @@ -35,9 +42,7 @@ class Contact(basecontact.BaseContact): if hasattr(self, '_message_getter'): del self._message_getter - # ----------------------------------------------------------------------------------------------------------------- # History support - # ----------------------------------------------------------------------------------------------------------------- def load_corr(self, first_time=True): """ @@ -114,9 +119,7 @@ class Contact(basecontact.BaseContact): return TextMessage(message, author, unix_time, message_type, unique_id) - # ----------------------------------------------------------------------------------------------------------------- # Unsent messages - # ----------------------------------------------------------------------------------------------------------------- def get_unsent_messages(self): """ @@ -129,8 +132,11 @@ class Contact(basecontact.BaseContact): """ :return list of unsent messages for saving """ - messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) - and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr) +# and m.tox_message_id == tox_message_id, + messages = filter(lambda m: m.author is not None + and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], + self._corr) + # was message = list(...)[0] return list(messages) def mark_as_sent(self, tox_message_id): @@ -139,11 +145,10 @@ class Contact(basecontact.BaseContact): and m.tox_message_id == tox_message_id, self._corr))[0] message.mark_as_sent() except Exception as ex: - util.log('Mark as sent ex: ' + str(ex)) + # wrapped C/C++ object of type QLabel has been deleted + LOG.error(f"Mark as sent: {ex}") - # ----------------------------------------------------------------------------------------------------------------- # Message deletion - # ----------------------------------------------------------------------------------------------------------------- def delete_message(self, message_id): elem = list(filter(lambda m: m.message_id == message_id, self._corr))[0] @@ -189,9 +194,7 @@ 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 @@ -224,9 +227,7 @@ 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 @@ -236,9 +237,7 @@ class Contact(basecontact.BaseContact): curr_text = property(get_curr_text, set_curr_text) - # ----------------------------------------------------------------------------------------------------------------- # Alias support - # ----------------------------------------------------------------------------------------------------------------- def set_name(self, value): """ @@ -254,9 +253,7 @@ class Contact(basecontact.BaseContact): def has_alias(self): return self._alias - # ----------------------------------------------------------------------------------------------------------------- # Visibility in friends' list - # ----------------------------------------------------------------------------------------------------------------- def get_visibility(self): return self._visible @@ -266,9 +263,7 @@ 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 @@ -296,9 +291,7 @@ 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 @@ -308,25 +301,19 @@ 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 8178d31..6f45ca6 100644 --- a/toxygen/contacts/contact_menu.py +++ b/toxygen/contacts/contact_menu.py @@ -1,10 +1,14 @@ -from PyQt5 import QtWidgets +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtWidgets + import utils.ui as util_ui +from toxygen_wrapper.toxcore_enums_and_consts import * +global LOG +import logging +LOG = logging.getLogger('app') -# ----------------------------------------------------------------------------------------------------------------- # Builder -# ----------------------------------------------------------------------------------------------------------------- def _create_menu(menu_name, parent): menu_name = menu_name or '' @@ -77,9 +81,7 @@ class ContactMenuBuilder: self._actions[self._index] = (text, handler) self._index += 1 -# ----------------------------------------------------------------------------------------------------------------- # Generators -# ----------------------------------------------------------------------------------------------------------------- class BaseContactMenuGenerator: @@ -90,17 +92,15 @@ 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,9 +144,7 @@ class FriendMenuGenerator(BaseContactMenuGenerator): return menu - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _generate_plugins_menu_builder(plugin_loader, number): @@ -165,11 +163,13 @@ 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 + #? return None + pass 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 76e8e79..0c5a61d 100644 --- a/toxygen/contacts/contact_provider.py +++ b/toxygen/contacts/contact_provider.py @@ -1,22 +1,32 @@ +# -*- 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): + def __init__(self, tox, friend_factory, group_factory, group_peer_factory, app=None): super().__init__(tox) self._friend_factory = friend_factory self._group_factory = group_factory self._group_peer_factory = group_peer_factory self._cache = {} # key - contact's public key, value - contact instance + self._app = app - # ----------------------------------------------------------------------------------------------------------------- # Friends - # ----------------------------------------------------------------------------------------------------------------- - - def get_friend_by_number(self, friend_number): - 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): @@ -24,67 +34,118 @@ class ContactProvider(tox_save.ToxSave): if friend is not None: return friend friend = self._friend_factory.create_friend_by_public_key(public_key) - self._add_to_cache(public_key, friend) - + if friend is None: + LOG_WARN(f"CP.get_friend_by_public_key NULL {friend} ") + else: + self._add_to_cache(public_key, friend) + LOG_DEBUG(f"CP.get_friend_by_public_key ADDED {friend} ") return friend - def get_all_friends(self): - friend_numbers = self._tox.self_get_friend_list() + def get_all_friends(self) -> list: + if self._app and self._app.bAppExiting: + return [] + try: + friend_numbers = self._tox.self_get_friend_list() + except Exception as e: + LOG_WARN(f"CP.get_all_friends EXCEPTION {e} ") + return [] friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) - return list(friends) - # ----------------------------------------------------------------------------------------------------------------- # Groups - # ----------------------------------------------------------------------------------------------------------------- def get_all_groups(self): - group_numbers = range(self._tox.group_get_number_groups()) - groups = map(lambda n: self.get_group_by_number(n), group_numbers) - - return list(groups) + """from callbacks""" + try: + len_groups = self._tox.group_get_number_groups() + group_numbers = range(len_groups) + except Exception as e: + return None + groups = list(map(lambda n: self.get_group_by_number(n), group_numbers)) + # failsafe in case there are bogus None groups? + fgroups = list(filter(lambda x: x, groups)) + if len(fgroups) != len_groups: + LOG_WARN(f"CP.are there are bogus None groups in libtoxcore? {len(fgroups)} != {len_groups}") + for group_num in group_numbers: + group = self.get_group_by_number(group_num) + if group is None: + LOG_ERROR(f"There are bogus None groups in libtoxcore {group_num}!") + # fixme: do something + groups = fgroups + return groups def get_group_by_number(self, group_number): - public_key = self._tox.group_get_chat_id(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 - return self.get_group_by_public_key(public_key) + def get_group_by_chat_id(self, chat_id): + group = self._get_contact_from_cache(chat_id) + if group is not None: + return group + group = self._group_factory.create_group_by_chat_id(chat_id) + if group is None: + LOG_ERROR(f"get_group_by_chat_id NULL chat_id={chat_id}") + else: + self._add_to_cache(chat_id, group) + + return group def get_group_by_public_key(self, public_key): group = self._get_contact_from_cache(public_key) if group is not None: return group group = self._group_factory.create_group_by_public_key(public_key) - self._add_to_cache(public_key, group) + if group is None: + LOG_WARN(f"get_group_by_public_key NULL group public_key={public_key}") + else: + self._add_to_cache(public_key, group) return group - # ----------------------------------------------------------------------------------------------------------------- # Group peers - # ----------------------------------------------------------------------------------------------------------------- def get_all_group_peers(self): - return list() + return [] def get_group_peer_by_id(self, group, peer_id): peer = group.get_peer_by_id(peer_id) - - return self._get_group_peer(group, peer) + if peer is not None: + return self._get_group_peer(group, peer) + LOG_WARN(f"get_group_peer_by_id peer_id={peer_id}") + return None def get_group_peer_by_public_key(self, group, public_key): peer = group.get_peer_by_public_key(public_key) + if peer is not None: + return self._get_group_peer(group, peer) + LOG_WARN(f"get_group_peer_by_public_key public_key={public_key}") + return None - 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() @@ -93,9 +154,7 @@ 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 87a61ff..6f0dae8 100644 --- a/toxygen/contacts/contacts_manager.py +++ b/toxygen/contacts/contacts_manager.py @@ -1,9 +1,36 @@ +# -*- 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): """ @@ -15,12 +42,14 @@ 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 = '' @@ -28,6 +57,11 @@ 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 @@ -36,36 +70,46 @@ 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): + def save_profile(self) -> None: data = self._tox.get_savedata() self._profile_manager.save_profile(data) - def is_friend_active(self, friend_number): + def is_friend_active(self, friend_number:int) -> bool: if not self.is_active_a_friend(): return False return self.get_curr_contact().number == friend_number - def is_group_active(self, group_number): + def is_group_active(self, group_number) -> bool: if self.is_active_a_friend(): return False return self.get_curr_contact().number == group_number - def is_contact_active(self, contact): + def is_contact_active(self, contact) -> bool: + if self._active_contact == -1: +# LOG.debug("No self._active_contact") + return False + if self._active_contact >= len(self._contacts): + LOG.warn(f"ERROR _active_contact={self._active_contact} >= contacts len={len(self._contacts)}") + return False + if not self._contacts[self._active_contact]: + LOG.warn(f"ERROR NULL {self._contacts[self._active_contact]} {contact.tox_id}") + return False + + if not hasattr(contact, 'tox_id'): + LOG.warn(f"ERROR is_contact_active no contact.tox_id {type(contact)} contact={contact}") + return False + return self._contacts[self._active_contact].tox_id == contact.tox_id - # ----------------------------------------------------------------------------------------------------------------- # Reconnection support - # ----------------------------------------------------------------------------------------------------------------- - def reset_contacts_statuses(self): + def reset_contacts_statuses(self) -> None: for contact in self._contacts: contact.status = None - # ----------------------------------------------------------------------------------------------------------------- # Work with active friend - # ----------------------------------------------------------------------------------------------------------------- def get_active(self): return self._active_contact @@ -95,11 +139,16 @@ class ContactsManager(ToxSave): current_contact.remove_messages_widgets() # TODO: if required self._unsubscribe_from_events(current_contact) - if self._active_contact + 1 and self._active_contact != value: + if self._active_contact >= 0 and self._active_contact != value: try: current_contact.curr_text = self._screen.messageEdit.toPlainText() except: pass + + # IndexError: list index out of range + if value >= len(self._contacts): + LOG.warn("CM.set_active value too big: {{self._contacts}}") + return contact = self._contacts[value] self._subscribe_to_events(contact) contact.remove_invalid_unsent_files() @@ -128,10 +177,9 @@ class ContactsManager(ToxSave): # self._screen.call_finished() self._set_current_contact_data(contact) self._active_contact_changed(contact) - except Exception as ex: # no friend found. ignore - util.log('Friend value: ' + str(value)) - util.log('Error in set active: ' + str(ex)) - raise + except Exception as e: # no friend found. ignore + LOG.warn(f"CM.set_active EXCEPTION value:{value} len={len(self._contacts)} {e}") + # gulp raise active_contact = property(get_active, set_active) @@ -153,21 +201,23 @@ 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 + 4 - online and by name, 5 - online first and by name, 6 kind :param filter_str: show contacts which name contains this substring """ filter_str = filter_str.lower() current_contact = self.get_curr_contact() - if sorting > 5 or sorting < 0: + for index, contact in enumerate(self._contacts): + if not contact._kind: + set_contact_kind(contact) + + if sorting > 6 or sorting < 0: sorting = 0 if sorting in (1, 2, 4, 5): # online first @@ -183,18 +233,30 @@ 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): @@ -222,9 +284,7 @@ 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] @@ -235,9 +295,15 @@ 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): - self.add_group_peer(group, peer) - + contact = self.add_group_peer(group, peer) + # dunno + return contact + # me - later wrong kind of object? return self.get_contact_by_tox_id(peer.public_key) def check_if_contact_exists(self, tox_id): @@ -255,9 +321,7 @@ 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): """ @@ -299,7 +363,10 @@ class ContactsManager(ToxSave): """ friend = self._contacts[num] self._cleanup_contact_data(friend) - self._tox.friend_delete(friend.number) + try: + self._tox.friend_delete(friend.number) + except Exception as e: + LOG.warn(f"'There was no friend with the given friend number {e}") self._delete_contact(num) def add_friend(self, tox_id): @@ -314,8 +381,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[:TOX_PUBLIC_KEY_SIZE * 2] - if tox_id == self._tox.self_get_address[:TOX_PUBLIC_KEY_SIZE * 2]: + tox_id = tox_id[:enums.TOX_PUBLIC_KEY_SIZE * 2] + if tox_id == self._tox.self_get_address()[:enums.TOX_PUBLIC_KEY_SIZE * 2]: return if tox_id not in self._settings['blocked']: self._settings['blocked'].append(tox_id) @@ -339,21 +406,27 @@ 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): - group = self._contact_provider.get_group_by_number(group_number) index = len(self._contacts) - self._contacts.append(group) - group.reset_avatar(self._settings['identicons']) - self._save_profile() - self.set_active(index) - self.update_filtration() + group = self._contact_provider.get_group_by_number(group_number) + if group is None: + LOG.warn(f"CM.add_group: NULL group from group_number={group_number}") + elif type(group) == int and group < 0: + LOG.warn(f"CM.add_group: NO group from group={group} group_number={group_number}") + else: + LOG.info(f"CM.add_group: Adding group {group._name}") + self._contacts.append(group) + LOG.info(f"contacts_manager.add_group: saving profile") + self._save_profile() + group.reset_avatar(self._settings['identicons']) + LOG.info(f"contacts_manager.add_group: setting active") + self.set_active(index) + self.update_filtration() def delete_group(self, group_number): group = self.get_group_by_number(group_number) @@ -361,30 +434,32 @@ 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 + return contact + contact._kind = 'grouppeer' self._contacts.append(contact) contact.reset_avatar(self._settings['identicons']) self._save_profile() + return contact def remove_group_peer_by_id(self, group, peer_id): peer = group.get_peer_by_id(peer_id) - if 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 peer: # broken + if not self.check_if_contact_exists(peer.public_key): + return + contact = self.get_contact_by_tox_id(peer.public_key) + self.remove_group_peer(contact) def remove_group_peer(self, group_peer_contact): contact = self.get_contact_by_tox_id(group_peer_contact.tox_id) - self._cleanup_contact_data(contact) - num = self._contacts.index(contact) - self._delete_contact(num) + if contact: + self._cleanup_contact_data(contact) + num = self._contacts.index(contact) + self._delete_contact(num) def get_gc_peer_name(self, name): group = self.get_curr_contact() @@ -402,38 +477,48 @@ class ContactsManager(ToxSave): return suggested_names[0] - # ----------------------------------------------------------------------------------------------------------------- # Friend requests - # ----------------------------------------------------------------------------------------------------------------- - def send_friend_request(self, tox_id, message): + def send_friend_request(self, sToxPkOrId, message): """ Function tries to send request to contact with specified id - :param tox_id: id of new contact or tox dns 4 value + :param sToxPkOrId: id of new contact or tox dns 4 value :param message: additional message :return: True on success else error string """ + retval = '' try: message = message or 'Hello! Add me to your contact list please' - if '@' 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) + if len(sToxPkOrId) == enums.TOX_PUBLIC_KEY_SIZE * 2: # public key + self.add_friend(sToxPkOrId) + title = 'Friend added' + text = 'Friend added without sending friend request' else: - 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 + num = self._tox.friend_add(sToxPkOrId, message.encode('utf-8')) + if num < UINT32_MAX: + tox_pk = sToxPkOrId[:enums.TOX_PUBLIC_KEY_SIZE * 2] + self._add_friend(tox_pk) + self.update_filtration() + title = 'Friend added' + text = 'Friend added by sending friend request' + self.save_profile() + retval = True + else: + title = 'Friend failed' + text = 'Friend failed sending friend request' + retval = text + except Exception as ex: # wrong data - util.log('Friend request failed with ' + str(ex)) - return str(ex) + title = 'Friend add exception' + text = 'Friend request exception with ' + str(ex) + self._log(text) + LOG.exception(text) + LOG.warn(f"DELETE {sToxPkOrId} ?") + retval = str(ex) + title = util_ui.tr(title) + text = util_ui.tr(text) + util_ui.message_box(text, title) + return retval def process_friend_request(self, tox_id, message): """ @@ -451,14 +536,12 @@ class ContactsManager(ToxSave): data = self._tox.get_savedata() self._profile_manager.save_profile(data) except Exception as ex: # something is wrong - util.log('Accept friend request failed! ' + str(ex)) + LOG.error('Accept friend request failed! ' + str(ex)) def can_send_typing_notification(self): return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() - # ----------------------------------------------------------------------------------------------------------------- # Contacts numbers update - # ----------------------------------------------------------------------------------------------------------------- def update_friends_numbers(self): for friend in self._contact_provider.get_all_friends(): @@ -467,9 +550,17 @@ 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() @@ -478,16 +569,22 @@ 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) - for contact in filter(lambda c: not c.has_avatar(), self._contacts): + # filter(lambda c: not c.has_avatar(), self._contacts) + for (i, contact) in enumerate(self._contacts): + if contact is None: + LOG.warn(f"_load_contacts NULL contact {i}") + LOG.info(f"_load_contacts deleting NULL {self._contacts[i]}") + del self._contacts[i] + #? self.save_profile() + continue + if contact.has_avatar(): continue contact.reset_avatar(self._settings['identicons']) self.update_filtration() @@ -497,9 +594,7 @@ 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) @@ -554,7 +649,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: + except Exception as e: 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 5c8eabb..24b04ad 100644 --- a/toxygen/contacts/friend.py +++ b/toxygen/contacts/friend.py @@ -1,9 +1,11 @@ +# -*- 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. @@ -14,9 +16,7 @@ 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: - pass + return -1 def get_unsent_files(self): messages = filter(lambda m: type(m) is UnsentFileMessage, self._corr) @@ -52,23 +52,17 @@ 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 8ebafd6..31d5eec 100644 --- a/toxygen/contacts/friend_factory.py +++ b/toxygen/contacts/friend_factory.py @@ -1,7 +1,8 @@ +# -*- 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): @@ -13,28 +14,26 @@ 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): + def create_friend_by_number(self, friend_number:int): aliases = self._settings['friends_aliases'] - tox_id = self._tox.friend_get_public_key(friend_number) + sToxPk = self._tox.friend_get_public_key(friend_number) + assert sToxPk, sToxPk try: - alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] + alias = list(filter(lambda x: x[0] == sToxPk, aliases))[0][1] except: alias = '' item = self._create_friend_item() - name = alias or self._tox.friend_get_name(friend_number) or tox_id + name = alias or self._tox.friend_get_name(friend_number) or sToxPk status_message = self._tox.friend_get_status_message(friend_number) - message_getter = self._db.messages_getter(tox_id) - friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, tox_id) + message_getter = self._db.messages_getter(sToxPk) + friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, sToxPk) friend.set_alias(alias) return friend - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _create_friend_item(self): """ diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py index 19ebc8e..c060e65 100644 --- a/toxygen/contacts/group_chat.py +++ b/toxygen/contacts/group_chat.py @@ -1,11 +1,17 @@ +# -*- 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 wrapper import toxcore_enums_and_consts as constants +from toxygen_wrapper import toxcore_enums_and_consts as constants from common.tox_save import ToxSave from groups.group_ban import GroupBan +global LOG +import logging +LOG = logging.getLogger(__name__) +from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE class GroupChat(contact.Contact, ToxSave): @@ -25,9 +31,7 @@ class GroupChat(contact.Contact, ToxSave): def get_context_menu_generator(self): return GroupMenuGenerator(self) - # ----------------------------------------------------------------------------------------------------------------- # Properties - # ----------------------------------------------------------------------------------------------------------------- def get_is_private(self): return self._is_private @@ -53,9 +57,7 @@ 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] @@ -73,12 +75,20 @@ 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) + is_current_user, + status_message=status_message) self._peers.append(peer) def remove_peer(self, peer_id): @@ -86,25 +96,40 @@ class GroupChat(contact.Contact, ToxSave): self.remove_all_peers_except_self() else: peer = self.get_peer_by_id(peer_id) - self._peers.remove(peer) + if peer: # broken + self._peers.remove(peer) + else: + LOG_WARN(f"remove_peer empty peers for {peer_id}") def get_peer_by_id(self, peer_id): peers = list(filter(lambda p: p.id == peer_id, self._peers)) - - return peers[0] + if peers: + return peers[0] + else: + LOG_WARN(f"get_peer_by_id empty peers for {peer_id}") + return None def get_peer_by_public_key(self, public_key): peers = list(filter(lambda p: p.public_key == public_key, self._peers)) - - return peers[0] + # DEBUGc: group_moderation #0 mod_id=4294967295 event_type=3 + # WARN_: get_peer_by_id empty peers for 4294967295 + if peers: + return peers[0] + else: + LOG_WARN(f"get_peer_by_public_key empty peers for {public_key}") + return None def remove_all_peers_except_self(self): self._peers = self._peers[:1] def get_peers_names(self): peers_names = map(lambda p: p.name, self._peers) - - return list(peers_names) + if peers_names: # broken + return list(peers_names) + else: + LOG_WARN(f"get_peers_names empty peers") + #? broken + return [] def get_peers(self): return self._peers[:] @@ -112,21 +137,20 @@ class GroupChat(contact.Contact, ToxSave): peers = property(get_peers) def get_bans(self): - ban_ids = self._tox.group_ban_get_list(self._number) - bans = [] - for ban_id in ban_ids: - ban = GroupBan(ban_id, - self._tox.group_ban_get_target(self._number, ban_id), - self._tox.group_ban_get_time_set(self._number, ban_id)) - bans.append(ban) - - return bans - + return [] +# ban_ids = self._tox.group_ban_get_list(self._number) +# bans = [] +# for ban_id in ban_ids: +# ban = GroupBan(ban_id, +# self._tox.group_ban_get_target(self._number, ban_id), +# self._tox.group_ban_get_time_set(self._number, ban_id)) +# bans.append(ban) +# +# return bans +# bans = property(get_bans) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _get_default_avatar_path(): diff --git a/toxygen/contacts/group_factory.py b/toxygen/contacts/group_factory.py index 4083438..4345c4b 100644 --- a/toxygen/contacts/group_factory.py +++ b/toxygen/contacts/group_factory.py @@ -1,7 +1,12 @@ +# -*- 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 wrapper.toxcore_enums_and_consts as constants +import toxygen_wrapper.toxcore_enums_and_consts as constants +global LOG +import logging +LOG = logging.getLogger(__name__) class GroupFactory(ToxSave): @@ -12,12 +17,15 @@ 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: @@ -35,9 +43,7 @@ class GroupFactory(ToxSave): return group - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _create_group_item(self): """ @@ -47,7 +53,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()): + for i in range(self._tox.group_get_number_groups()+100): if self._tox.group_get_chat_id(i) == chat_id: return i return -1 diff --git a/toxygen/contacts/group_peer_contact.py b/toxygen/contacts/group_peer_contact.py index 8854198..3e6131c 100644 --- a/toxygen/contacts/group_peer_contact.py +++ b/toxygen/contacts/group_peer_contact.py @@ -1,11 +1,13 @@ +# -*- 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): - super().__init__(profile_manager, message_getter, peer_number, name, str(), widget, tox_id) + def __init__(self, profile_manager, message_getter, peer_number, name, widget, tox_id, group_pk, status_message=None): + if status_message is None: status_message=str() + super().__init__(profile_manager, message_getter, peer_number, name, status_message, widget, tox_id) self._group_pk = group_pk def get_group_pk(self): diff --git a/toxygen/contacts/group_peer_factory.py b/toxygen/contacts/group_peer_factory.py index 38b3a20..1804b50 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,7 +14,10 @@ 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) + item, + peer.public_key, + group.tox_id, + status_message=peer.status_message) group_peer_contact.status = peer.status return group_peer_contact diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py index 81220af..3afcf2b 100644 --- a/toxygen/contacts/profile.py +++ b/toxygen/contacts/profile.py @@ -1,19 +1,27 @@ +# -*- 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): + def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action, app=None): """ :param tox: tox instance :param screen: ref to main screen """ + assert tox basecontact.BaseContact.__init__(self, profile_manager, tox.self_get_name(), @@ -27,61 +35,73 @@ 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): + def change_status(self) -> None: """ Changes status of user (online, away, busy) """ if self._status is not None: self.set_status((self._status + 1) % 3) - def set_status(self, status): + def set_status(self, status) -> None: super().set_status(status) if status is not None: self._tox.self_set_status(status) elif not self._waiting_for_reconnection: self._waiting_for_reconnection = True - self._timer = threading.Timer(50, self._reconnect) + self._timer = threading.Timer(iRECONNECT, self._reconnect) self._timer.start() - def set_name(self, value): + def set_name(self, value) -> None: if self.name == value: return super().set_name(value) self._tox.self_set_name(self._name) - def set_status_message(self, value): + def set_status_message(self, value) -> None: super().set_status_message(value) self._tox.self_set_status_message(self._status_message) def set_new_nospam(self): """Sets new nospam part of tox id""" - self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32 + self._tox.self_set_nospam(random.randint(0, iUMAXINT)) # no spam - uint32 self._tox_id = self._tox.self_get_address() + self._sToxId = self._tox.self_get_address() + return self._sToxId - return self._tox_id - - # ----------------------------------------------------------------------------------------------------------------- # Reset - # ----------------------------------------------------------------------------------------------------------------- - def restart(self): + def restart(self) -> None: """ Recreate tox instance """ self.status = None invoke_in_main_thread(self._reset_action) - def _reconnect(self): + def _reconnect(self) -> None: self._waiting_for_reconnection = False + if self._app and self._app.bAppExiting: + # dont do anything after the app has been shipped + # there's a segv that results + return contacts = self._contacts_provider.get_all_friends() all_friends_offline = all(list(map(lambda x: x.status is None, contacts))) if self.status is None or (all_friends_offline and len(contacts)): self._waiting_for_reconnection = True self.restart() - self._timer = threading.Timer(50, self._reconnect) + self._timer = threading.Timer(iRECONNECT, self._reconnect) self._timer.start() + +# Current thread 0x00007901a13ccb80 (most recent call first): +# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 826 in self_get_friend_list_size +# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 838 in self_get_friend_list +# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact_provider.py", line 45 in get_all_friends +# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/profile.py", line 90 in _reconnect +# File "/usr/lib/python3.11/threading.py", line 1401 in run +# File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner +# File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap +# + diff --git a/toxygen/file_transfers/file_transfers.py b/toxygen/file_transfers/file_transfers.py index 0f04e5b..5fa87f9 100644 --- a/toxygen/file_transfers/file_transfers.py +++ b/toxygen/file_transfers/file_transfers.py @@ -1,11 +1,15 @@ -from wrapper.toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os +from os import chdir, remove, rename 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, @@ -78,6 +82,7 @@ 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) @@ -112,9 +117,6 @@ 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: @@ -126,9 +128,7 @@ class FileTransfer: def _finished(self): self._finished_event(self._friend_number, self._file_number) -# ----------------------------------------------------------------------------------------------------------------- # Send file -# ----------------------------------------------------------------------------------------------------------------- class SendTransfer(FileTransfer): @@ -174,11 +174,14 @@ class SendAvatar(SendTransfer): """ def __init__(self, path, tox, friend_number): - if path is None: + LOG_DEBUG(f"SendAvatar path={path} friend_number={friend_number}") + if path is None or not os.path.exists(path): avatar_hash = None else: with open(path, 'rb') as fl: - avatar_hash = Tox.hash(fl.read()) + data=fl.read() + LOG_DEBUG(f"SendAvatar data={data} type={type(data)}") + avatar_hash = tox.hash(data, None) super().__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], avatar_hash) @@ -220,12 +223,10 @@ class SendFromFileBuffer(SendTransfer): def send_chunk(self, position, size): super().send_chunk(position, size) if not size: - chdir(dirname(self._path)) - remove(self._path) + os.chdir(dirname(self._path)) + os.remove(self._path) -# ----------------------------------------------------------------------------------------------------------------- # Receive file -# ----------------------------------------------------------------------------------------------------------------- class ReceiveTransfer(FileTransfer): @@ -315,7 +316,6 @@ 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): - hash = self.get_file_id() + ihash = self.get_file_id() with open(path, 'rb') as fl: data = fl.read() existing_hash = Tox.hash(data) - if hash == existing_hash: + if ihash == 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 114383b..a9085c2 100644 --- a/toxygen/file_transfers/file_transfers_handler.py +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -1,11 +1,22 @@ +# -*- 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 @@ -19,16 +30,16 @@ class FileTransfersHandler(ToxSave): # key = (friend number, file number), value - message id profile.avatar_changed_event.add_callback(self._send_avatar_to_contacts) - - def stop(self): + self. lBlockAvatars = [] + + def stop(self) -> None: self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {} self._settings.save() - # ----------------------------------------------------------------------------------------------------------------- # File transfers support - # ----------------------------------------------------------------------------------------------------------------- - def incoming_file_transfer(self, friend_number, file_number, size, file_name): + def incoming_file_transfer(self, friend_number, file_number, size, file_name) -> None: + # main thread """ New transfer :param friend_number: number of friend who sent file @@ -37,11 +48,15 @@ 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 = is_inline(file_name) and self._settings['allow_inline'] + inline = False # ?is_inline(file_name) and self._settings['allow_inline'] file_id = self._tox.file_get_file_id(friend_number, file_number) accepted = True if file_id in self._paused_file_transfers: + LOG_INFO(f'incoming_file_handler paused friend_number={friend_number}') (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[file_id] pos = start_position if os.path.exists(path) else 0 if pos >= size: @@ -52,26 +67,33 @@ 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): + def cancel_transfer(self, friend_number, file_number, already_cancelled=False) -> None: """ Stop transfer :param friend_number: number of friend :param file_number: file number :param already_cancelled: was cancelled by friend """ + # callback if (friend_number, file_number) in self._file_transfers: tr = self._file_transfers[(friend_number, file_number)] if not already_cancelled: @@ -84,17 +106,19 @@ 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): - self._get_friend_by_number(friend_number).delete_one_unsent_file(message_id) + def cancel_not_started_transfer(self, friend_number, message_id) -> None: + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + friend.delete_one_unsent_file(message_id) - def pause_transfer(self, friend_number, file_number, by_friend=False): + def pause_transfer(self, friend_number, file_number, by_friend=False) -> None: """ Pause transfer with specified data """ tr = self._file_transfers[(friend_number, file_number)] tr.pause(by_friend) - def resume_transfer(self, friend_number, file_number, by_friend=False): + def resume_transfer(self, friend_number, file_number, by_friend=False) -> None: """ Resume transfer with specified data """ @@ -104,7 +128,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): + def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0) -> None: """ :param path: path for saving :param friend_number: friend number @@ -115,6 +139,7 @@ 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: @@ -130,7 +155,7 @@ class FileTransfersHandler(ToxSave): if inline: self._insert_inline_before[(friend_number, file_number)] = message.message_id - def send_screenshot(self, data, friend_number): + def send_screenshot(self, data, friend_number) -> None: """ Send screenshot :param data: raw data - png format @@ -138,22 +163,26 @@ class FileTransfersHandler(ToxSave): """ self.send_inline(data, 'toxygen_inline.png', friend_number) - def send_sticker(self, path, friend_number): + def send_sticker(self, path, friend_number) -> None: with open(path, 'rb') as fl: data = fl.read() self.send_inline(data, 'sticker.png', friend_number) - def send_inline(self, data, file_name, friend_number, is_resend=False): + def send_inline(self, data, file_name, friend_number, is_resend=False) -> None: friend = self._get_friend_by_number(friend_number) + if friend is None: + LOG_WARN("fsend_inline Error friend is None file_name: {file_name}") + return if friend.status is None and not is_resend: self._file_transfers_message_service.add_unsent_file_message(friend, file_name, data) return elif friend.status is None and is_resend: - raise RuntimeError() + LOG_WARN("fsend_inline Error friend.status is None file_name: {file_name}") + return st = SendFromBuffer(self._tox, friend.number, data, file_name) self._send_file_add_set_handlers(st, friend, file_name, True) - def send_file(self, path, friend_number, is_resend=False, file_id=None): + def send_file(self, path, friend_number, is_resend=False, file_id=None) -> None: """ Send file to current active friend :param path: file path @@ -162,47 +191,52 @@ 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: - print('Error in sending') + LOG_WARN('Error in sending') return st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id) file_name = os.path.basename(path) self._send_file_add_set_handlers(st, friend, file_name) - def incoming_chunk(self, friend_number, file_number, position, data): + def incoming_chunk(self, friend_number, file_number, position, data) -> None: """ Incoming chunk """ self._file_transfers[(friend_number, file_number)].write_chunk(position, data) - def outgoing_chunk(self, friend_number, file_number, position, size): + def outgoing_chunk(self, friend_number, file_number, position, size) -> None: """ Outgoing chunk """ self._file_transfers[(friend_number, file_number)].send_chunk(position, size) - def transfer_finished(self, friend_number, file_number): + def transfer_finished(self, friend_number, file_number) -> None: transfer = self._file_transfers[(friend_number, file_number)] + friend = self._get_friend_by_number(friend_number) + if friend is None: return None t = type(transfer) if t is ReceiveAvatar: - self._get_friend_by_number(friend_number).load_avatar() + friend.load_avatar() elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image - print('inline') + LOG.debug('inline') inline = InlineImageMessage(transfer.data) message_id = self._insert_inline_before[(friend_number, file_number)] del self._insert_inline_before[(friend_number, file_number)] - index = self._get_friend_by_number(friend_number).insert_inline(message_id, inline) + if friend is None: return None + index = friend.insert_inline(message_id, inline) self._file_transfers_message_service.add_inline_message(transfer, index) del self._file_transfers[(friend_number, file_number)] - def send_files(self, friend_number): - friend = self._get_friend_by_number(friend_number) - friend.remove_invalid_unsent_files() - files = friend.get_unsent_files() + def send_files(self, friend_number:int) -> None: try: + friend = self._get_friend_by_number(friend_number) + if friend is None: return + friend.remove_invalid_unsent_files() + files = friend.get_unsent_files() for fl in files: data, path = fl.data, fl.path if data is not None: @@ -211,6 +245,7 @@ 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] @@ -218,32 +253,63 @@ class FileTransfersHandler(ToxSave): self.send_file(path, friend_number, True, key) del self._paused_file_transfers[key] except Exception as ex: - print('Exception in file sending: ' + str(ex)) + LOG_ERROR('send_files EXCEPTION in file sending: ' + str(ex)) - def friend_exit(self, friend_number): - for friend_num, file_num in self._file_transfers.keys(): + def friend_exit(self, friend_number:int) -> None: + # RuntimeError: dictionary changed size during iteration + lMayChangeDynamically = self._file_transfers.copy() + for friend_num, file_num in lMayChangeDynamically: if friend_num != friend_number: continue + if (friend_num, file_num) not in self._file_transfers: + continue ft = self._file_transfers[(friend_num, file_num)] if type(ft) is SendTransfer: - self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, False, -1] + try: + file_id = ft.file_id + except Exception as e: + LOG_WARN("friend_exit SendTransfer Error getting file_id: {e}") + # drop through + else: + self._paused_file_transfers[file_id] = [ft.path, friend_num, False, -1] elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, True, ft.total_size()] + try: + file_id = ft.file_id + except Exception as e: + LOG_WARN("friend_exit ReceiveTransfer Error getting file_id: {e}") + # drop through + else: + self._paused_file_transfers[file_id] = [ft.path, friend_num, True, ft.total_size()] self.cancel_transfer(friend_num, file_num, True) - # ----------------------------------------------------------------------------------------------------------------- # Avatars support - # ----------------------------------------------------------------------------------------------------------------- - def send_avatar(self, friend_number, avatar_path=None): + def send_avatar(self, friend_number, avatar_path=None) -> None: """ :param friend_number: number of friend who should get new avatar :param avatar_path: path to avatar or None if reset """ - sa = SendAvatar(avatar_path, self._tox, friend_number) - self._file_transfers[(friend_number, sa.file_number)] = sa + return + if (avatar_path, friend_number,) in self.lBlockAvatars: + return + if friend_number is None: + LOG_WARN(f"send_avatar friend_number NULL {friend_number}") + return + if avatar_path and type(avatar_path) != str: + LOG_WARN(f"send_avatar avatar_path type {type(avatar_path)}") + return + LOG_INFO(f"send_avatar avatar_path={avatar_path} friend_number={friend_number}") + try: + # self NOT missing - who's self? + sa = SendAvatar(avatar_path, self._tox, friend_number) + LOG_INFO(f"send_avatar avatar_path={avatar_path} sa={sa}") + self._file_transfers[(friend_number, sa.file_number)] = sa + except Exception as e: + # ArgumentError('This client is currently not connected to the friend.') + LOG_WARN(f"send_avatar EXCEPTION {e}") + self.lBlockAvatars.append( (avatar_path, friend_number,) ) - def incoming_avatar(self, friend_number, file_number, size): + def incoming_avatar(self, friend_number, file_number, size) -> None: """ Friend changed avatar :param friend_number: friend number @@ -251,6 +317,7 @@ 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 @@ -258,21 +325,21 @@ class FileTransfersHandler(ToxSave): elif not size: friend.reset_avatar(self._settings['identicons']) - def _send_avatar_to_contacts(self, _): + def _send_avatar_to_contacts(self, _) -> None: + # from a callback friends = self._get_all_friends() for friend in filter(self._is_friend_online, friends): self.send_avatar(friend.number) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- - def _is_friend_online(self, friend_number): + def _is_friend_online(self, friend_number:int) -> bool: friend = self._get_friend_by_number(friend_number) + if friend is None: return None return friend.status is not None - def _get_friend_by_number(self, friend_number): + def _get_friend_by_number(self, friend_number:int): 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 4509183..1b292ee 100644 --- a/toxygen/file_transfers/file_transfers_messages_service.py +++ b/toxygen/file_transfers/file_transfers_messages_service.py @@ -1,7 +1,15 @@ +# -*- 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: @@ -12,7 +20,9 @@ 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) @@ -27,6 +37,7 @@ 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) @@ -39,14 +50,21 @@ class FileTransfersMessagesService: return tm - def add_inline_message(self, transfer, index): + def add_inline_message(self, transfer, index) -> None: + """callback""" if not self._is_friend_active(transfer.friend_number): return + if transfer is None or not hasattr(transfer, 'data') or \ + not transfer.data: + LOG_ERROR(f"add_inline_message empty data") + return count = self._messages.count() if count + index + 1 >= 0: - self._create_inline_item(transfer.data, count + index + 1) + # assumes .data + self._create_inline_item(transfer, count + index + 1) def add_unsent_file_message(self, friend, file_path, data): + assert friend author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME']) size = os.path.getsize(file_path) if data is None else len(data) tm = UnsentFileMessage(file_path, data, util.get_unix_time(), author, size, friend.number) @@ -58,11 +76,9 @@ class FileTransfersMessagesService: return tm - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- - def _is_friend_active(self, friend_number): + def _is_friend_active(self, friend_number:int) -> bool: 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 89ecc7e..2b17a25 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 a2eed47..2332933 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 4eaf255..a96c751 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): + def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False, status_message=None): self._peer_id = peer_id self._name = name self._status = status + self._status_message = status_message self._role = role self._public_key = public_key self._is_current_user = is_current_user self._is_muted = is_muted + self._kind = 'grouppeer' - # ----------------------------------------------------------------------------------------------------------------- # Readonly properties - # ----------------------------------------------------------------------------------------------------------------- def get_id(self): return self._peer_id @@ -33,9 +33,12 @@ 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 b8fc7cc..0e52d2a 100644 --- a/toxygen/groups/groups_service.py +++ b/toxygen/groups/groups_service.py @@ -1,9 +1,16 @@ +# -*- 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 wrapper.toxcore_enums_and_consts as constants +import toxygen_wrapper.toxcore_enums_and_consts as constants +from toxygen_wrapper.toxcore_enums_and_consts import * +from toxygen_wrapper.tox import UINT32_MAX +global LOG +LOG = logging.getLogger('app.'+'gs') class GroupsService(tox_save.ToxSave): @@ -16,18 +23,22 @@ 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): + def set_tox(self, tox) -> None: super().set_tox(tox) for group in self._get_all_groups(): group.set_tox(tox) - # ----------------------------------------------------------------------------------------------------------------- # Groups creation - # ----------------------------------------------------------------------------------------------------------------- - def create_new_gc(self, name, privacy_state, nick, status): - group_number = self._tox.group_new(privacy_state, name, nick, status) + def create_new_gc(self, name, privacy_state, nick, status) -> None: + try: + group_number = self._tox.group_new(privacy_state, name, nick, status) + except Exception as e: + LOG.error(f"create_new_gc {e}") + return if group_number == -1: return @@ -36,51 +47,82 @@ 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): - group_number = self._tox.group_join(chat_id, password, nick, status) + def join_gc_by_id(self, chat_id, password, nick, status) -> None: + try: + group_number = self._tox.group_join(chat_id, password, nick, status) + assert type(group_number) == int, group_number + assert group_number < UINT32_MAX, group_number + except Exception as e: + # gui + title = f"join_gc_by_id {chat_id}" + util_ui.message_box(title +'\n' +str(e), title) + LOG.error(f"_join_gc_via_id {e}") + return + LOG.debug(f"_join_gc_via_id {group_number}") self._add_new_group_by_number(group_number) + group = self._get_group_by_number(group_number) + try: + assert group and hasattr(group, 'status') + except Exception as e: + # gui + title = f"join_gc_by_id {chat_id}" + util_ui.message_box(title +'\n' +str(e), title) + LOG.error(f"_join_gc_via_id {e}") + return + group.status = constants.TOX_USER_STATUS['NONE'] + self._contacts_manager.update_filtration() - # ----------------------------------------------------------------------------------------------------------------- # Groups reconnect and leaving - # ----------------------------------------------------------------------------------------------------------------- - def leave_group(self, group_number): - self._tox.group_leave(group_number) - self._contacts_manager.delete_group(group_number) + def leave_group(self, group_number) -> None: + if type(group_number) == int: + self._tox.group_leave(group_number) + self._contacts_manager.delete_group(group_number) - def disconnect_from_group(self, group_number): + def disconnect_from_group(self, group_number) -> None: self._tox.group_disconnect(group_number) group = self._get_group_by_number(group_number) group.status = None self._clear_peers_list(group) - def reconnect_to_group(self, group_number): + def reconnect_to_group(self, group_number) -> None: self._tox.group_reconnect(group_number) group = self._get_group_by_number(group_number) group.status = constants.TOX_USER_STATUS['NONE'] self._clear_peers_list(group) - # ----------------------------------------------------------------------------------------------------------------- # Group invites - # ----------------------------------------------------------------------------------------------------------------- - def invite_friend(self, friend_number, group_number): - self._tox.group_invite_friend(group_number, friend_number) + 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 process_group_invite(self, friend_number, group_name, invite_data): + try: + self._tox.group_invite_friend(group_number, friend_number) + except Exception as e: + title = f"Error in group_invite_friend {group_number} {friend_number}" + util_ui.message_box(title +'\n' +str(e), title) + + def process_group_invite(self, friend_number, group_name, invite_data) -> None: friend = self._get_friend_by_number(friend_number) + # binary {invite_data} + LOG.debug(f"process_group_invite {friend_number} {group_name}") invite = GroupInvite(friend.tox_id, group_name, invite_data) self._group_invites.append(invite) self._update_invites_button_state() - def accept_group_invite(self, invite, name, status, password): + def accept_group_invite(self, invite, name, status, password) -> None: pk = invite.friend_public_key friend = self._get_friend_by_public_key(pk) + LOG.debug(f"accept_group_invite {name}") self._join_gc_via_invite(invite.invite_data, friend.number, name, status, password) self._delete_group_invite(invite) self._update_invites_button_state() - def decline_group_invite(self, invite): + def decline_group_invite(self, invite) -> None: self._delete_group_invite(invite) self._main_screen.update_gc_invites_button_state() @@ -94,15 +136,13 @@ 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): + def set_group_topic(self, group) -> None: if not group.is_self_moderator_or_founder(): return text = util_ui.tr('New topic for group "{}":'.format(group.name)) @@ -113,46 +153,44 @@ class GroupsService(tox_save.ToxSave): self._tox.group_set_topic(group.number, topic) group.status_message = topic - def show_group_management_screen(self, group): + def show_group_management_screen(self, group) -> None: widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_group_management_screen(group) self._screen.show() - def show_group_settings_screen(self, group): + def show_group_settings_screen(self, group) -> None: widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_group_settings_screen(group) self._screen.show() - def set_group_password(self, group, password): + def set_group_password(self, group, password) -> None: if group.password == password: return self._tox.group_founder_set_password(group.number, password) group.password = password - def set_group_peers_limit(self, group, peers_limit): + def set_group_peers_limit(self, group, peers_limit) -> None: if group.peers_limit == peers_limit: return self._tox.group_founder_set_peer_limit(group.number, peers_limit) group.peers_limit = peers_limit - def set_group_privacy_state(self, group, privacy_state): + def set_group_privacy_state(self, group, privacy_state) -> None: is_private = privacy_state == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE'] if group.is_private == is_private: return self._tox.group_founder_set_privacy_state(group.number, privacy_state) group.is_private = is_private - # ----------------------------------------------------------------------------------------------------------------- # Peers list - # ----------------------------------------------------------------------------------------------------------------- - def generate_peers_list(self): + def generate_peers_list(self) -> None: if not self._contacts_manager.is_active_a_group(): return group = self._contacts_manager.get_curr_contact() PeersListGenerator().generate(group.peers, self, self._peers_list_widget, group.tox_id) - def peer_selected(self, chat_id, peer_id): + def peer_selected(self, chat_id, peer_id) -> None: widgets_factory = self._get_widgets_factory() group = self._get_group_by_public_key(chat_id) self_peer = group.get_self_peer() @@ -162,20 +200,18 @@ 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): + def set_new_peer_role(self, group, peer, role) -> None: self._tox.group_mod_set_role(group.number, peer.id, role) peer.role = role self.generate_peers_list() - def toggle_ignore_peer(self, group, peer, ignore): + def toggle_ignore_peer(self, group, peer, ignore) -> None: self._tox.group_toggle_ignore(group.number, peer.id, ignore) peer.is_muted = ignore - def set_self_info(self, group, name, status): + def set_self_info(self, group, name, status) -> None: self._tox.group_self_set_name(group.number, name) self._tox.group_self_set_status(group.number, status) self_peer = group.get_self_peer() @@ -183,29 +219,27 @@ class GroupsService(tox_save.ToxSave): self_peer.status = status self.generate_peers_list() - # ----------------------------------------------------------------------------------------------------------------- # Bans support - # ----------------------------------------------------------------------------------------------------------------- - def show_bans_list(self, group): + def show_bans_list(self, group) -> None: + return widgets_factory = self._get_widgets_factory() self._screen = widgets_factory.create_groups_bans_screen(group) self._screen.show() - def ban_peer(self, group, peer_id, ban_type): + def ban_peer(self, group, peer_id, ban_type) -> None: self._tox.group_mod_ban_peer(group.number, peer_id, ban_type) - def kick_peer(self, group, peer_id): + def kick_peer(self, group, peer_id) -> None: self._tox.group_mod_remove_peer(group.number, peer_id) - def cancel_ban(self, group_number, ban_id): + def cancel_ban(self, group_number, ban_id) -> None: self._tox.group_mod_remove_ban(group_number, ban_id) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- - def _add_new_group_by_number(self, group_number): + def _add_new_group_by_number(self, group_number) -> None: + LOG.debug(f"_add_new_group_by_number group_number={group_number}") self._contacts_manager.add_group(group_number) def _get_group_by_number(self, group_number): @@ -217,26 +251,41 @@ 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): + def _get_friend_by_number(self, friend_number:int): return self._contacts_provider.get_friend_by_number(friend_number) def _get_friend_by_public_key(self, public_key): return self._contacts_provider.get_friend_by_public_key(public_key) - def _clear_peers_list(self, group): + def _clear_peers_list(self, group) -> None: group.remove_all_peers_except_self() self.generate_peers_list() - def _delete_group_invite(self, invite): + def _delete_group_invite(self, invite) -> None: if invite in self._group_invites: self._group_invites.remove(invite) - def _join_gc_via_invite(self, invite_data, friend_number, nick, status, password): - group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, status, password) - self._add_new_group_by_number(group_number) + # status should be dropped + def _join_gc_via_invite(self, invite_data, friend_number, nick, status='', password='') -> None: + LOG.debug(f"_join_gc_via_invite friend_number={friend_number} nick={nick} datalen={len(invite_data)}") + if nick is None: + nick = '' + if invite_data is None: + invite_data = b'' + try: + # status should be dropped + group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, password=password) + except Exception as e: + LOG.error(f"_join_gc_via_invite ERROR {e}") + return + try: + self._add_new_group_by_number(group_number) + except Exception as e: + LOG.error(f"_join_gc_via_invite group_number={group_number} {e}") + return - def _update_invites_button_state(self): + def _update_invites_button_state(self) -> None: self._main_screen.update_gc_invites_button_state() - def _get_widgets_factory(self): + def _get_widgets_factory(self) -> None: return self._widgets_factory_provider.get_item() diff --git a/toxygen/groups/peers_list.py b/toxygen/groups/peers_list.py index 17495f5..97641d9 100644 --- a/toxygen/groups/peers_list.py +++ b/toxygen/groups/peers_list.py @@ -1,11 +1,10 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + from ui.group_peers_list import PeerItem, PeerTypeItem -from wrapper.toxcore_enums_and_consts import * +from toxygen_wrapper.toxcore_enums_and_consts import * from ui.widgets import * - -# ----------------------------------------------------------------------------------------------------------------- # Builder -# ----------------------------------------------------------------------------------------------------------------- class PeerListBuilder: @@ -63,9 +62,7 @@ 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 751c74b..7d8dd35 100644 --- a/toxygen/history/database.py +++ b/toxygen/history/database.py @@ -1,19 +1,20 @@ +# -*- 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, @@ -24,23 +25,33 @@ CONTACT_TYPE = { class Database: def __init__(self, path, toxes): - self._path, self._toxes = path, toxes + self._path = path + self._toxes = 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(): @@ -58,6 +69,7 @@ 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() @@ -72,11 +84,14 @@ class Database: ' message_type INTEGER' ')') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG.error("dd_friend_to_db " +self._name +f" Database exception! {e}") db.rollback() + return False finally: db.close() + LOG.debug(f"add_friend_to_db {tox_id}") def delete_friend_from_db(self, tox_id): db = self._connect() @@ -84,11 +99,14 @@ class Database: cursor = db.cursor() cursor.execute('DROP TABLE id' + tox_id + ';') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG.error("delete_friend_from_db " +self._name +f" Database exception! {e}") db.rollback() + return False finally: db.close() + LOG.debug(f"delete_friend_from_db {tox_id}") def save_messages_to_db(self, tox_id, messages_iter): db = self._connect() @@ -96,13 +114,16 @@ 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() - except: - print('Database is locked!') + return True + except Exception as e: + LOG.error("save_messages_to_db" +self._name +f" Database exception! {e}") db.rollback() + return False finally: db.close() + LOG.debug(f"save_messages_to_db {tox_id}") def update_messages(self, tox_id, message_id): db = self._connect() @@ -111,11 +132,14 @@ class Database: cursor.execute('UPDATE id' + tox_id + ' SET author = 0 ' 'WHERE id = ' + str(message_id) + ' AND author = 2;') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG.error("update_messages" +self._name +f" Database exception! {e}") db.rollback() + return False finally: db.close() + LOG.debug(f"update_messages {tox_id}") def delete_message(self, tox_id, unique_id): db = self._connect() @@ -123,11 +147,14 @@ class Database: cursor = db.cursor() cursor.execute('DELETE FROM id' + tox_id + ' WHERE id = ' + str(unique_id) + ';') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG.error("delete_message" +self._name +f" Database exception! {e}") db.rollback() + return False finally: db.close() + LOG.debug(f"delete_message {tox_id}") def delete_messages(self, tox_id): db = self._connect() @@ -135,20 +162,21 @@ class Database: cursor = db.cursor() cursor.execute('DELETE FROM id' + tox_id + ';') db.commit() - except: - print('Database is locked!') + return True + except Exception as e: + LOG.error("delete_messages" +self._name +f" Database exception! {e}") db.rollback() + return False finally: db.close() + LOG.debug(f"delete_messages {tox_id}") def messages_getter(self, tox_id): self.add_friend_to_db(tox_id) return Database.MessageGetter(self._path, tox_id) - # ----------------------------------------------------------------------------------------------------------------- # Messages loading - # ----------------------------------------------------------------------------------------------------------------- class MessageGetter: @@ -193,9 +221,7 @@ 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 bd7e353..971fa29 100644 --- a/toxygen/history/history.py +++ b/toxygen/history/history.py @@ -1,5 +1,9 @@ +# -*- 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: @@ -11,22 +15,21 @@ 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 """ - if self._settings['save_db']: + # me a mistake? was _db not _history + if self._settings['save_history']: for friend in self._contact_provider.get_all_friends(): self._db.add_friend_to_db(friend.tox_id) if not self._settings['save_unsent_only']: @@ -57,8 +60,10 @@ 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() @@ -121,9 +126,7 @@ 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 b8d0a56..91c0a28 100644 --- a/toxygen/history/history_logs_generators.py +++ b/toxygen/history/history_logs_generators.py @@ -1,5 +1,7 @@ -from messenger.messages import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import utils.util as util +from messenger.messages import * class HistoryLogsGenerator: diff --git a/toxygen/images/accept.png b/toxygen/images/accept.png old mode 100755 new mode 100644 index aaa1388..eedb818 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 100755 new mode 100644 index 2fd2818..7969974 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 100755 new mode 100644 index 2fdebe7..bac3af7 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 100755 new mode 100644 index 83ac757..06255a1 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 857b396..40b9bff 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 a01eb3f..5f73464 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 100755 new mode 100644 index dc0d672..1820653 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 100755 new mode 100644 index ef9fa86..ba153e9 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 100755 new mode 100644 index 9bbc9d5..e6313fd 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 100755 new mode 100644 index 9f39789..3ac0b6d 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 100755 new mode 100644 index edbfad9..526fd10 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 100755 new mode 100644 index a08361e..d8d85d7 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 100755 new mode 100644 index 8465106..9e4f830 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 22adab0..3ea6469 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 a790ae1..6051ac7 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 new file mode 100644 index 0000000..b9fae66 Binary files /dev/null and b/toxygen/images/icon.xcf differ diff --git a/toxygen/images/icon_new_messages.png b/toxygen/images/icon_new_messages.png old mode 100755 new mode 100644 index a3f1900..aa15890 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 2550926..62fa74c 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 29f3b49..be372f9 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 100755 new mode 100644 index b83350a..6467b23 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 100755 new mode 100644 index 4fe4c98..2301877 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 100755 new mode 100644 index 4d72f03..72bd478 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 70a863b..54f83b7 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 77006ed..98dc068 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 1e5f40a..2381304 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 6e85b15..72b988b 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 100755 new mode 100644 index 5c8ee4c..bbedc4a 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 100755 new mode 100644 index 22bb736..4ceca74 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 100755 new mode 100644 index 5599da9..9c14c6f 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 bf0dff6..8e4875b 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 100755 new mode 100644 index a2aeed8..ef17f60 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 100755 new mode 100644 index 6b5c0f6..98787dc 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 100755 new mode 100644 index f82eae7..901de59 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 100755 new mode 100644 index 26ad69b..405f80d Binary files a/toxygen/images/typing.png and b/toxygen/images/typing.png differ diff --git a/toxygen/main.py b/toxygen/main.py deleted file mode 100644 index eca3ac3..0000000 --- a/toxygen/main.py +++ /dev/null @@ -1,51 +0,0 @@ -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 e777c4b..d44a7a9 100644 --- a/toxygen/messenger/messages.py +++ b/toxygen/messenger/messages.py @@ -1,7 +1,9 @@ -from history.database import MESSAGE_AUTHOR -import os.path -from ui.messages_widgets import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os.path + +from history.database import MESSAGE_AUTHOR +from ui.messages_widgets import * MESSAGE_TYPE = { 'TEXT': 0, @@ -38,8 +40,8 @@ class Message: MESSAGE_ID = 0 - def __init__(self, message_type, author, time): - self._time = time + def __init__(self, message_type, author, iTime): + self._time = iTime self._type = message_type self._author = author self._widget = None @@ -66,7 +68,8 @@ class Message: message_id = property(get_message_id) def get_widget(self, *args): - self._widget = self._create_widget(*args) + # FixMe + self._widget = self._create_widget(*args) # pylint: disable=assignment-from-none return self._widget @@ -81,10 +84,11 @@ class Message: self._widget.mark_as_sent() def _create_widget(self, *args): - pass + # overridden + return None @staticmethod - def _get_id(): + def _get_id() -> int: Message.MESSAGE_ID += 1 return int(Message.MESSAGE_ID) @@ -95,12 +99,12 @@ class TextMessage(Message): Plain text or action message """ - def __init__(self, message, owner, time, message_type, message_id=0): - super().__init__(message_type, owner, time) + def __init__(self, message, owner, iTime, message_type, message_id=0): + super().__init__(message_type, owner, iTime) self._message = message self._id = message_id - def get_text(self): + def get_text(self) -> str: return self._message text = property(get_text) @@ -119,8 +123,8 @@ class TextMessage(Message): class OutgoingTextMessage(TextMessage): - def __init__(self, message, owner, time, message_type, tox_message_id=0): - super().__init__(message, owner, time, message_type) + def __init__(self, message, owner, iTime, message_type, tox_message_id=0): + super().__init__(message, owner, iTime, message_type) self._tox_message_id = tox_message_id def get_tox_message_id(self): @@ -134,8 +138,8 @@ class OutgoingTextMessage(TextMessage): class GroupChatMessage(TextMessage): - def __init__(self, id, message, owner, time, message_type, name): - super().__init__(id, message, owner, time, message_type) + def __init__(self, cid, message, owner, iTime, message_type, name): + super().__init__(cid, message, owner, iTime, message_type) self._user_name = name @@ -144,20 +148,20 @@ class TransferMessage(Message): Message with info about file transfer """ - def __init__(self, author, time, state, size, file_name, friend_number, file_number): - super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, time) + def __init__(self, author, iTime, state, size, file_name, friend_number, file_number): + super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, iTime) self._state = state self._size = size self._file_name = file_name self._friend_number, self._file_number = friend_number, file_number - def is_active(self, file_number): + def is_active(self, file_number) -> bool: if self._file_number != file_number: return False return self._state not in (FILE_TRANSFER_STATE['FINISHED'], FILE_TRANSFER_STATE['CANCELLED']) - def get_friend_number(self): + def get_friend_number(self) -> int: return self._friend_number friend_number = property(get_friend_number) @@ -185,10 +189,10 @@ class TransferMessage(Message): file_name = property(get_file_name) - def transfer_updated(self, state, percentage, time): + def transfer_updated(self, state, percentage, iTime): self._state = state if self._widget is not None: - self._widget.update_transfer_state(state, percentage, time) + self._widget.update_transfer_state(state, percentage, iTime) def _create_widget(self, *args): return FileTransferItem(self, *args) @@ -196,9 +200,9 @@ class TransferMessage(Message): class UnsentFileMessage(TransferMessage): - def __init__(self, path, data, time, author, size, friend_number): + def __init__(self, path, data, iTime, author, size, friend_number): file_name = os.path.basename(path) - super().__init__(author, time, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) + super().__init__(author, iTime, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) self._data, self._path = data, path def get_data(self): @@ -235,5 +239,5 @@ class InlineImageMessage(Message): class InfoMessage(TextMessage): - def __init__(self, message, time): - super().__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) + def __init__(self, message, iTime): + super().__init__(message, None, iTime, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py index e859135..c38bc31 100644 --- a/toxygen/messenger/messenger.py +++ b/toxygen/messenger/messenger.py @@ -1,6 +1,15 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import logging import common.tox_save as tox_save -from messenger.messages import * +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): @@ -19,18 +28,19 @@ 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 get_last_message(self): + def __repr__(self): + return "" + + def get_last_message(self) -> str: contact = self._contacts_manager.get_curr_contact() if contact is None: return str() return contact.get_last_message_text() - # ----------------------------------------------------------------------------------------------------------------- # Messaging - friends - # ----------------------------------------------------------------------------------------------------------------- - def new_message(self, friend_number, message_type, message): + def new_message(self, friend_number, message_type, message) -> None: """ Current user gets new message :param friend_number: friend_num of friend who sent message @@ -42,7 +52,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): + def send_message(self) -> None: text = self._screen.messageEdit.toPlainText() plugin_command_prefix = '/plugin ' @@ -51,33 +61,53 @@ class Messenger(tox_save.ToxSave): self._screen.messageEdit.clear() return - action_message_prefix = '/me ' - if text.startswith(action_message_prefix): - message_type = TOX_MESSAGE_TYPE['ACTION'] - text = text[len(action_message_prefix):] - else: - message_type = TOX_MESSAGE_TYPE['NORMAL'] + message_type = TOX_MESSAGE_TYPE['NORMAL'] + if False: # undocumented + action_message_prefix = '/me ' + if text.startswith(action_message_prefix): + message_type = TOX_MESSAGE_TYPE['ACTION'] + text = text[len(action_message_prefix):] - if 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) + if len(text) > TOX_MAX_MESSAGE_LENGTH: + text = text[:TOX_MAX_MESSAGE_LENGTH] # 1372 + try: + if self._contacts_manager.is_active_a_friend(): + self.send_message_to_friend(text, message_type) + elif self._contacts_manager.is_active_a_group(): + self.send_message_to_group('~'+text, message_type) + elif self._contacts_manager.is_active_a_group_chat_peer(): + self.send_message_to_group_peer(text, message_type) + else: + LOG.warn(f'Unknown friend type for Messenger send_message') + except Exception as e: + LOG.error(f'Messenger send_message {e}') + import traceback + LOG.warn(traceback.format_exc()) + title = 'Messenger send_message Error' + text = 'Error: ' + str(e) + assert_main_thread() + util_ui.message_box(text, title) - def send_message_to_friend(self, text, message_type, friend_number=None): + def send_message_to_friend(self, text, message_type, friend_number=None) -> None: """ Send message :param text: message text :param friend_number: number of friend + from Qt callback """ + if not text: + return if friend_number is None: friend_number = self._contacts_manager.get_active_number() - - if not text or friend_number < 0: + if friend_number is None or friend_number < 0: + LOG.error(f"No _contacts_manager.get_active_number") return + assert_main_thread() friend = self._get_friend_by_number(friend_number) + if not friend: + LOG.error(f"No self._get_friend_by_number") + return messages = self._split_message(text.encode('utf-8')) t = util.get_unix_time() for message in messages: @@ -94,7 +124,7 @@ class Messenger(tox_save.ToxSave): self._screen.messageEdit.clear() self._screen.messages.scrollToBottom() - def send_messages(self, friend_number): + def send_messages(self, friend_number:int) -> None: """ Send 'offline' messages to friend """ @@ -106,13 +136,11 @@ 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: - util.log('Sending pending messages failed with ' + str(ex)) + LOG.warn('Sending pending messages failed with ' + str(ex)) - # ----------------------------------------------------------------------------------------------------------------- # Messaging - groups - # ----------------------------------------------------------------------------------------------------------------- - def send_message_to_group(self, text, message_type, group_number=None): + def send_message_to_group(self, text, message_type, group_number=None) -> None: if group_number is None: group_number = self._contacts_manager.get_active_number() @@ -133,7 +161,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): + def new_group_message(self, group_number, message_type, message, peer_id) -> None: """ Current user gets new message :param message_type: message type - plain text or action message (/me) @@ -141,40 +169,63 @@ 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): + def send_message_to_group_peer(self, text, message_type, group_number=None, peer_id=None) -> None: if group_number is None or peer_id is None: group_peer_contact = self._contacts_manager.get_curr_contact() peer_id = group_peer_contact.number group = self._get_group_by_public_key(group_peer_contact.group_pk) group_number = group.number - if not text or group_number < 0 or peer_id < 0: + if not text: + return + if group.number < 0: + return + if peer_id is not None and peer_id < 0: return - group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) + assert_main_thread() group = self._get_group_by_number(group_number) messages = self._split_message(text.encode('utf-8')) + + # FixMe: peer_id is None? + group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) + if group_peer_contact is None: + LOG.warn("M.group_send_private_message group_peer_contact is None") + return + # group_peer_contact now may be None + t = util.get_unix_time() for message in messages: - self._tox.group_send_private_message(group_number, peer_id, message_type, message) + bRet = self._tox.group_send_private_message(group_number, peer_id, message_type, message) + if not bRet: + LOG.warn("M.group_send_private_messag failed") + continue message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) message = OutgoingTextMessage(text, message_author, t, message_type) - group_peer_contact.append_message(message) + # AttributeError: 'GroupChatPeer' object has no attribute 'append_message' + if not hasattr(group_peer_contact, 'append_message'): + LOG.warn("M. group_peer_contact has no append_message group_peer_contact={group_peer_contact}") + else: + group_peer_contact.append_message(message) if not self._contacts_manager.is_contact_active(group_peer_contact): return self._create_message_item(message) self._screen.messageEdit.clear() self._screen.messages.scrollToBottom() - def new_group_private_message(self, group_number, message_type, message, peer_id): + def new_group_private_message(self, group_number, message_type, message, peer_id) -> None: """ Current user gets new message :param message: text of message @@ -182,24 +233,26 @@ 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): + def receipt(self, friend_number, message_id) -> None: friend = self._get_friend_by_number(friend_number) friend.mark_as_sent(message_id) - # ----------------------------------------------------------------------------------------------------------------- # Typing notifications - # ----------------------------------------------------------------------------------------------------------------- - def send_typing(self, typing): + def send_typing(self, typing) -> None: """ Send typing notification to a friend """ @@ -208,18 +261,16 @@ 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): + def friend_typing(self, friend_number, typing) -> None: """ Display incoming typing notification """ if self._contacts_manager.is_friend_active(friend_number): self._screen.typing.setVisible(typing) - # ----------------------------------------------------------------------------------------------------------------- # Contact info updated - # ----------------------------------------------------------------------------------------------------------------- - def new_friend_name(self, friend, old_name, new_name): + def new_friend_name(self, friend, old_name, new_name) -> None: if old_name == new_name or friend.has_alias(): return message = util_ui.tr('User {} is now known as {}') @@ -228,12 +279,10 @@ class Messenger(tox_save.ToxSave): friend.actions = True self._add_info_message(friend.number, message) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod - def _split_message(message): + def _split_message(message) -> list: messages = [] while len(message) > TOX_MAX_MESSAGE_LENGTH: size = TOX_MAX_MESSAGE_LENGTH * 4 // 5 @@ -254,7 +303,7 @@ class Messenger(tox_save.ToxSave): return messages - def _get_friend_by_number(self, friend_number): + def _get_friend_by_number(self, friend_number:int): return self._contacts_provider.get_friend_by_number(friend_number) def _get_group_by_number(self, group_number): @@ -263,7 +312,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): + def _on_profile_name_changed(self, new_name) -> None: if self._profile_name == new_name: return message = util_ui.tr('User {} is now known as {}') @@ -272,39 +321,47 @@ 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): + def _on_call_started(self, friend_number, audio, video, is_outgoing) -> None: if is_outgoing: text = util_ui.tr("Outgoing video call") if video else util_ui.tr("Outgoing audio call") else: text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") self._add_info_message(friend_number, text) - def _on_call_finished(self, friend_number, is_declined): + def _on_call_finished(self, friend_number, is_declined) -> None: text = util_ui.tr("Call declined") if is_declined else util_ui.tr("Call finished") self._add_info_message(friend_number, text) - def _add_info_message(self, friend_number, text): + def _add_info_message(self, friend_number, text) -> None: friend = self._get_friend_by_number(friend_number) + assert friend message = InfoMessage(text, util.get_unix_time()) friend.append_message(message) if self._contacts_manager.is_friend_active(friend_number): self._create_info_message_item(message) - def _create_info_message_item(self, message): + def _create_info_message_item(self, message) -> None: + assert_main_thread() self._items_factory.create_message_item(message) self._screen.messages.scrollToBottom() - def _add_message(self, text_message, contact): + def _add_message(self, text_message, contact) -> None: + assert_main_thread() + if not contact: + LOG.warn("_add_message null contact") + return if self._contacts_manager.is_contact_active(contact): # add message to list +# LOG.debug("_add_message is_contact_active(contact)") self._create_message_item(text_message) self._screen.messages.scrollToBottom() self._contacts_manager.get_curr_contact().append_message(text_message) else: +# LOG.debug("_add_message not is_contact_active(contact)") contact.inc_messages() contact.append_message(text_message) if not contact.visibility: self._contacts_manager.update_filtration() - def _create_message_item(self, text_message): + def _create_message_item(self, text_message) -> None: # pixmap = self._contacts_manager.get_curr_contact().get_pixmap() self._items_factory.create_message_item(text_message) diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py index b9a4099..e0842f7 100644 --- a/toxygen/middleware/callbacks.py +++ b/toxygen/middleware/callbacks.py @@ -1,48 +1,113 @@ -from PyQt5 import QtGui -from wrapper.toxcore_enums_and_consts import * -from wrapper.toxav_enums import * -from wrapper.tox import bin_to_string +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import sys +import os +import threading +from qtpy import QtGui +from toxygen_wrapper.toxcore_enums_and_consts import * +from toxygen_wrapper.toxav_enums import * +from toxygen_wrapper.tox import bin_to_string import utils.ui as util_ui import utils.util as util -import cv2 -import numpy as np from middleware.threads import invoke_in_main_thread, execute from notifications.tray import tray_notification from notifications.sound import * -import threading +from datetime import datetime + +iMAX_INT32 = 4294967295 +# callbacks can be called in any thread so were being careful +def LOG_ERROR(l): print(f"EROR. {l}") +def LOG_WARN(l): print(f"WARN. {l}") +def LOG_INFO(l): + bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel <= 20-1 # pylint dusable=undefined-variable + if bIsVerbose: print(f"INFO. {l}") +def LOG_DEBUG(l): + bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel <= 10-1 # pylint dusable=undefined-variable + if bIsVerbose: print(f"DBUG. {l}") +def LOG_TRACE(l): + bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel < 10-1 # pylint dusable=undefined-variable + pass # print(f"TRACE. {l}") + +global aTIMES +aTIMES=dict() +def bTooSoon(key, sSlot, fSec=10.0): + # rate limiting + global aTIMES + if sSlot not in aTIMES: + aTIMES[sSlot] = dict() + OTIME = aTIMES[sSlot] + now = datetime.now() + if key not in OTIME: + OTIME[key] = now + return False + delta = now - OTIME[key] + OTIME[key] = now + if delta.total_seconds() < fSec: return True + return False # TODO: refactoring. Use contact provider instead of manager -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - current user -# ----------------------------------------------------------------------------------------------------------------- +global iBYTES +iBYTES=0 +def sProcBytes(sFile=None): + if sys.platform == 'win32': return '' + global iBYTES + if sFile is None: + pid = os.getpid() + sFile = f"/proc/{pid}/net/softnet_stat" + if os.path.exists(sFile): + total = 0 + with open(sFile, 'r') as iFd: + for elt in iFd.readlines(): + i = elt.find(' ') + p = int(elt[:i], 16) + total = total + p + if iBYTES == 0: + iBYTES = total + return '' + diff = total - iBYTES + s = f' {diff // 1024} Kbytes' + else: + s = '' + return s def self_connection_status(tox, profile): """ Current user changed connection status (offline, TCP, UDP) """ + sSlot = 'self connection status' def wrapped(tox_link, connection, user_data): - 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) + key = f"connection {connection}" + if bTooSoon(key, sSlot, 10): return + s = sProcBytes() + try: + status = tox.self_get_status() if connection != TOX_CONNECTION['NONE'] else None + if status: + LOG_DEBUG(f"self_connection_status: connection={connection} status={status}" +' '+s) + invoke_in_main_thread(profile.set_status, status) + except Exception as e: + LOG_ERROR(f"self_connection_status: {e}") + pass return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - friends -# ----------------------------------------------------------------------------------------------------------------- def friend_status(contacts_manager, file_transfer_handler, profile, settings): + sSlot = 'friend status' def wrapped(tox, friend_number, new_status, user_data): """ Check friend's status (none, busy, away) """ - print("Friend's #{} status changed!".format(friend_number)) + LOG_INFO(f"Friend's #{friend_number} status changed") + key = f"friend_number {friend_number}" + if bTooSoon(key, sSlot, 10): return friend = contacts_manager.get_friend_by_number(friend_number) - if friend.status is None and settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + 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) @@ -61,7 +126,7 @@ def friend_connection_status(contacts_manager, profile, settings, plugin_loader, """ Check friend's connection status (offline, udp, tcp) """ - print("Friend #{} connection status: {}".format(friend_number, new_status)) + LOG_DEBUG(f"Friend #{friend_number} connection status: {new_status}") friend = contacts_manager.get_friend_by_number(friend_number) if new_status == TOX_CONNECTION['NONE']: invoke_in_main_thread(friend.set_status, None) @@ -79,29 +144,35 @@ 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 """ - print('New name friend #' + str(friend_number)) + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 60): return friend = contacts_provider.get_friend_by_number(friend_number) old_name = friend.name new_name = str(name, 'utf-8') + LOG_DEBUG(f"get_friend_by_number #{friend_number} {new_name}") invoke_in_main_thread(friend.set_name, new_name) invoke_in_main_thread(messenger.new_friend_name, friend, old_name, new_name) return wrapped - def friend_status_message(contacts_manager, messenger): + sSlot = 'status_message' def wrapped(tox, friend_number, status_message, size, user_data): """ :return: function for callback friend_status_message. It updates friend's status message and calls window repaint """ friend = contacts_manager.get_friend_by_number(friend_number) + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 10): return + invoke_in_main_thread(friend.set_status_message, str(status_message, 'utf-8')) - print('User #{} has new status message'.format(friend_number)) + LOG_DEBUG(f'User #{friend_number} has new status message') invoke_in_main_thread(messenger.send_messages, friend_number) return wrapped @@ -112,16 +183,20 @@ 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') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if tray: + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped @@ -131,7 +206,7 @@ def friend_request(contacts_manager): """ Called when user get new friend request """ - print('Friend request') + LOG_DEBUG(f'Friend request') key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) invoke_in_main_thread(contacts_manager.process_friend_request, tox_id, str(message, 'utf-8')) @@ -140,9 +215,12 @@ 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 @@ -153,9 +231,7 @@ def friend_read_receipt(messenger): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - file transfers -# ----------------------------------------------------------------------------------------------------------------- def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager, settings): @@ -164,7 +240,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']: - print('File') + LOG_INFO(f'file_transfer_handler File friend_number={friend_number}') try: file_name = str(file_name[:file_name_size], 'utf-8') except: @@ -176,15 +252,18 @@ 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']) - icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if tray: + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) else: # avatar - print('Avatar') + LOG_DEBUG(f'file_transfer_handler Avatar') invoke_in_main_thread(file_transfer_handler.incoming_avatar, friend_number, file_number, @@ -227,9 +306,7 @@ def file_recv_control(file_transfer_handler): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - custom packets -# ----------------------------------------------------------------------------------------------------------------- def lossless_packet(plugin_loader): @@ -254,20 +331,20 @@ def lossy_packet(plugin_loader): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - audio -# ----------------------------------------------------------------------------------------------------------------- def call_state(calls_manager): - def wrapped(toxav, friend_number, mask, user_data): + def wrapped(iToxav, friend_number, mask, user_data): """ New call state """ - print(friend_number, mask) + LOG_DEBUG(f"call_state #{friend_number}") if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: invoke_in_main_thread(calls_manager.stop_call, friend_number, True) else: - calls_manager.toxav_call_state_cb(friend_number, mask) + # guessing was calls_manager. + #? incoming_call + calls_manager._call.toxav_call_state_cb(friend_number, mask) return wrapped @@ -277,7 +354,7 @@ def call(calls_manager): """ Incoming call from friend """ - print(friend_number, audio, video) + LOG_DEBUG(f"Incoming call from {friend_number} {audio} {video}") invoke_in_main_thread(calls_manager.incoming_call, audio, video, friend_number) return wrapped @@ -288,16 +365,16 @@ def callback_audio(calls_manager): """ New audio chunk """ - calls_manager.call.audio_chunk( +#trace LOG_DEBUG(f"callback_audio #{friend_number}") + # dunno was .call + calls_manager._call.audio_chunk( bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), audio_channels_count, rate) return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - video -# ----------------------------------------------------------------------------------------------------------------- def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data): @@ -324,6 +401,9 @@ 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))) @@ -345,15 +425,14 @@ 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) - - invoke_in_main_thread(cv2.imshow, str(friend_number), frame) + 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: - print(ex) + LOG_ERROR(f"video_receive_frame {ex} #{friend_number}") + pass -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - groups -# ----------------------------------------------------------------------------------------------------------------- def group_message(window, tray, tox, messenger, settings, profile): @@ -361,18 +440,24 @@ 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['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']: + if settings['sound_notifications'] and bl and \ + profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['MESSAGE']) - icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if False and settings['tray_icon'] and tray: + if settings['notifications'] and \ + profile.status != TOX_USER_STATUS['BUSY'] and \ + (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + if tray: + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped @@ -382,54 +467,87 @@ 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 - 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: + try: + name = tox.group_peer_get_name(group_number, peer_id) + except Exception as e: + LOG_WARN("tox.group_peer_get_name {group_number} {peer_id}") + name = '' + if settings['notifications'] and settings['tray_icon'] \ + and profile.status != TOX_USER_STATUS['BUSY'] \ + and (not settings.locked) and bl: invoke_in_main_thread(tray_notification, name, message, tray, window) if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: sound_notification(SOUND_NOTIFICATION['MESSAGE']) icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if tray and hasattr(tray, 'setIcon'): + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped - +# Exception ignored on calling ctypes callback function: .wrapped at 0x7ffede910700> def group_invite(window, settings, tray, profile, groups_service, contacts_provider): def wrapped(tox, friend_number, invite_data, length, group_name, group_name_length, user_data): + LOG_DEBUG(f"group_invite friend_number={friend_number}") group_name = str(bytes(group_name[:group_name_length]), 'utf-8') invoke_in_main_thread(groups_service.process_group_invite, friend_number, group_name, bytes(invite_data[:length])) if window.isActiveWindow(): return - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: + bHasTray = tray and settings['tray_icon'] + if settings['notifications'] \ + and bHasTray \ + and profile.status != TOX_USER_STATUS['BUSY'] \ + and not settings.locked: friend = contacts_provider.get_friend_by_number(friend_number) title = util_ui.tr('New invite to group chat') text = util_ui.tr('{} invites you to group "{}"').format(friend.name, group_name) invoke_in_main_thread(tray_notification, title, text, tray, window) - icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + if tray: + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) return wrapped def group_self_join(contacts_provider, contacts_manager, groups_service): + sSlot = 'group_self_join' def wrapped(tox, group_number, user_data): + if group_number is None: + LOG_ERROR(f"group_self_join NULL group_number #{group_number}") + return + LOG_DEBUG(f"group_self_join #{group_number}") + key = f"group_number {group_number}" + if bTooSoon(key, sSlot, 10): return group = contacts_provider.get_group_by_number(group_number) + if group is None: + LOG_ERROR(f"group_self_join NULL group #{group}") + return invoke_in_main_thread(group.set_status, TOX_USER_STATUS['NONE']) invoke_in_main_thread(groups_service.update_group_info, group) invoke_in_main_thread(contacts_manager.update_filtration) return wrapped - def group_peer_join(contacts_provider, groups_service): + sSlot = "group_peer_join" def wrapped(tox, group_number, peer_id, user_data): + key = f"group_peer_join #{group_number} peer_id={peer_id}" + if bTooSoon(key, sSlot, 20): return group = contacts_provider.get_group_by_number(group_number) + if group is None: + LOG_ERROR(f"group_peer_join NULL group #{group} group_number={group_number}") + return + if peer_id > group._peers_limit: + LOG_ERROR(key +f" {peer_id} > {group._peers_limit}") + return + LOG_DEBUG(f"group_peer_join group={group}") group.add_peer(peer_id) invoke_in_main_thread(groups_service.generate_peers_list) invoke_in_main_thread(groups_service.update_group_info, group) @@ -438,29 +556,49 @@ 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, message, length, user_data): + def wrapped(tox, + group_number, peer_id, + exit_type, name, name_length, + message, length, + user_data): group = contacts_provider.get_group_by_number(group_number) - group.remove_peer(peer_id) - invoke_in_main_thread(groups_service.generate_peers_list) + if group: + LOG_DEBUG(f"group_peer_exit #{group_number} peer_id={peer_id} exit_type={exit_type}") + group.remove_peer(peer_id) + invoke_in_main_thread(groups_service.generate_peers_list) + else: + LOG_WARN(f"group_peer_exit group not found #{group_number} peer_id={peer_id}") return wrapped - def group_peer_name(contacts_provider, groups_service): def wrapped(tox, group_number, peer_id, name, length, user_data): + LOG_DEBUG(f"group_peer_name #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - peer.name = str(name[:length], 'utf-8') - invoke_in_main_thread(groups_service.generate_peers_list) + if peer: + peer.name = str(name[:length], 'utf-8') + invoke_in_main_thread(groups_service.generate_peers_list) + else: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + return return wrapped def group_peer_status(contacts_provider, groups_service): def wrapped(tox, group_number, peer_id, peer_status, user_data): + LOG_DEBUG(f"group_peer_status #{group_number} peer_id={peer_id}") group = contacts_provider.get_group_by_number(group_number) peer = group.get_peer_by_id(peer_id) - peer.status = peer_status + if peer: + peer.status = peer_status + else: + # _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + # TODO: add info message invoke_in_main_thread(groups_service.generate_peers_list) return wrapped @@ -468,32 +606,62 @@ 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) - topic = str(topic[:length], 'utf-8') - invoke_in_main_thread(group.set_status_message, topic) + if group: + topic = str(topic[:length], 'utf-8') + invoke_in_main_thread(group.set_status_message, topic) + else: + _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_WARN(f"group_topic {group} has no peer_id={peer_id} in {_peers}") + # TODO: add info message return wrapped - def group_moderation(groups_service, contacts_provider, contacts_manager, messenger): - def update_peer_role(group, mod_peer_id, peer_id, new_role): peer = group.get_peer_by_id(peer_id) - peer.role = new_role + if peer: + peer.role = new_role + # TODO: add info message + else: + # FixMe: known signal to revalidate roles... + # _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"update_peer_role group {group} has no peer_id={peer_id} in _peers!r") # TODO: add info message def remove_peer(group, mod_peer_id, peer_id, is_ban): - contacts_manager.remove_group_peer_by_id(group, peer_id) - group.remove_peer(peer_id) + peer = group.get_peer_by_id(peer_id) + if peer: + contacts_manager.remove_group_peer_by_id(group, peer_id) + group.remove_peer(peer_id) + else: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") # TODO: add info message + # source_peer_number, target_peer_number, def wrapped(tox, group_number, mod_peer_id, peer_id, event_type, user_data): + if mod_peer_id == iMAX_INT32 or peer_id == iMAX_INT32: + # FixMe: known signal to revalidate roles... + return + LOG_DEBUG(f"group_moderation #{group_number} mod_id={mod_peer_id} peer_id={peer_id} event_type={event_type}") group = contacts_provider.get_group_by_number(group_number) + mod_peer = group.get_peer_by_id(mod_peer_id) + if not mod_peer: + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no mod_peer_id={mod_peer_id} in _peers!r") + return + peer = group.get_peer_by_id(peer_id) + if not peer: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + return if event_type == TOX_GROUP_MOD_EVENT['KICK']: remove_peer(group, mod_peer_id, peer_id, False) - elif event_type == TOX_GROUP_MOD_EVENT['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']: @@ -509,6 +677,7 @@ 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 @@ -519,6 +688,7 @@ 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 @@ -528,19 +698,18 @@ 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): + contacts_provider, ms=None): """ Initialization of all callbacks. :param tox: Tox instance @@ -557,6 +726,7 @@ 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 5f9404b..75e3fc9 100644 --- a/toxygen/middleware/threads.py +++ b/toxygen/middleware/threads.py @@ -1,115 +1,170 @@ -from bootstrap.bootstrap import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import sys import threading import queue +from qtpy import QtCore + +from bootstrap.bootstrap import * +from bootstrap.bootstrap import download_nodes_list +from toxygen_wrapper.toxcore_enums_and_consts import TOX_USER_STATUS, TOX_CONNECTION +import toxygen_wrapper.tests.support_testing as ts from utils import util + import time -from PyQt5 import QtCore +sleep = time.sleep +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+'threads') +# log = lambda x: LOG.info(x) + +def LOG_ERROR(l): print('EROR+ '+l) +def LOG_WARN(l): print('WARN+ '+l) +def LOG_INFO(l): print('INFO+ '+l) +def LOG_DEBUG(l): print('DBUG+ '+l) +def LOG_TRACE(l): pass # print('TRAC+ '+l) + +iLAST_CONN = 0 +iLAST_DELTA = 60 -# ----------------------------------------------------------------------------------------------------------------- # Base threads -# ----------------------------------------------------------------------------------------------------------------- class BaseThread(threading.Thread): - def __init__(self): - super().__init__() + def __init__(self, name=None, target=None): self._stop_thread = False + if name: + super().__init__(name=name, target=target) + else: + super().__init__(target=target) - def stop_thread(self): + def stop_thread(self, timeout=-1): self._stop_thread = True - self.join() - + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.join(timeout) + if not self.is_alive(): break + i = i + 1 + else: + LOG_WARN(f"BaseThread {self.name} BLOCKED after {ts.iTHREAD_JOINS}") class BaseQThread(QtCore.QThread): - def __init__(self): + def __init__(self, name=None): + # NO name=name super().__init__() self._stop_thread = False + self.name = str(id(self)) - def stop_thread(self): + def stop_thread(self, timeout=-1): self._stop_thread = True - self.wait() + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.wait(timeout) + if not self.isRunning(): break + i = i + 1 + sleep(ts.iTHREAD_TIMEOUT) + else: + LOG_WARN(f"BaseQThread {self.name} BLOCKED") - -# ----------------------------------------------------------------------------------------------------------------- # Toxcore threads -# ----------------------------------------------------------------------------------------------------------------- class InitThread(BaseThread): - def __init__(self, tox, plugin_loader, settings, is_first_start): - super().__init__() - self._tox, self._plugin_loader, self._settings = tox, plugin_loader, settings + def __init__(self, tox, plugin_loader, settings, app, is_first_start): + super().__init__(name='InitThread') + self._tox = tox + self._plugin_loader = plugin_loader + self._settings = settings + self._app = app self._is_first_start = is_first_start def run(self): - if self._is_first_start: - # download list of nodes if needed - download_nodes_list(self._settings) - # start plugins - self._plugin_loader.load() - - # bootstrap + # DBUG+ InitThread run: ERROR name 'ts' is not defined + import toxygen_wrapper.tests.support_testing as ts + LOG_DEBUG('InitThread run: ') try: - for data in generate_nodes(): - if self._stop_thread: - return - self._tox.bootstrap(*data) - self._tox.add_tcp_relay(*data) - except: + if self._is_first_start and ts.bAreWeConnected() and \ + self._settings['download_nodes_list']: + LOG_INFO(f"downloading list of nodes {self._settings['download_nodes_list']}") + download_nodes_list(self._settings, oArgs=self._app._args) + + if ts.bAreWeConnected(): + LOG_INFO(f"calling test_net nodes") + self._app.test_net(oThread=self, iMax=4) + + if self._is_first_start: + LOG_INFO('starting plugins') + self._plugin_loader.load() + + except Exception as e: + LOG_DEBUG(f"InitThread run: ERROR {e}") pass - for _ in range(10): + for _ in range(ts.iTHREAD_JOINS): if self._stop_thread: return - time.sleep(1) - - while not self._tox.self_get_connection_status(): - try: - for data in generate_nodes(None): - if self._stop_thread: - return - self._tox.bootstrap(*data) - self._tox.add_tcp_relay(*data) - except: - pass - finally: - time.sleep(5) - + sleep(ts.iTHREAD_SLEEP) + return class ToxIterateThread(BaseQThread): - def __init__(self, tox): + def __init__(self, tox, app=None): super().__init__() self._tox = tox + self._app = app def run(self): + LOG_DEBUG('ToxIterateThread run: ') while not self._stop_thread: - self._tox.iterate() - time.sleep(self._tox.iteration_interval() / 1000) + try: + iMsec = self._tox.iteration_interval() + self._tox.iterate() + except Exception as e: + # Fatal Python error: Segmentation fault + LOG_ERROR(f"ToxIterateThread run: {e}") + else: + sleep(iMsec / 1000.0) + + global iLAST_CONN + if not iLAST_CONN: + iLAST_CONN = time.time() + # TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes + + # and segv + if time.time() - iLAST_CONN > iLAST_DELTA and \ + ts.bAreWeConnected() and \ + self._tox.self_get_status() == TOX_USER_STATUS['NONE'] and \ + self._tox.self_get_connection_status() == TOX_CONNECTION['NONE']: + iLAST_CONN = time.time() + LOG_INFO(f"ToxIterateThread calling test_net") + invoke_in_main_thread( + self._app.test_net, oThread=self, iMax=2) class ToxAVIterateThread(BaseQThread): - def __init__(self, toxav): super().__init__() self._toxav = toxav def run(self): + LOG_DEBUG('ToxAVIterateThread run: ') while not self._stop_thread: self._toxav.iterate() - time.sleep(self._toxav.iteration_interval() / 1000) + sleep(self._toxav.iteration_interval() / 1000) -# ----------------------------------------------------------------------------------------------------------------- # File transfers thread -# ----------------------------------------------------------------------------------------------------------------- class FileTransfersThread(BaseQThread): def __init__(self): - super().__init__() + super().__init__('FileTransfers') self._queue = queue.Queue() self._timeout = 0.01 @@ -124,14 +179,12 @@ class FileTransfersThread(BaseQThread): except queue.Empty: pass except queue.Full: - util.log('Queue is full in _thread') + LOG_WARN('Queue is full in _thread') except Exception as ex: - util.log('Exception in _thread: ' + str(ex)) + LOG_ERROR('in _thread: ' + str(ex)) _thread = FileTransfersThread() - - def start_file_transfer_thread(): _thread.start() @@ -144,9 +197,7 @@ 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 9ee5c01..1216dd8 100644 --- a/toxygen/middleware/tox_factory.py +++ b/toxygen/middleware/tox_factory.py @@ -1,34 +1,89 @@ -import user_data.settings -import wrapper.tox -import wrapper.toxcore_enums_and_consts as enums +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import ctypes +import traceback +import os +from ctypes import * + +import user_data.settings +import toxygen_wrapper.tox +import toxygen_wrapper.toxcore_enums_and_consts as enums +from toxygen_wrapper.tests import support_testing as ts +# callbacks can be called in any thread so were being careful +# tox.py can be called by callbacks +from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +global LOG +import logging +LOG = logging.getLogger('app.'+'tox_factory') + +from utils import util +from utils import ui as util_ui -def tox_factory(data=None, settings=None): +def tox_factory(data=None, settings=None, args=None, app=None): """ :param data: user data from .tox file. None = no saved data, create new profile :param settings: current profile settings. None = default settings will be used :return: new tox instance """ - if settings is None: + if not settings: + LOG_WARN("tox_factory using get_default_settings") settings = user_data.settings.Settings.get_default_settings() + else: + user_data.settings.clean_settings(settings) - 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 + 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 - return wrapper.tox.Tox(tox_options) + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.ipv6_enabled = False + tox_options.contents.hole_punching_enabled = False + + LOG.debug("toxygen_wrapper.tox.Tox settings: " +repr(settings)) + + if 'trace_enabled' in settings and not settings['trace_enabled']: + LOG_DEBUG("settings['trace_enabled' disabled" ) + elif tox_options._options_pointer and \ + 'trace_enabled' in settings and settings['trace_enabled']: + ts.vAddLoggerCallback(tox_options) + LOG_INFO("c-toxcore trace_enabled enabled" ) + else: + LOG_WARN("No tox_options._options_pointer to add self_logger_cb" ) + + retval = toxygen_wrapper.tox.Tox(tox_options) + except Exception as e: + if app and hasattr(app, '_log'): + pass + LOG_ERROR(f"toxygen_wrapper.tox.Tox failed: {e}") + LOG_WARN(traceback.format_exc()) + raise + + if app and hasattr(app, '_log'): + app._log("DEBUG: toxygen_wrapper.tox.Tox succeeded") + return retval diff --git a/toxygen/network/tox_dns.py b/toxygen/network/tox_dns.py index 02e97f5..58c9da1 100644 --- a/toxygen/network/tox_dns.py +++ b/toxygen/network/tox_dns.py @@ -1,20 +1,44 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import json import urllib.request -import utils.util as util -from PyQt5 import QtNetwork, QtCore +import logging +try: + import requests +except ImportError: + requests = None +from qtpy import QtNetwork, QtCore + +import utils.util as util + +global LOG + +iTIMEOUT=60 +LOG = logging.getLogger('app.'+__name__) class ToxDns: - def __init__(self, settings): + def __init__(self, settings, log=None): self._settings = settings + self._log = log @staticmethod def _send_request(url, data): - req = urllib.request.Request(url) - req.add_header('Content-Type', 'application/json') - response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) - res = json.loads(str(response.read(), 'utf-8')) + if requests: + LOG.info('send_request loading with requests: ' + str(url)) + headers = dict() + headers['Content-Type'] = 'application/json' + req = requests.get(url, headers=headers) + if req.status_code < 300: + retval = req.content + else: + raise LookupError(str(req.status_code)) + else: + req = urllib.request.Request(url) + req.add_header('Content-Type', 'application/json') + response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) + retval = response.read() + res = json.loads(str(retval, 'utf-8')) if not res['c']: return res['tox_id'] else: @@ -29,12 +53,25 @@ class ToxDns: site = email.split('@')[1] data = {"action": 3, "name": "{}".format(email)} urls = ('https://{}/api'.format(site), 'http://{}/api'.format(site)) - if not self._settings['proxy_type']: # no proxy + if requests: + for url in urls: + LOG.info('TOX nodes loading with requests: ' + str(url)) + try: + headers = dict() + headers['Content-Type'] = 'application/json' + req = requests.get(url, headers=headers, timeout=iTIMEOUT) + if req.status_code < 300: + result = req.content + return result + except Exception as ex: + LOG.error('ERROR: TOX DNS loading error with requests: ' + str(ex)) + + elif not self._settings['proxy_type']: # no proxy for url in urls: try: return self._send_request(url, data) except Exception as ex: - util.log('TOX DNS ERROR: ' + str(ex)) + LOG.error('ERROR: TOX DNS ' + str(ex)) else: # proxy netman = QtNetwork.QNetworkAccessManager() proxy = QtNetwork.QNetworkProxy() @@ -60,6 +97,6 @@ class ToxDns: if not result['c']: return result['tox_id'] except Exception as ex: - util.log('TOX DNS ERROR: ' + str(ex)) + LOG.error('ERROR: TOX DNS ' + str(ex)) return None # error diff --git a/toxygen/notifications/sound.py b/toxygen/notifications/sound.py index 361cd05..9df46b2 100644 --- a/toxygen/notifications/sound.py +++ b/toxygen/notifications/sound.py @@ -1,8 +1,17 @@ -import utils.util -import wave -import pyaudio -import os.path +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os.path +import wave + +import utils.util + +import toxygen_wrapper.tests.support_testing as ts +with ts.ignoreStderr(): + import pyaudio + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) SOUND_NOTIFICATION = { 'MESSAGE': 0, @@ -25,9 +34,19 @@ class AudioFile: def play(self): data = self.wf.readframes(self.chunk) - while data: - self.stream.write(data) - data = self.wf.readframes(self.chunk) + try: + while data: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + except Exception as e: + LOG.error(f"Error during AudioFile play {e}") + LOG.debug("Error during AudioFile play " \ + +' rate=' +str(self.wf.getframerate()) \ + + 'format=' +str(self.p.get_format_from_width(self.wf.getsampwidth())) \ + +' channels=' +str(self.wf.getnchannels()) \ + ) + + raise def close(self): self.stream.close() diff --git a/toxygen/notifications/tray.py b/toxygen/notifications/tray.py index 4232253..0a6bca3 100644 --- a/toxygen/notifications/tray.py +++ b/toxygen/notifications/tray.py @@ -1,5 +1,6 @@ -from PyQt5 import QtCore, QtWidgets +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtWidgets def tray_notification(title, text, tray, window): """ @@ -10,7 +11,7 @@ def tray_notification(title, text, tray, window): :param tray: ref to tray icon :param window: main window """ - if QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + if tray and 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 ed45910..f180e4d 100644 --- a/toxygen/plugin_support/plugin_support.py +++ b/toxygen/plugin_support/plugin_support.py @@ -1,10 +1,20 @@ -import utils.util as util +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import os import importlib import inspect -import plugins.plugin_super_class as pl import sys +import logging +import utils.util as util +import plugins.plugin_super_class as pl + +# LOG=util.log +global LOG +LOG = logging.getLogger('plugin_support') +def trace(msg, *args, **kwargs): LOG._log(0, msg, []) +LOG.trace = trace + +log = lambda x: LOG.info(x) class Plugin: @@ -33,54 +43,65 @@ class PluginLoader: self._app = app self._plugins = {} # dict. key - plugin unique short name, value - Plugin instance - def set_tox(self, tox): + def set_tox(self, tox) -> None: """ New tox instance """ for plugin in self._plugins.values(): plugin.instance.set_tox(tox) - def load(self): + def load(self) -> None: """ Load all plugins in plugins folder """ path = util.get_plugins_directory() if not os.path.exists(path): - util.log('Plugin dir not found') + self._app._LOG('WARN: Plugin directory not found: ' + path) return - else: - sys.path.append(path) + + sys.path.append(path) files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] for fl in files: if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): continue - name = fl[:-3] # module name without .py + base_name = fl[:-3] # module name without .py try: - module = importlib.import_module(name) # import plugin - except ImportError: - util.log('Import error in module ' + name) + module = importlib.import_module(base_name) # import plugin + LOG.trace('Imported module: ' +base_name +' file: ' +fl) + except ImportError as e: + LOG.warn(f"Import error: {e}" +' file: ' +fl) continue except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + LOG.error('importing ' + base_name + ' Exception: ' + str(ex)) continue for elem in dir(module): obj = getattr(module, elem) # looking for plugin class in module if not inspect.isclass(obj) or not hasattr(obj, 'is_plugin') or not obj.is_plugin: continue - print('Plugin', elem) try: # create instance of plugin class - instance = obj(self._app) - is_active = instance.get_short_name() in self._settings['plugins'] + instance = obj(self._app) # name, short_name, app + # needed by bday... + instance._profile=self._app._ms._profile + instance._settings=self._settings + short_name = instance.get_short_name() + is_active = short_name in self._settings['plugins'] if is_active: - instance.start() + try: + instance.start() + self._app._log('INFO: Started Plugin ' +short_name) + except Exception as e: + self._app._log.error(f"Starting Plugin ' +short_name +' {e}") + # else: LOG.info('Defined Plugin ' +short_name) except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + LOG.error('in module ' + short_name + ' Exception: ' + str(ex)) continue - self._plugins[instance.get_short_name()] = Plugin(instance, is_active) + short_name = instance.get_short_name() + self._plugins[short_name] = Plugin(instance, is_active) + LOG.info('Added plugin: ' +short_name +' from file: ' +fl) break - def callback_lossless(self, friend_number, data): + def callback_lossless(self, friend_number, data) -> None: """ New incoming custom lossless packet (callback) """ @@ -98,7 +119,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): + def friend_online(self, friend_number:int) -> None: """ Friend with specified number is online """ @@ -106,7 +127,7 @@ class PluginLoader: if plugin.is_active: plugin.instance.friend_connected(friend_number) - def get_plugins_list(self): + def get_plugins_list(self) -> list: """ Returns list of all plugins """ @@ -114,7 +135,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: @@ -126,9 +147,15 @@ class PluginLoader: """ Return window or None for specified plugin """ - return self._plugins[key].instance.get_window() + 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)) - def toggle_plugin(self, key): + return None + + def toggle_plugin(self, key) -> None: """ Enable/disable plugin :param key: plugin short name @@ -145,7 +172,7 @@ class PluginLoader: self._settings['plugins'].remove(key) self._settings.save() - def command(self, text): + def command(self, text) -> None: """ New command for plugin """ @@ -162,6 +189,7 @@ class PluginLoader: for plugin in self._plugins.values(): if not plugin.is_active: continue + try: result.extend(plugin.instance.get_menu(num)) except: @@ -173,13 +201,17 @@ 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): + def stop(self) -> None: """ App is closing, stop all plugins """ @@ -188,7 +220,12 @@ class PluginLoader: self._plugins[key].instance.close() del self._plugins[key] - def reload(self): - print('Reloading plugins') + def reload(self) -> None: + path = util.get_plugins_directory() + if not os.path.exists(path): + self._app._log('WARN: Plugin directory not found: ' + path) + return + self.stop() + self._app._log('INFO: Reloading plugins from ' +path) self.load() diff --git a/toxygen/plugins/README.md b/toxygen/plugins/README.md new file mode 100644 index 0000000..12ed7b0 --- /dev/null +++ b/toxygen/plugins/README.md @@ -0,0 +1,27 @@ +# 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 new file mode 100644 index 0000000..b30ea66 --- /dev/null +++ b/toxygen/plugins/ae.py @@ -0,0 +1,85 @@ +# -*- 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 new file mode 100644 index 0000000..9b63720 --- /dev/null +++ b/toxygen/plugins/awayl.py @@ -0,0 +1,114 @@ +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 new file mode 100644 index 0000000..5c4b768 --- /dev/null +++ b/toxygen/plugins/awayw.py.windows @@ -0,0 +1,115 @@ +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 new file mode 100644 index 0000000..7393e95 --- /dev/null +++ b/toxygen/plugins/bday.pro @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..8563638 --- /dev/null +++ b/toxygen/plugins/bday.py @@ -0,0 +1,98 @@ +# -*- 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 new file mode 100644 index 0000000..71db5a0 --- /dev/null +++ b/toxygen/plugins/bot.py @@ -0,0 +1,83 @@ +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 new file mode 100644 index 0000000..f5c6feb --- /dev/null +++ b/toxygen/plugins/chess.py @@ -0,0 +1,1696 @@ +# -*- 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 new file mode 100644 index 0000000..b7be07c --- /dev/null +++ b/toxygen/plugins/en_GB.ts @@ -0,0 +1,31 @@ + + + + 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 new file mode 100644 index 0000000..b7be07c --- /dev/null +++ b/toxygen/plugins/en_US.ts @@ -0,0 +1,31 @@ + + + + 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 new file mode 100644 index 0000000..d6e1a0d --- /dev/null +++ b/toxygen/plugins/garland.py @@ -0,0 +1,78 @@ +# -*- 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 new file mode 100644 index 0000000..db718fe --- /dev/null +++ b/toxygen/plugins/mrq.py @@ -0,0 +1,87 @@ +# -*- 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 0056d36..4c6287d 100644 --- a/toxygen/plugins/plugin_super_class.py +++ b/toxygen/plugins/plugin_super_class.py @@ -1,16 +1,17 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import os -from PyQt5 import QtCore, QtWidgets + +from qtpy 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 @@ -19,7 +20,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 @@ -47,14 +48,12 @@ class PluginSuperClass(tox_save.ToxSave): name = name.strip() short_name = short_name.strip() if not name or not short_name: - raise NameError('Wrong name') + raise NameError('Wrong name or not name or not short_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): """ @@ -74,7 +73,7 @@ class PluginSuperClass(tox_save.ToxSave): """ return self.__doc__ - def get_menu(self, row_number): + def get_menu(self, menu, row_number=None): """ 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 @@ -97,9 +96,7 @@ class PluginSuperClass(tox_save.ToxSave): """ return None - # ----------------------------------------------------------------------------------------------------------------- # Plugin was stopped, started or new command received - # ----------------------------------------------------------------------------------------------------------------- def start(self): """ @@ -129,9 +126,7 @@ 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): """ @@ -148,9 +143,7 @@ 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): """ @@ -169,9 +162,7 @@ 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): """ @@ -189,15 +180,13 @@ class PluginSuperClass(tox_save.ToxSave): """ pass - def friend_connected(self, friend_number): + def friend_connected(self, friend_number:int): """ 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 new file mode 100644 index 0000000..6ba937c Binary files /dev/null and b/toxygen/plugins/ru_RU.qm differ diff --git a/toxygen/plugins/ru_RU.ts b/toxygen/plugins/ru_RU.ts new file mode 100644 index 0000000..d5b0374 --- /dev/null +++ b/toxygen/plugins/ru_RU.ts @@ -0,0 +1,32 @@ + + + + + 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 new file mode 100644 index 0000000..d071285 --- /dev/null +++ b/toxygen/plugins/srch.pro @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..5dcf8d3 --- /dev/null +++ b/toxygen/plugins/srch.py @@ -0,0 +1,56 @@ +# -*- 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 new file mode 100644 index 0000000..3b1cc64 --- /dev/null +++ b/toxygen/plugins/toxid.pro @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..e604092 --- /dev/null +++ b/toxygen/plugins/toxid.py @@ -0,0 +1,140 @@ +# -*- 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 a196fa1..e64ea3a 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 26d6754..9501bdf 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 645c904..8c44746 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 1674b69..ab5b4bc 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 ef64830..4ecbce7 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 782ee47..c3d3077 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 07f549a..617d2ca 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 5093629..7fce639 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 aea2c90..3ecb8fc 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 5a19d1b..f1d6641 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 5f52426..57666e9 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 ebc7dd9..98fb62a 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 e1c3057..36d8dcf 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 0bacbe9..feb5368 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 8b5e91a..5119eae 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 89e6eb4..4393f8a 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 87aa873..c8f4d49 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 beb8b2c..7d49587 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 a1769d4..210315d 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 2b637fe..b7f91c4 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 d868dd7..e128d70 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 3775673..34cbf64 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 9f9af80..d8a6c0b 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 c13226b..588acf5 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 699dddd..dbe2607 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 b69f1ed..060cf43 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 f4b575a..4fc83f7 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 557b09f..4909b06 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 80b209b..3240476 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 36688b2..9996c1a 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 c8ec471..63485f6 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 eadb18c..0958429 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 8af2206..bccac5e 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 baed686..54992ea 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 34a504f..a957fca 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 7ffe84e..9c291f6 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 ea2a965..0ab4d16 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 1a9b1e4..02b39c8 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 8ae60bf..c1ba9c6 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 66144a8..0aab847 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 300b92d..1de985b 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 ad91b05..fcbfe56 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 14ee8fd..d1f979d 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 ae88c82..ef1b3c5 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 21f462c..c4b49d6 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 154540c..2dad11e 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 f3ac6c9..944af22 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 caf5e7f..8458b0e 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 8a3409d..5db95a5 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 36ca321..9d529e5 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 4e0687e..d67cb84 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 1e131e0..92fa0e7 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 b02cca6..0753593 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 6354aa4..7a44286 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 19cd5dc..f45f5f0 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 e000b39..80fa2eb 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 82eb8eb..ad41780 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 2b4fa50..a4cf9d8 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 ce713d8..7e40bc3 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 0032211..09f6d3b 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 85c701a..a4da181 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 3ed0373..e2a9757 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 4dd8e0b..43b0f13 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 1088ec5..7fd68db 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 0fa97e6..e123db3 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 244e954..6e148ea 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 39e485d..4c7b51d 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 8e0341d..1a33390 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 0a20950..f87253d 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 da04fd6..ec5d59f 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 aa730a7..a753ef4 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 5a7d5c3..d6dc51e 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 4cf2098..d603714 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 7bfd040..27ff3f8 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 ef49d55..aa929b6 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 93bef58..f7e509b 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 eb04e5d..8677043 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 0ad227f..35c5af5 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 e28fada..dcfb49e 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 17727e0..d606692 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 720ad23..2a86834 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 50e4a27..c51400b 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 4a5c029..5fd1cfb 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 516ad10..053a098 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 dbd528b..fcf14e6 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 0822488..15ea13b 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 0f82df9..9c849f7 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 509fa7b..c053b6a 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 4035754..dfeac0a 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 43d7ca8..51eefbb 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 984f829..3f03100 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 7fe482f..437fc21 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 a86cf25..2aa3ee0 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 cc6c6ab..97c1072 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 b675396..df3540f 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 7fac672..041e6c2 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 82aad35..5e7c381 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 d9b1f08..334a066 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 f95730e..93a995f 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 f88a35d..bf4d09e 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 6179ee0..1eaf5d2 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 64036e1..2bf2526 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 9a337af..8bbf629 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 303b2f5..fd4a4f8 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 ce83bb6..c1afcb2 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 74b34e0..80cc60e 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 0932319..5ce95cb 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 db9de9e..20b145d 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 33bb432..fedda3d 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 ca89edf..0341ac4 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 04ca489..d820089 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 36fad95..f0dd357 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 8460f2e..3ce1cc0 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 1245f99..798387c 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 7b52ecd..3517e59 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 0aba0d0..857caf4 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 8bacdda..368e2fa 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 b394430..56bb954 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 bc9532a..ed86d82 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 6a833f5..9f51c6b 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 94275fd..25ce49a 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 358da2b..d08be34 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 ff62f1b..bb71bcc 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 aeb952e..9a8d53a 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 6701f76..09639ea 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 6521d64..fb1f1f6 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 754d3c2..3ea5b82 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 dd82624..75ea41b 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 84f20f3..13c53fc 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 9a56329..eb5ebf8 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 aa5dca1..c8f6432 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 3e3a43e..f615013 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 2f37aac..09b02e0 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 6727be3..fefcd76 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 47a754e..2294126 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 0b710e1..5681f7a 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 25149a7..4b2176b 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 14d0d3f..1835eec 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 8ffef12..4c5e3dc 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 7288cbb..4cd5f0c 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 6d7180d..ab809ad 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 7c34f12..91e0db3 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 93c9689..f3aed09 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 bec5f14..f920a25 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 ac39b56..a834fd3 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 c4c1867..c668d0d 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 cfbe0c0..67e776a 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 fdc05fe..8b5e8fe 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 4ee1bf4..8a9b125 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 47856c7..f456a1d 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 235c6bb..0627f7d 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 428c842..0f9f281 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 ebe76dc..3075763 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 f844fde..fb65924 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 76fecab..362aea0 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 8f50380..222ef58 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 ab306db..b76776d 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 3ccaf4f..8a21855 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 5d3be08..3cb44be 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 b5f35fb..1b50e85 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 078260d..031d83f 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 d0a0f72..1833939 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 2c72896..37c2f24 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 66696d8..68e1e8a 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 ff5c8e0..8a91553 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 63734dd..efcf233 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 97e3de6..18e5714 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 13f8d9c..eb66d26 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 4443ab3..6092dfa 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 48cf54e..edfbed2 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 3f93634..42516ba 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 a57bf54..048e306 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 c982949..3c2f76a 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 a78f6b1..888b5c9 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 a5aa959..1350976 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 502a017..ea8ff38 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 ca0e78a..37e573e 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 e2a1224..036d056 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 ea51b2b..d0658c0 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 25ad311..d9130ec 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 5d1fc87..60a8055 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 d9f1ebe..d72047e 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 58f4f4c..59a7b43 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 3e76c62..ab096d3 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 aec58de..5c285fa 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 1b83115..6058a37 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 97da792..6ff1d5d 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 cc737e7..18e7026 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 648a283..853a69f 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 ecbf4cd..e63cc58 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 dd5399e..789498b 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 86ac7ed..9699a95 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 e2a9cdd..a2876b5 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 640daa0..d2b2b31 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 94773f8..0be4af5 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 f1114e7..73218b9 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 d11e096..4b3fae0 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 a0ea6fc..cce4962 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 ffe08fe..05ba907 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 dd86e85..3e50ffc 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 45f804c..1110ede 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 7b3689a..9747153 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 3fa9c85..f88a92e 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 700ff44..8843766 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 9f1070e..7e96d5f 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 e360df0..73a3174 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 4f42927..9dec886 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 436b580..b56380f 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 677749f..df81f72 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 3069b83..262cd7a 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 eeb27c8..5438131 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 8065a3e..f5dc18f 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 5cb9566..d3a43de 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 2c9d393..3cae88e 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 d21ea0d..fafc625 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 948a08d..5e40d1b 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 61ab47a..9c72e3c 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 6cb3253..e9e4f2e 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 0a79679..a6808e1 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 12fa5e9..3d94196 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 f76f82a..7b67c2f 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 281ddda..9a99501 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 0b4ca04..6b0d1cf 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 d25bedc..27ab69e 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 f8a2280..edb01f7 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 62a24bc..ce1b492 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 361eb81..9cb9907 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 5bd6768..3aa20b7 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 a480926..a8d746f 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 7d67e8e..7e6e8ab 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 b88025d..e64baba 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 429f44e..0e23805 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 54efe4f..de6d759 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 a739508..cd6c10a 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 b16a6e0..73ad91c 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 b796b6a..fbc30fd 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 622f296..90a201a 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 c534a4b..f3a454c 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 3f03181..f64f24e 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 f930ce7..4101f40 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 0db1d71..dce9338 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 0ae27b1..3f26b49 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 5b7dcfa..4b8c2ef 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 a15bf59..368e073 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 cc30ad5..e1fd614 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 449d352..84afa14 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 12098c5..0ac8434 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 f4ed4ea..ea85994 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 ce34a5f..5b94fda 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 e5efdae..7f8a1f2 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 f690c80..5fac44f 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 81e6102..765efa2 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 18bfbb4..3e00c99 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 aa28c36..6fa89b5 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 a9c0f5b..5de5f4e 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 d446cf6..29f2d56 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 c86aa0f..d219ab8 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 d9a4273..2b96030 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 3c07faf..bbe1a2f 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 6fb75ec..1af4546 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 ad51677..f208b55 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 5c4d559..aaf4071 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 7d5afa9..ace4f9e 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 51c96fe..fedb653 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 f2f460b..3b62bba 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 b83bebb..f73d236 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 734e849..a3dcad2 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 a23ab7e..ef3b5fe 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 7a282a3..17e008f 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 2c748d0..a306b33 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 485bd18..557fcf4 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 5a601fe..8d6ae23 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 0ba4267..7e28985 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 d59c5e5..0413512 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 3e8437b..eb1699c 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 493f9f4..215a5a4 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 8ad0988..8f12411 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 d21fd0e..7d73059 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 e3a45c1..8a0dceb 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 e351eff..3b36443 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 ef35ada..73bba44 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 27f6c29..1337f64 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 ccb34e0..81fd66e 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 38e00dd..a0ca7dc 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 6ddf1db..3effe3a 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 ec99842..97b9917 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 a94e3a6..c125606 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 b8aa1e1..3ba2c9c 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 a3c36cf..f6ac7a2 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 efb17ad..5d28833 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 8fc6c03..dc90beb 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 c7459fa..953c7d9 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 2090a8d..6f383e7 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 e9c0683..4024eeb 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 956bc4d..685a06f 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 4fde005..421da92 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 584ba69..649899d 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 748a587..ecb2a80 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 77ca90e..e19dd82 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 0f1b9a7..5b59ae9 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 0441b72..12bbebd 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 5d2beac..724799d 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 96e8605..24ae90a 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 79f2da3..8891637 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 8bf69e2..a877641 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 be1a59b..f0f5e29 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 9bdd6b4..9e35a6e 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 d8c9cf3..f4721f1 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 dbe988e..19b88bb 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 ea79487..7ede172 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 1309322..4579ae2 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 4540605..d804bf5 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 f4048b8..fca8fbf 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 45aa9fb..4fdb629 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 2fcd7b3..2324908 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 18ae9e6..197c598 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 4c73dcb..43f5c16 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 dafcbb0..8214077 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 32ec6cc..28cec25 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 05da811..a048c9c 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 4527782..612e467 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 a8586ee..d6ef1f5 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 54bc6d1..55f1ea2 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 4060f51..dceb8d4 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 0b2ec51..e20e33f 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 7b0d510..a2e4eb8 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 0ae5367..8172224 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 55982b6..5885b27 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 1e446c8..22f4662 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 0db16f2..9eccbe5 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 f7982a4..7ced002 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 6d16b88..0a276c7 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 4ec1cce..c59ce81 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 df9e284..e9eb0d1 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 2d50aa0..8f72f4f 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 94cb5f0..5fcfd9e 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 bb771a6..df6bdd1 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 53b5530..69a3c74 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 17991b3..2c239f2 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 6ce569d..77e3895 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 a8e76cb..bb83653 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 9cc5171..878e117 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 0d36155..700a0dc 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 6b38170..411c781 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 9080dd0..b5774b4 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 e74447a..69c405b 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 070c460..871bcad 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 6f143a6..7f92df6 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 a584b4a..30c9e4f 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 ed3c077..db6531a 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 2e92ba2..7af6dd1 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 d9fc622..ee2f83f 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 c321277..a1c3d5d 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 0043f3c..42f14de 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 8a93ce9..27f35b6 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 ac19c2d..da588a4 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 635ccfa..ae8a07b 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 dccb76e..412d5fe 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 73d740e..fd285ed 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 1b49267..c74c7a7 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 d66de86..4a47598 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 52f30a8..ca88daf 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 2b1e644..465e11a 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 279dc2e..475564a 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 2314d9f..9db8e98 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 7a6f8d5..12b00cd 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 480fcf1..37ff954 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 6e05fed..f72eb69 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 e53f643..033be84 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 779766c..f48cc2f 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 cb6821c..4a113e2 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 a44d2e1..c3c5aef 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 f09e1a3..0f40d31 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 2c855eb..d107ff6 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 ff2e49a..d8b4f90 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 f95c3b9..cd05541 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 3598329..086a5b6 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 3249366..e926a23 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 5a410e3..c250baf 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 0857137..a2f991b 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 6f025da..aacba60 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 0be777d..a0b1b67 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 5ccdc02..4873b38 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 50ff6c0..e4bb3d7 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 78afd2c..4d6fad6 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 2141d1b..d646948 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 775d857..cd2027c 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 a2bbc5b..7dcd9f7 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 6a91df8..f9d2a6b 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 58ecebc..c18d728 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 3863e97..0044223 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 288939d..5f8023e 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 3a43419..4c4ede3 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 baeb7b1..990bff9 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 71af1e2..72b0103 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 fd7a6e2..627f204 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 75aec77..63ec09e 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 d881fdb..ff52801 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 029274e..3fc0730 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 7a68d8c..c961655 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 db52a0d..06ce893 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 0026ab1..a4f5a83 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 87d5bfe..66590dd 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 60b7abc..5445e2f 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 2f816aa..3a8e512 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 7773282..79dec71 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 45a633a..f511857 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 da64391..8809893 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 0eeaeec..d05d576 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 897a330..4f4cc0f 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 5b9b401..3a691f0 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 fa76d90..b095f9c 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 23d1ebc..84a5d62 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 3d3656b..6e6cdf4 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 14a9774..a795c98 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 553cc6e..9f02a38 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 4d2cfde..8ebca9a 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 f72b865..b065c3b 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 c5ea2dd..4fb1977 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 4fad011..2dc62a9 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 ab72e00..ad2c7e2 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 a3cf22a..260ffaf 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 36f592b..246033a 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 03325c8..069b0ba 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 e565a42..c55056b 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 445320f..c024df0 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 171c4c6..b9a69c7 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 ebd2d98..8661c68 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 67d500e..90bc937 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 00b77bc..ae329b3 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 162941f..8d73e2a 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 37dfd2a..1ae9332 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 176ad8f..983e540 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 1009ac8..711c23f 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 be243b4..11e9b49 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 9a262f1..4fa7870 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 217da23..caf627a 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 f389f63..b321f21 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 6d1645b..9575084 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 1311170..2125032 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 ca207b0..79394f1 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 86dc325..23686f7 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 c5aada5..8d81068 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 e007082..b3de20e 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 1c70b19..78898d7 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 3b23af4..72f543c 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 65e3966..e1b062e 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 ccd0a43..622e525 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 5b3e009..b5b0ea2 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 e649501..ac66041 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 abc5fe2..f1f17be 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 4dec37d..71683c9 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 57db9bb..6c4630f 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 854cae3..8151fb6 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 6283942..47ae002 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 73f61d9..5dbecd7 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 ec18497..f8a8ea5 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 4591862..94dcdec 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 cae7c04..4294502 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 514f9b0..a4c6036 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 9d85f43..504a06e 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 45b22d1..ebdd6ab 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 aa8ac45..2b05cff 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 0491543..cf3e845 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 c2151a2..ba2b624 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 1ee7330..950a9fb 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 c2ae15e..620a4e5 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 9a0a3eb..ed94152 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 cde47d9..1c90ba0 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 d17d19c..379a76d 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 af81a7f..8837f68 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 41d16a3..2ee1054 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 2654b92..e8638cc 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 9146473..621b28b 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 cf1b001..b6443f8 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 17a5bd9..b2c521a 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 8757fb1..bef3d7c 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 1dda2ee..161fd16 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 7baa800..aaa9839 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 2cee5aa..1459cdd 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 e9ab2c1..dc7c449 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 9f94d53..1100ab0 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 77174a5..ce1b877 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 207d7d3..0d9d147 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 908575c..148421e 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 d0d1292..030ceb5 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 c4d1c4e..11d897a 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 fc2c29f..f596e31 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 57a5d7f..d2cb0f2 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 cff291f..e232809 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 2b943e9..2480754 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 d25ffff..04fa05f 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 4db5a0e..7fbed7d 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 758ce6d..d4b4dde 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 74c1d2b..1602702 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 f8039e1..c1d6de3 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 a86877f..e1a4e26 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 5a1e68d..50a329b 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 999a667..ab3e93e 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 effcbbe..d9d38ca 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 f23fd2b..9f2dbd6 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 b9af846..f28bd12 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 5fb8824..2a48074 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 4b7d9ad..6fe6259 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 fea9346..ae95bc2 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 4e83e77..6b2c85a 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 6141cec..6976b53 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 9f6bda2..7d94640 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 d27fb53..92f8caf 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 b4d6405..d47427b 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 e1f5526..1e7679c 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 20240f8..10b4518 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 ba319c9..63ea27d 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 4a9e280..a3ba77d 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 d4f6546..feb6e40 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 4f7011c..ec6ce62 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 d2e416e..11ba64a 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 de1a1c0..6137dff 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 38c906b..d50b58b 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 da3cd5d..5a76a4c 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 f37868d..29f32f3 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 4b727dd..aae823b 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 08f5dc1..902cf6b 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 33665a1..ef8e394 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 b4c0e8c..63cca6c 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 698aabb..56da2d6 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 e1b35a1..f9519fd 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 ddaa706..22100cb 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 7b956c6..ff5eca4 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 4778f38..d67cb31 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 2d0720d..ee94954 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 9735eca..c880d4b 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 f50854a..918bf6a 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 ce86e8b..7fd4ef8 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 8aa5e8f..62159a8 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 f637998..21d5db7 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 c0a4b77..5b4e246 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 400cf7b..9f5585b 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 930e01f..d045646 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 b26265e..3f988be 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 06d3364..5da1fd4 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 be0ef9c..9d187a4 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 1b3f7b7..7546b7c 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 7cb1ac9..bec3da5 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 ecf7d46..7004cc8 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 2ebfaf0..66f8c39 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 36a9b0f..4445168 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 056647b..64d1bfb 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 35e9942..418fd8c 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 20ba9ba..a38c396 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 8d932d2..4d557ff 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 781669e..f3cfa40 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 c2a3bc9..b690973 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 4c3be3e..0ff4ad0 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 5847867..aa3537c 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 0e6254d..b54ab57 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 6a731d1..3e3c172 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 4d3f701..f087231 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 5bd2454..6855487 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 446ff97..8b185e7 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 b7b83f5..329d08e 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 ec474be..1855bf2 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 4239a5a..831daf7 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 4289c26..8b04b2d 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 2084740..0e50de2 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 e50f686..7213a4e 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 2e33772..370aef4 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 016fa96..8d12ebe 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 cc722ad..3571e61 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 c954661..ab7fc38 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 687897b..2fd96ff 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 0547aba..41aa227 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 136b78a..30fd19c 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 68a63e0..c0be3ad 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 d38227e..b02f891 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 6cb3b36..3ce2305 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 c282230..c81b1f6 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 173b13c..0c1e441 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 7c71a81..d7fcb26 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 03465b5..08fd865 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 bc521ef..3f8c7bf 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 41ac492..373200a 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 6f24b20..fc4963b 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 6255482..ba2b21f 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 0fd3e11..e6d2462 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 7df3172..771f42a 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 ff3769c..cc4fc65 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 a51efc8..435a7b9 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 fd3e3d2..c637732 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 0317018..b0fa88c 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 2bdf40b..4f7cb05 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 d9a8b8a..fe0ace1 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 3dc1ea0..0b76fe1 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 4210428..e94d395 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 c60bcad..37ac8b5 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 8a680cf..74fb17e 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 bfc3b9b..a0f8311 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 937d445..fefc17b 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 135191f..061cebb 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 8081be2..42f2d33 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 fbab54d..d7df173 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 9787965..da9708d 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 ed66b43..54da6d2 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 adcdb79..b77ca43 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 956d7d7..9ebeaac 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 72d88f5..bb52bb6 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 940a84d..75acdb0 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 4577ba8..81a8f84 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 9533fa0..c709d36 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 74e29fa..0fdbe29 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 c77b49a..4cc424d 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 841012e..d86193a 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 8320fa3..5684e65 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 eeb0666..37b4083 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 f4db0d9..3625517 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 78acca9..ec4ca85 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 9a424b4..c29a3d3 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 0193fc1..d63385a 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 7eaa1d6..59c3282 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 02c024a..3070ae7 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 40cd7f9..d54b05b 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 0147271..abf56d7 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 450c039..be1709a 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 2c056bf..b6d25a8 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 d115de5..35dc231 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 f0c6e28..04e60df 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 9b0a44d..2d7cd76 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 a1b4491..7b308fb 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 e746012..67cf643 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 2d3c57d..54bdf32 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 7fea98d..32336fe 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 136df51..dc39083 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 00a0c43..e6bce51 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 8f7b1d3..d0902a9 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 e980342..de5a4d5 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 c5f37cb..5e58b6f 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 d887596..8c6b23f 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 cdecd76..588ac40 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 ea72808..77970e7 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 899fe6e..e189cd1 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 bd3ca85..d8fbe06 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 bb6cad6..89148f1 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 e7cbe1d..0cd39d2 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 deee5ea..acf0f88 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 89190fa..ba6136b 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 be04f6c..1f17728 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 435b0ca..eaddfd3 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 2aaf1b7..0ffdcd3 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 f3f1c7e..99739e2 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 00ddb6e..12dee1b 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 b775c51..aa09cf9 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 5eccad2..510fd1e 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 2885494..c24689c 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 83c0e83..cdac089 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 b8a367a..a1a9721 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 6fdf5c6..0ec4145 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 8c1b63f..4393ef6 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 d8e00d0..a14dc21 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 f5b9b12..21b43ea 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 c050655..e6946b0 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 bfd07f9..bd3e0a2 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 9812eea..d9be4e9 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 e2ff195..c73602b 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 c1dcf86..6d16ea3 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 e61bc89..a0ae46a 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 b583473..d110e6a 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 b4b985e..04f349e 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 6981b2b..be3d55c 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 5d72bc9..0be83c9 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 5466a03..0a52738 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 6796924..628cea5 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 aa3d784..a646c97 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 f2845ef..908d406 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 b21d08f..486b21c 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 5b4a0cd..7bcfc84 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 4d891fa..4e176b4 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 2cc2c82..11063d8 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 3cd5062..bab140c 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 7a98d95..900d0dc 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 e244fe7..271da83 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 48641e6..bd494b6 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 a2e655a..b18443d 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 76ccea9..75eafd2 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 4430882..28252f8 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 0bb276b..c98fe7f 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 0b459a2..fc972fb 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 b945eff..e4bc449 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 aac29ca..2c7f81d 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 f6df656..f9d5570 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 98c4308..79a6935 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 8f50d8d..b86ff4b 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 a54a8a0..fbd5527 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 48c87ea..f401d0e 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 cf50892..cecc347 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 3f4f1d6..b5fb2d7 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 24391db..41cacc7 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 46b30af..a81be46 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 1dea4b5..c8177d6 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 882d0ac..ffad9c5 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 c311744..828b832 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 a18fa7d..8022c4d 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 ed35e28..c9405b7 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 f924c45..cb088d1 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 bf8c962..ca2a4cc 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 e02931c..840ced0 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 a7cd4e1..8d0375b 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 9a72002..01df29d 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 503fd32..f9da41a 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 f965125..cd8c707 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 355c9d2..a491820 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 098c7f5..937cb83 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 320c7fa..e63475f 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 2a36454..e73108d 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 e9f8655..90e1e1c 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 ccb621a..a6e7081 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 fc9fb7e..2954a6e 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 2fd7c71..8032b6f 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 204545d..d597f2c 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 7e9241d..6ddfda4 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 ade03ae..3f6eb56 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 ad242a2..335e87f 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 0c6dc18..25ae099 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 829cb73..025cf05 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 07eb96a..4afbff7 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 d757d24..8fc9521 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 c50d0df..b518e93 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 2be47b2..1120b8b 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 0de6bd4..fff5d09 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 46637f4..84a5df9 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 c25512b..53c3359 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 95e047a..2630cd1 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 eeffc28..a923db8 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 cce5a5b..d75ab66 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 be13deb..df2c9ca 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 8407d41..44dc513 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 d9b1421..fd48b03 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 57981c7..8d5c3e9 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 b2c6847..beb4e21 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 3cacb88..b6efd4a 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 7facea0..d2e3f98 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 8fe25d5..cdda0b7 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 26e2b61..15ab93c 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 1324bd2..7c80a77 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 9b0d95c..6a965bb 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 ef64e61..93a7987 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 59af9fe..5edebd5 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 3fa3330..bbc00b8 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 7c4a412..255da82 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 19b0411..be587b1 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 f391cdf..af0cff5 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 6987fd5..82936e9 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 e6abeb1..9149416 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 ec8cd9e..023eb77 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 4f29601..b91a9fb 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 e4ab4aa..1f18619 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 d3d6899..f6dab29 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 4f32bd1..a5c108f 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 041e7ba..dab4659 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 b67f810..3e7d2ac 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 a0127db..08f1c5d 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 808f73c..d186b37 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 092f7f7..8d02055 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 2295dd1..0e9d890 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 947ecea..e8eb437 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 7c37d2e..89e1dbf 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 cc94e68..c1febe4 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 3790c67..5a814ce 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 6b01824..b94e904 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 ff22425..204b1ff 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 3aa33ae..94b6121 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 5b8e424..2b63d91 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 a53016c..9f9fb96 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 3cae405..2662d36 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 464b925..a3c0a9b 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 ff26204..479c4ee 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 34f6afe..ed294c0 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 42f346a..2ec1130 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 730020e..d213051 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 2e16d90..b473c5f 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 4bae582..911fac4 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 0ad1c76..e2bfc5d 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 90e601a..093ca87 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 048ec2c..907b1da 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 84976f7..9ac0098 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 9149a02..2e8a9b7 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 affea8e..d76d3e2 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 1ba4191..2bb6658 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 cd438cb..19f966a 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 100755 new mode 100644 index 625ca84..552e976 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 100755 new mode 100644 index ef3a1ec..670f615 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 100755 new mode 100644 index a4742e2..cb6e23b 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 100755 new mode 100644 index 556d550..421ae03 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 100755 new mode 100644 index 74ed29d..d4683cb 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 100755 new mode 100644 index 92354cb..1fe5c25 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 100755 new mode 100644 index 344a2a8..0bc24a7 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 100755 new mode 100644 index 633e4b8..796943f 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 bcbd1d6..5efe456 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 100755 new mode 100644 index e5ef8f1..8cc53f5 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 100755 new mode 100644 index 32f30e4..a13e8c8 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 100755 new mode 100644 index 0f15f34..a420c86 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 100755 new mode 100644 index a01389a..f847827 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 100755 new mode 100644 index a3579c2..3804086 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 100755 new mode 100644 index 1eea80a..d8075a5 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 100755 new mode 100644 index 4ee9fe5..0eaf6b2 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 100755 new mode 100644 index c774992..96619d2 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 100755 new mode 100644 index 0df19c7..bf17f85 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 100755 new mode 100644 index 076a8bf..4f0390c 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 100755 new mode 100644 index d86ebc8..4c2c9da 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 100755 new mode 100644 index ab5ce8f..b7de459 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 100755 new mode 100644 index 0469f06..c3c2e2c 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 100755 new mode 100644 index ea8ce68..f3a88fd 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 100755 new mode 100644 index 5cc2e30..18d51e2 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 100755 new mode 100644 index 1cc8b45..32cf542 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 100755 new mode 100644 index c0c7aea..007e4d8 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 100755 new mode 100644 index 8fb0984..a9e83e9 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 100755 new mode 100644 index ce7ba52..06eb7a7 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 100755 new mode 100644 index 9b1a553..f9da237 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 100755 new mode 100644 index 639fa6c..09c3c6d 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 100755 new mode 100644 index 1d512df..5f07fd9 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 100755 new mode 100644 index 160b6b5..00997d1 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 100755 new mode 100644 index fcb1039..51d5ec4 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 100755 new mode 100644 index 504774e..b26d9a7 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 100755 new mode 100644 index be63ee1..3de1ee3 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 100755 new mode 100644 index 1f20419..d11daef 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 5041e30..0ae1406 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 100755 new mode 100644 index aed3d3b..6b71349 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 5e48942..d04d285 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 100755 new mode 100644 index da687bd..e08fe16 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 100755 new mode 100644 index a859792..5ff3986 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 100755 new mode 100644 index 242ec01..da989f3 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 100755 new mode 100644 index 3f2c62e..631d1fb 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 100755 new mode 100644 index 746d3d6..6f8d893 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 100755 new mode 100644 index 29c6d61..3fe300c 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 100755 new mode 100644 index f65c5bd..6a13412 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 100755 new mode 100644 index 8914414..a73f73b 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 100755 new mode 100644 index a118ff4..075ff39 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 100755 new mode 100644 index c7a3731..a90450c 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 100755 new mode 100644 index 8254790..45b4710 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 100755 new mode 100644 index 083f1d6..eef7f8a 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 100755 new mode 100644 index a63f7ea..4ac3d24 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 100755 new mode 100644 index 48e31ad..1c57fbf 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 100755 new mode 100644 index 5b1ad6c..6e234cc 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 100755 new mode 100644 index c8403dd..526d990 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 100755 new mode 100644 index ac4a977..4e202a6 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 100755 new mode 100644 index 582af36..9b3da9c 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 100755 new mode 100644 index e2993d3..72af9e3 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 100755 new mode 100644 index 5fbffcb..d10d036 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 100755 new mode 100644 index 5a04932..2134259 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 100755 new mode 100644 index 335c239..f49fb58 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 100755 new mode 100644 index 0caa0b1..d6b42d6 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 100755 new mode 100644 index 0c82efb..5ffe80e 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 100755 new mode 100644 index 8a3f7a1..50e4c7e 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 100755 new mode 100644 index 90a1195..b4f35cd 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 100755 new mode 100644 index 3a7311d..0cd6e96 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 100755 new mode 100644 index 13065ae..4d302a6 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 100755 new mode 100644 index c2de2d7..c804049 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 100755 new mode 100644 index 2e893fa..ebc5f34 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 d6d8711..50815ae 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 100755 new mode 100644 index cf50c75..e2cdcb7 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 100755 new mode 100644 index 14ec091..0c0af94 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 100755 new mode 100644 index cee9988..14a9d76 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 100755 new mode 100644 index ceaeb27..0b2c8e1 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 100755 new mode 100644 index 066bb24..c3fbeed 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 100755 new mode 100644 index cbceb80..b48a3f9 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 100755 new mode 100644 index 8332c4e..eaec4f3 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 100755 new mode 100644 index 0e0d434..14df032 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 ff701e1..032b04d 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 100755 new mode 100644 index 9ab57f5..96ddfd9 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 100755 new mode 100644 index 728d970..2f5475e 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 100755 new mode 100644 index 8332c4e..fddf8f6 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 100755 new mode 100644 index 4e2f896..57561cf 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 100755 new mode 100644 index e76797f..29a981a 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 100755 new mode 100644 index ef12a73..d0f4bca 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 100755 new mode 100644 index 0720b66..abf8f8f 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 100755 new mode 100644 index ea660b0..ff76a52 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 100755 new mode 100644 index dbb086d..88d2995 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 100755 new mode 100644 index ebe20a2..1051698 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 100755 new mode 100644 index 8651ade..0c856e4 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 100755 new mode 100644 index 7ef0bf5..a0d6575 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 100755 new mode 100644 index c43a70d..cec6821 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 100755 new mode 100644 index 92f37c0..da5f65b 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 100755 new mode 100644 index b37bcf0..9d3af7c 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 100755 new mode 100644 index 22cbe2f..eee94e9 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 100755 new mode 100644 index d5c380c..4ca283f 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 100755 new mode 100644 index a01389a..67c1149 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 100755 new mode 100644 index 96f8388..b1eb441 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 100755 new mode 100644 index 696b515..8cf6064 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 100755 new mode 100644 index 416052a..9e447d6 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 100755 new mode 100644 index 7baafe4..09361e4 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 100755 new mode 100644 index c6bc0fa..76e9fbd 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 100755 new mode 100644 index 26baa31..fd87d4b 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 100755 new mode 100644 index 2ca772d..b4d8f2d 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 100755 new mode 100644 index e4d7e81..f72030a 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 100755 new mode 100644 index 3e74b6a..0f338e8 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 100755 new mode 100644 index 878a351..97219ae 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 100755 new mode 100644 index c5fd136..f0b721c 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 100755 new mode 100644 index b8f6d0f..5236627 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 100755 new mode 100644 index 89692f7..a2c0f02 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 100755 new mode 100644 index 7be119e..37ae2ba 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 100755 new mode 100644 index 11bd497..97c0f1a 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 100755 new mode 100644 index 325fbad..7b5c019 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 100755 new mode 100644 index 51879ad..a6ae21e 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 100755 new mode 100644 index 0a818f6..0d09612 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 100755 new mode 100644 index 30f6bb1..1f272a5 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 100755 new mode 100644 index 2dcce4b..83b15b8 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 100755 new mode 100644 index 812b2f5..5d8863a 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 100755 new mode 100644 index febd5b4..6d48d10 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 100755 new mode 100644 index d3d509a..50dfa1e 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 100755 new mode 100644 index 9c0a78e..33b8144 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 100755 new mode 100644 index 96546da..66ae3a4 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 100755 new mode 100644 index 15c5f8e..823b285 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 100755 new mode 100644 index 45a8c88..aa8118a 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 100755 new mode 100644 index e28acd0..302427f 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 100755 new mode 100644 index d0d452b..55a5e5b 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 a47d065..291f1c5 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 100755 new mode 100644 index 6469909..5c0ec41 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 100755 new mode 100644 index 088aad6..d2bc667 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 100755 new mode 100644 index 89a5bc7..24db5a9 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 100755 new mode 100644 index 33fdef1..e4e7966 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 100755 new mode 100644 index c8ef0da..7c2bdd6 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 100755 new mode 100644 index 4cabba9..37544b4 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 100755 new mode 100644 index 49b6998..6bb32b0 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 100755 new mode 100644 index b163a9f..86c41fc 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 100755 new mode 100644 index f386770..e720d87 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 100755 new mode 100644 index 1aa830f..5666a75 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 100755 new mode 100644 index 4e92c18..1bc8b47 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 ac72535..7449387 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 100755 new mode 100644 index d2715b3..65e7f27 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 100755 new mode 100644 index fb523a8..67cc066 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 100755 new mode 100644 index db173aa..2e50b58 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 100755 new mode 100644 index 2cec8ba..47844ad 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 100755 new mode 100644 index f464f67..db89f01 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 100755 new mode 100644 index 9396355..c976ecd 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 100755 new mode 100644 index deb801d..cf8113c 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 100755 new mode 100644 index 298d588..013e183 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 100755 new mode 100644 index 010143b..1920168 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 100755 new mode 100644 index 319546b..06984ac 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 100755 new mode 100644 index d4cbb43..ab6f7fb 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 100755 new mode 100644 index 00af948..0d1f30c 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 100755 new mode 100644 index b7fdce1..e0191f7 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 100755 new mode 100644 index 5073d9e..44c2b5f 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 100755 new mode 100644 index 13886e9..675d2c2 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 100755 new mode 100644 index 5bc58ab..0c11c5a 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 100755 new mode 100644 index 9034cba..2757cf3 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 100755 new mode 100644 index 76405e0..e4ff602 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 100755 new mode 100644 index 63358c6..4bf47fd 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 100755 new mode 100644 index 2cad283..a4c6811 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 100755 new mode 100644 index d85f424..bc088df 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 100755 new mode 100644 index f9bcdda..a02fcb8 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 100755 new mode 100644 index 3eea2e0..cc46ceb 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 100755 new mode 100644 index 3969aaa..4171012 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 100755 new mode 100644 index fe44791..00df165 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 100755 new mode 100644 index 160b6b5..d76758b 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 100755 new mode 100644 index aeb058b..48b9d27 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 100755 new mode 100644 index 705fc33..10f1242 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 100755 new mode 100644 index c3ce4ae..abae7f1 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 100755 new mode 100644 index 10d6306..c92a8a9 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 100755 new mode 100644 index 2ffba7e..53b7a32 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 100755 new mode 100644 index 9b2ee9a..6b0c717 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 100755 new mode 100644 index 62a0497..2e9d19b 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 100755 new mode 100644 index 771a0f6..cac6edd 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 100755 new mode 100644 index 10d6233..0ee77b3 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 100755 new mode 100644 index b89e159..464cb77 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 100755 new mode 100644 index e9df70c..de39a39 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 100755 new mode 100644 index d413d01..ed09b83 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 100755 new mode 100644 index ba91d2c..507dd9f 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 100755 new mode 100644 index aa9344f..fb14070 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 100755 new mode 100644 index 82d9130..452991e 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 100755 new mode 100644 index f5f5477..ead1ff3 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 100755 new mode 100644 index ece7980..98dddc4 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 100755 new mode 100644 index 6178b25..e7b5f90 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 100755 new mode 100644 index cb8723c..ae83d82 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 100755 new mode 100644 index ed4c621..edea054 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 100755 new mode 100644 index 8332c4e..7a9a7fa 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 100755 new mode 100644 index 57e74a6..6d38ac7 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 9439a5b..178e8b4 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 100755 new mode 100644 index 47da421..6f73c01 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 100755 new mode 100644 index 5356491..33f99b9 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 100755 new mode 100644 index b4641c7..2057140 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 100755 new mode 100644 index a9937cc..7b61cab 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 100755 new mode 100644 index 39ee371..a222766 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 100755 new mode 100644 index a0e57b4..44ef46d 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 100755 new mode 100644 index eaab69e..d3a1f2b 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 100755 new mode 100644 index 1994653..995f965 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 100755 new mode 100644 index dd34d61..35f8df7 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 100755 new mode 100644 index 4b1d2a2..34f77a7 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 100755 new mode 100644 index bb1476f..0e218b6 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 100755 new mode 100644 index 160b6b5..eb91f75 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 100755 new mode 100644 index 7ccbc82..1d389f7 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 100755 new mode 100644 index 12d812d..4e620b3 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 100755 new mode 100644 index 3df2fdc..9b02225 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 100755 new mode 100644 index eabb71d..188e42a 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 100755 new mode 100644 index 4a1ea4b..f1a1dfc 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 100755 new mode 100644 index 5eff927..d6be029 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 100755 new mode 100644 index 2978557..0786db0 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 100755 new mode 100644 index 2498799..7b533d1 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 100755 new mode 100644 index f5ce30d..dfecd39 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 100755 new mode 100644 index 914ee86..4d4fb90 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 100755 new mode 100644 index 8fc1156..eaec510 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 100755 new mode 100644 index 667f21f..6236dfa 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 100755 new mode 100644 index 80529a4..8534274 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 100755 new mode 100644 index 3aa00ad..ad50b11 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 100755 new mode 100644 index dd8ba91..bb00577 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 100755 new mode 100644 index 617bf64..060d647 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 100755 new mode 100644 index 67b8c8c..050fd63 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 100755 new mode 100644 index 77da181..a4fc566 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 100755 new mode 100644 index 828020e..2981188 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 100755 new mode 100644 index 183cdd3..202faea 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 100755 new mode 100644 index f89b8ba..63949b1 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 100755 new mode 100644 index 1c551f7..ad5e1d5 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 100755 new mode 100644 index be32f77..58ee839 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 100755 new mode 100644 index 2a11c1e..e7d7502 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 100755 new mode 100644 index 28274c5..83720a3 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 100755 new mode 100644 index f31c654..3e751fd 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 100755 new mode 100644 index c00ff79..e1cde1b 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 100755 new mode 100644 index 09563a2..100319b 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 100755 new mode 100644 index 33f4aff..659f629 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 100755 new mode 100644 index c1dd965..2f425ad 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 100755 new mode 100644 index 10f451f..fae49a0 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 100755 new mode 100644 index 31d948a..dc42cd1 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 100755 new mode 100644 index fef5dc1..e2a6331 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 100755 new mode 100644 index b31eaf2..f6ac0a5 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 100755 new mode 100644 index 8fa17b0..d737c4b 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 100755 new mode 100644 index 00c90f9..629fe46 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 100755 new mode 100644 index 4156907..b250b1f 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 100755 new mode 100644 index ed26915..22623b0 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 100755 new mode 100644 index ec7cd48..76c3aa7 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 100755 new mode 100644 index b3397bc..c92506e 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 100755 new mode 100644 index e0d7cee..bc0200b 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 100755 new mode 100644 index 9f95587..879d578 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 100755 new mode 100644 index c169508..3f3e7d7 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 100755 new mode 100644 index d95f396..a5c49a7 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 100755 new mode 100644 index 468dfad..9dcf729 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 100755 new mode 100644 index c298f37..6170745 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 100755 new mode 100644 index 57c58e2..ad4d0eb 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 100755 new mode 100644 index c25b07b..38d8a3c 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 100755 new mode 100644 index 53c9725..e8e51b7 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 12a51f5..2659bf2 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 f345319..6ecbb1e 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 a68d15d..9b9ebc0 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 4593e9a..b7fe238 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 3f84805..21f37ca 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 cc04aa2..da19990 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 b223eb3..891ed33 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 3e04321..3dea030 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 bf422fb..fea2481 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 2ea2ca3..7b22495 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 a0483e9..140a259 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 0388ab2..c01efdd 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 ca53961..5463fda 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 c54c5d0..2c75727 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 aa0e9eb..b557c50 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 ed79b59..b35bf24 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 32a3e62..25d3d7f 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 5ba0bea..4764d69 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 c04bedd..7598896 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 801baab..9d42add 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 99487f5..03d421f 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 352e102..0fc9cf1 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 7389846..6c07260 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 3fef724..744a2a0 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 566d43f..cce0cad 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 8a265ea..2c7e60d 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 eb634bc..33bcdf1 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 3cafa05..0a33174 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 b9395c3..1b5c55c 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 8e137f6..83e0850 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 3f08d94..a431ca7 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 11ea087..4d003ae 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 840d743..bf6c37e 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 c093a9a..5bca721 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 f8a2672..5e00805 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 dff4463..0d17073 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 0391856..604e681 100644 --- a/toxygen/smileys/smileys.py +++ b/toxygen/smileys/smileys.py @@ -1,9 +1,18 @@ -from utils import util +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + 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: """ @@ -31,7 +40,7 @@ class SmileyLoader: self._smileys = json.loads(fl.read()) fl.seek(0) tmp = json.loads(fl.read(), object_pairs_hook=OrderedDict) - print('Smiley pack {} loaded'.format(pack_name)) + LOG.info('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) @@ -42,7 +51,7 @@ class SmileyLoader: except Exception as ex: self._smileys = {} self._list = [] - print('Smiley pack {} was not loaded. Error: {}'.format(pack_name, ex)) + LOG.error('Smiley pack {} was not loaded. Error: {}'.format(pack_name, str(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 0a0a482..1f8a4d5 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 88789dc..1c234c5 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 a37df94..be5adea 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 669dd36..8f5a5f6 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 54afd60..5cc2fd1 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 e536a7e..57e8ca1 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 6c787c1..de36348 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 a0b01e4..66c1409 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 383e730..3ee623c 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 39143ec..732d6a3 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 8ee5ff0..226f31d 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 68f8717..d1f08ed 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 5275e7d..dc731be 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 802c83a..b83ebbe 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 f8ff638..6bc3173 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 3550eb8..3f3a39d 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 2e40729..885087d 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 6109783..0380c2e 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 9d5f86f..080cc02 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 e114b3f..f0f0f9d 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 484d18f..c556767 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 d6f8024..3c109d9 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 fa3747d..2770ab3 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 e04a07c..38223e9 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 dcc34cb..79beb89 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 5014c1e..a4a4cfe 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 866ac05..04cefdb 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 b10b914..8c22f1e 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 dd5a292..50d0edc 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 e2a633a..e48036b 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 59d0b52..f750184 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 14142c7..56a0a29 100644 --- a/toxygen/stickers/stickers.py +++ b/toxygen/stickers/stickers.py @@ -1,7 +1,8 @@ +# -*- 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 100755 new mode 100644 index 5d1e0eb..3a38a70 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 100755 new mode 100644 index 3185319..cf7fb77 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 100755 new mode 100644 index 977c5fc..afb2d2d 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 100755 new mode 100644 index cf1932c..038d833 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 100755 new mode 100644 index 745b597..bee4a90 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 100755 new mode 100644 index cead99e..4b55192 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 100755 new mode 100644 index 7f183c8..58840be 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 100755 new mode 100644 index 512edce..c3b4762 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 100755 new mode 100644 index d9dc156..5de9a34 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 100755 new mode 100644 index d081e9b..9020fe7 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 100755 new mode 100644 index d652159..7c20500 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 100755 new mode 100644 index ec372b2..f41f80c 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 100755 new mode 100644 index 66f8e1a..efb6068 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 100755 new mode 100644 index e09ce02..1539bc9 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 100755 new mode 100644 index e09ce02..1539bc9 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 100755 new mode 100644 index e09ce02..1539bc9 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 100755 new mode 100644 index 41024f7..15e221b 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 100755 new mode 100644 index abdc01d..bc26933 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 100755 new mode 100644 index a9a16f7..7c00620 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 100755 new mode 100644 index 30deeb5..30631ba 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 100755 new mode 100644 index 30deeb5..30631ba 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 100755 new mode 100644 index 30deeb5..30631ba 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 100755 new mode 100644 index 657943a..f8fbb31 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 100755 new mode 100644 index 937d005..7c644b6 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 100755 new mode 100644 index bc0f576..b3e51a0 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 100755 new mode 100644 index e271f7f..ff4a62b 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 100755 new mode 100644 index 5805d98..388339c 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 100755 new mode 100644 index f808d2d..f0c00ea 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 100755 new mode 100644 index f5b9af8..570a940 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 100755 new mode 100644 index 14b1cb1..c6aacda 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 100755 new mode 100644 index 14b1cb1..c6aacda 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 100755 new mode 100644 index 14b1cb1..c6aacda 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 100755 new mode 100644 index 27af811..c0565b5 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 100755 new mode 100644 index 27af811..c0565b5 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 100755 new mode 100644 index 27af811..c0565b5 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 100755 new mode 100644 index 9b0a4e6..75e5b5a 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 100755 new mode 100644 index 5c0bee4..31f4831 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 100755 new mode 100644 index 350583a..09473be 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 100755 new mode 100644 index cb5d3b5..5569ee6 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 100755 new mode 100644 index 6271140..57fe30d 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 100755 new mode 100644 index 87536cc..253cacb 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 100755 new mode 100644 index 483df25..cf1c4f6 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 100755 new mode 100644 index 88691d7..4a7b0c8 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 100755 new mode 100644 index abcc724..0cc7d6d 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 100755 new mode 100644 index b9c8e3b..99c6b67 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 6e05c3e..dca54d8 100644 --- a/toxygen/styles/style.py +++ b/toxygen/styles/style.py @@ -1,17 +1,14 @@ -# -*- coding: utf-8 -*- +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -try: - from PyQt5 import QtCore -except ImportError: - from PyQt4 import QtCore +from qtpy 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(): +def qInitResources() -> None: QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) -def qCleanupResources(): +def qCleanupResources() -> None: 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 new file mode 100644 index 0000000..b2c475f --- /dev/null +++ b/toxygen/tests/README.txt @@ -0,0 +1 @@ +unused diff --git a/toxygen/tests/__init__.py b/toxygen/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/tests/conference_tests.py.bak b/toxygen/tests/conference_tests.py.bak new file mode 100644 index 0000000..8da5912 --- /dev/null +++ b/toxygen/tests/conference_tests.py.bak @@ -0,0 +1,151 @@ +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 new file mode 100644 index 0000000..f9f730e --- /dev/null +++ b/toxygen/tests/socks.py @@ -0,0 +1,393 @@ +# -*- 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 new file mode 100644 index 0000000..584987a --- /dev/null +++ b/toxygen/tests/test_gdb.py @@ -0,0 +1,938 @@ +# -*- 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 new file mode 100644 index 0000000..5f2cb10 --- /dev/null +++ b/toxygen/tests/test_gdb.urls @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..1551557 --- /dev/null +++ b/toxygen/tests/tests_socks.py @@ -0,0 +1,1885 @@ +# -*- 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 new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/third_party/qweechat/data/icons/README b/toxygen/third_party/qweechat/data/icons/README new file mode 100644 index 0000000..0694819 --- /dev/null +++ b/toxygen/third_party/qweechat/data/icons/README @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000..dd76354 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/application-exit.png 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 new file mode 100644 index 0000000..ea80953 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/bullet_green_8x8.png 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 new file mode 100644 index 0000000..58ad5cf Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/bullet_yellow_8x8.png differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-close.png b/toxygen/third_party/qweechat/data/icons/dialog-close.png new file mode 100644 index 0000000..2c2f99e Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/dialog-close.png 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 new file mode 100644 index 0000000..f1d290c Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/dialog-ok-apply.png differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-password.png b/toxygen/third_party/qweechat/data/icons/dialog-password.png new file mode 100644 index 0000000..2151029 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/dialog-password.png differ diff --git a/toxygen/third_party/qweechat/data/icons/dialog-warning.png b/toxygen/third_party/qweechat/data/icons/dialog-warning.png new file mode 100644 index 0000000..43ca31a Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/dialog-warning.png differ diff --git a/toxygen/third_party/qweechat/data/icons/document-save.png b/toxygen/third_party/qweechat/data/icons/document-save.png new file mode 100644 index 0000000..7fa489c Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/document-save.png differ diff --git a/toxygen/third_party/qweechat/data/icons/edit-find.png b/toxygen/third_party/qweechat/data/icons/edit-find.png new file mode 100644 index 0000000..9b3fe6b Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/edit-find.png differ diff --git a/toxygen/third_party/qweechat/data/icons/help-about.png b/toxygen/third_party/qweechat/data/icons/help-about.png new file mode 100644 index 0000000..ee59e17 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/help-about.png differ diff --git a/toxygen/third_party/qweechat/data/icons/network-connect.png b/toxygen/third_party/qweechat/data/icons/network-connect.png new file mode 100644 index 0000000..4e32020 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/network-connect.png differ diff --git a/toxygen/third_party/qweechat/data/icons/network-disconnect.png b/toxygen/third_party/qweechat/data/icons/network-disconnect.png new file mode 100644 index 0000000..623c8e0 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/network-disconnect.png differ diff --git a/toxygen/third_party/qweechat/data/icons/preferences-other.png b/toxygen/third_party/qweechat/data/icons/preferences-other.png new file mode 100644 index 0000000..711881e Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/preferences-other.png differ diff --git a/toxygen/third_party/qweechat/data/icons/weechat.png b/toxygen/third_party/qweechat/data/icons/weechat.png new file mode 100644 index 0000000..7eca5c8 Binary files /dev/null and b/toxygen/third_party/qweechat/data/icons/weechat.png differ diff --git a/toxygen/third_party/qweechat/weechat/__init__.py b/toxygen/third_party/qweechat/weechat/__init__.py new file mode 100644 index 0000000..f510618 --- /dev/null +++ b/toxygen/third_party/qweechat/weechat/__init__.py @@ -0,0 +1,19 @@ +# -*- 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 new file mode 100644 index 0000000..0ed52ef --- /dev/null +++ b/toxygen/third_party/qweechat/weechat/color.py @@ -0,0 +1,201 @@ +# -*- 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 new file mode 100644 index 0000000..90ce7d2 --- /dev/null +++ b/toxygen/third_party/qweechat/weechat/protocol.py @@ -0,0 +1,361 @@ +# -*- 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 new file mode 100644 index 0000000..2afabd9 --- /dev/null +++ b/toxygen/third_party/qweechat/weechat/testproto.py @@ -0,0 +1,252 @@ +# -*- 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 e5773a8..e750231 100644 --- a/toxygen/ui/av_widgets.py +++ b/toxygen/ui/av_widgets.py @@ -1,9 +1,18 @@ -from PyQt5 import QtCore, QtGui, QtWidgets -from ui import widgets -import utils.util as util -import pyaudio +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import logging +import threading import wave +from qtpy import QtCore, QtGui, QtWidgets + +from ui import widgets +import utils.util as util +import toxygen_wrapper.tests.support_testing as ts +with ts.ignoreStderr(): + import pyaudio + +global LOG +LOG = logging.getLogger('app.'+__name__) class IncomingCallWidget(widgets.CenteredWidget): @@ -11,7 +20,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)) @@ -45,9 +54,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) @@ -56,75 +65,118 @@ class IncomingCallWidget(widgets.CenteredWidget): self.accept_video.clicked.connect(self.accept_call_with_video) self.decline.clicked.connect(self.decline_call) - 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() + output_device_index = self._settings._oArgs.audio['output'] if self._settings['calls_sound']: + class SoundPlay(QtCore.QThread): + + def __init__(self): + QtCore.QThread.__init__(self) + self.a = None + + def run(self): + class AudioFile: + chunk = 1024 + + def __init__(self, fl): + self.stop = False + self.fl = fl + self.wf = wave.open(self.fl, 'rb') + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wf.getsampwidth()), + channels=self.wf.getnchannels(), + rate=self.wf.getframerate(), + # why no device? + output_device_index=output_device_index, + output=True) + + def play(self): + while not self.stop: + data = self.wf.readframes(self.chunk) + # dunno + if not data: break + while data and not self.stop: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + self.wf = wave.open(self.fl, 'rb') + + def close(self): + try: + self.stream.close() + self.p.terminate() + except Exception as e: + # malloc_consolidate(): unaligned fastbin chunk detected + LOG.warn("SoundPlay close exception {e}") + + self.a = AudioFile(util.join_path(util.get_sounds_directory(), 'call.wav')) + self.a.play() + self.a.close() + self.thread = SoundPlay() self.thread.start() else: self.thread = None def stop(self): + LOG.debug(f"stop from friend_number={self._friend_number}") + if self._processing: + self.close() if self.thread is not None: self.thread.a.stop = True - self.thread.wait() - self.close() + i = 0 + while i < ts.iTHREAD_JOINS: + self.thread.wait(ts.iTHREAD_TIMEOUT) + if not self.thread.isRunning(): break + i = i + 1 + else: + LOG.warn(f"stop {self.thread.a} BLOCKED") + self.thread.a.stream.close() + self.thread.a.p.terminate() + self.thread.a.close() + # dunno -failsafe + self.thread.terminate() + #? dunno + self._processing = False def accept_call_with_audio(self): if self._processing: + LOG.warn(f" accept_call_with_audio from {self._friend_number}") return + LOG.debug(f" accept_call_with_audio from {self._friend_number}") self._processing = True - self._calls_manager.accept_call(self._friend_number, True, False) - self.stop() + try: + self._calls_manager.accept_call(self._friend_number, True, False) + finally: + #? self.stop() + LOG.debug(f" accept_call_with_audio NOT stop from={self._friend_number}") + pass def accept_call_with_video(self): + # ts.trepan_handler() + if self._processing: + LOG.warn(f" accept_call_with_video from {self._friend_number}") return + self.setWindowTitle('Answering video call') self._processing = True - self._calls_manager.accept_call(self._friend_number, True, True) - self.stop() + LOG.debug(f" accept_call_with_video from {self._friend_number}") + try: + self._calls_manager.accept_call(self._friend_number, True, True) + finally: + self.stop() def decline_call(self): + LOG.debug(f"decline_call from {self._friend_number}") if self._processing: return self._processing = True - self._calls_manager.stop_call(self._friend_number, False) - self.stop() + try: + self._calls_manager.stop_call(self._friend_number, False) + except Exception as e: + LOG.warn(f"decline_call from {self._friend_number} {e}") + finally: + self.stop() def set_pixmap(self, pixmap): self.avatar_label.setPixmap(pixmap) diff --git a/toxygen/ui/contact_items.py b/toxygen/ui/contact_items.py index 7a32284..bdff447 100644 --- a/toxygen/ui/contact_items.py +++ b/toxygen/ui/contact_items.py @@ -1,5 +1,7 @@ -from wrapper.toxcore_enums_and_consts import * -from PyQt5 import QtCore, QtGui, QtWidgets +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtGui, QtWidgets +from toxygen_wrapper.toxcore_enums_and_consts import * + from utils.util import * from ui.widgets import DataLabel @@ -9,7 +11,7 @@ class ContactItem(QtWidgets.QWidget): Contact in friends list """ - def __init__(self, settings, parent=None): + def __init__(self, settings, parent=None, kind='friend'): QtWidgets.QWidget.__init__(self, parent) mode = settings['compact_mode'] self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) @@ -30,6 +32,11 @@ 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 512c141..f507a1d 100644 --- a/toxygen/ui/create_profile_screen.py +++ b/toxygen/ui/create_profile_screen.py @@ -1,9 +1,11 @@ +# -*- 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 b2758c7..60f1e9e 100644 --- a/toxygen/ui/group_bans_widgets.py +++ b/toxygen/ui/group_bans_widgets.py @@ -1,9 +1,11 @@ +# -*- 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): @@ -22,15 +24,20 @@ 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): - self._cancel_ban(self._ban.ban_id) + def cancel_ban(self): # pylint: disable=method-hidden + # FixMe broken + # self._cancel_ban(self._ban.ban_id) + pass + def can_cancel_ban(self): # pylint: disable=method-hidden + # FixMe missing + pass class GroupBansScreen(CenteredWidget): @@ -48,7 +55,8 @@ class GroupBansScreen(CenteredWidget): self._refresh_bans_list() def _retranslate_ui(self): - self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) +# self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) + pass 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 d35aca1..6b9fa9e 100644 --- a/toxygen/ui/group_invites_widgets.py +++ b/toxygen/ui/group_invites_widgets.py @@ -1,7 +1,13 @@ -from PyQt5 import uic, QtWidgets +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import logging +from qtpy import uic, QtWidgets + import utils.util as util from ui.widgets import * +global LOG +LOG = logging.getLogger('app') class GroupInviteItem(QtWidgets.QWidget): @@ -27,6 +33,7 @@ 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) @@ -68,8 +75,11 @@ 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() @@ -78,6 +88,7 @@ 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() @@ -90,7 +101,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.is_selected(): + if item_widget and 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 9d2632d..ec8f95d 100644 --- a/toxygen/ui/group_peers_list.py +++ b/toxygen/ui/group_peers_list.py @@ -1,6 +1,7 @@ -from ui.widgets import * -from wrapper.toxcore_enums_and_consts import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from ui.widgets import * +from toxygen_wrapper.toxcore_enums_and_consts import * class PeerItem(QtWidgets.QWidget): @@ -11,12 +12,15 @@ class PeerItem(QtWidgets.QWidget): self.nameLabel.setGeometry(5, 0, width - 5, 34) name = peer.name if peer.is_current_user: - name += util_ui.tr(' (You)') + name += util_ui.tr(' *') self.nameLabel.setText(name) if peer.status == TOX_USER_STATUS['NONE']: - style = 'QLabel {color: green}' + if peer.is_current_user: + style = 'QLabel {color: magenta}' + else: + style = 'QLabel {color: green}' elif peer.status == TOX_USER_STATUS['AWAY']: - style = 'QLabel {color: yellow}' + style = 'QLabel {color: blue}' 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 c32168b..5fd04d4 100644 --- a/toxygen/ui/group_settings_widgets.py +++ b/toxygen/ui/group_settings_widgets.py @@ -1,9 +1,8 @@ from ui.widgets import CenteredWidget -from PyQt5 import uic +from qtpy import uic import utils.util as util import utils.ui as util_ui - class GroupManagementScreen(CenteredWidget): def __init__(self, groups_service, group): @@ -21,6 +20,7 @@ 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,12 +28,21 @@ 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 ad4b703..ceda0ef 100644 --- a/toxygen/ui/groups_widgets.py +++ b/toxygen/ui/groups_widgets.py @@ -1,8 +1,10 @@ -from PyQt5 import uic +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import uic + import utils.util as util from ui.widgets import * -from wrapper.toxcore_enums_and_consts import * - +from toxygen_wrapper.toxcore_enums_and_consts import * class BaseGroupScreen(CenteredWidget): diff --git a/toxygen/ui/items_factories.py b/toxygen/ui/items_factories.py index 7346f8f..530839c 100644 --- a/toxygen/ui/items_factories.py +++ b/toxygen/ui/items_factories.py @@ -1,7 +1,8 @@ +# -*- 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): @@ -45,9 +46,29 @@ 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() - item = InlineImageItem(message.data, self._messages.width(), elem, self._messages) + # AttributeError: 'bytes' object has no attribute 'data' + if type(message) == bytes: + # was used + data = message + elif hasattr(message, 'data'): + # used + data = message.data + else: + # unreached + return None + item = InlineImageItem(data, self._messages.width(), elem, self._messages) elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) if append: self._messages.addItem(elem) @@ -81,9 +102,7 @@ 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 35e33b5..93362fd 100644 --- a/toxygen/ui/login_screen.py +++ b/toxygen/ui/login_screen.py @@ -1,9 +1,12 @@ -from ui.widgets import * -from PyQt5 import uic -import utils.util as util -import utils.ui as util_ui +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import os.path +from qtpy import uic + +from ui.widgets import * +import utils.util as util +import utils.ui as util_ui class LoginScreenResult: diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py index 5a510a5..f65a0af 100644 --- a/toxygen/ui/main_screen.py +++ b/toxygen/ui/main_screen.py @@ -1,31 +1,187 @@ +# -*- 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 PyQt5 import uic +from user_data.settings import Settings +import logging +global LOG +LOG = logging.getLogger('app.'+'mains') + +iMAX = 70 + +try: + # https://github.com/pyqtconsole/pyqtconsole + from pyqtconsole.console import PythonConsole + import pyqtconsole.highlighter as hl +except Exception as e: + LOG.warn(e) + PythonConsole = None +else: + if True: + # I want to do reverse video but I cant figure how + bg='white' + def hl_format(color, style=''): + """Return a QTextCharFormat with the given attributes. + """ + _color = QColor() + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + _bgcolor = QColor() + _bgcolor.setNamedColor(bg) + _format.setBackground(_bgcolor) + return _format + + aFORMATS = { + 'keyword': hl_format('blue', 'bold'), + 'operator': hl_format('red'), + 'brace': hl_format('darkGray'), + 'defclass': hl_format('black', 'bold'), + 'string': hl_format('magenta'), + 'string2': hl_format('darkMagenta'), + 'comment': hl_format('darkGreen', 'italic'), + 'self': hl_format('black', 'italic'), + 'numbers': hl_format('brown'), + 'inprompt': hl_format('darkBlue', 'bold'), + 'outprompt': hl_format('darkRed', 'bold'), + } + else: + bg = 'black' + def hl_format(color, style=''): + + """Return a QTextCharFormat with the given attributes. + unused + """ + _color = QColor() + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + _bgcolor = QColor() + _bgcolor.setNamedColor(bg) + _format.setBackground(_bgcolor) + return _format + aFORMATS = { + 'keyword': hl_format('blue', 'bold'), + 'operator': hl_format('red'), + 'brace': hl_format('lightGray'), + 'defclass': hl_format('white', 'bold'), + 'string': hl_format('magenta'), + 'string2': hl_format('lightMagenta'), + 'comment': hl_format('lightGreen', 'italic'), + 'self': hl_format('white', 'italic'), + 'numbers': hl_format('lightBrown'), + 'inprompt': hl_format('lightBlue', 'bold'), + 'outprompt': hl_format('lightRed', 'bold'), + } + +class QTextEditLogger(logging.Handler): + def __init__(self, parent, app): + super().__init__() + self.widget = QtWidgets.QPlainTextEdit(parent) + self.widget.setReadOnly(True) + + if app and app._settings: + size = app._settings['message_font_size'] + font_name = app._settings['font'] + else: + size = 12 + font_name = "Courier New" + font = QtGui.QFont(font_name, size, QtGui.QFont.Bold) + self.widget.setFont(font) + + def emit(self, record): + msg = self.format(record) + self.widget.appendPlainText(msg) + + +class LogDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit): + + def __init__(self, parent=None, app=None): + global iMAX + super().__init__(parent) + + logTextBox = QTextEditLogger(self, app) + # You can format what is printed to text box - %(levelname)s + logTextBox.setFormatter(logging.Formatter('%(name)s %(asctime)-4s - %(message)s')) + logTextBox.setLevel(app._args.loglevel) + logging.getLogger().addHandler(logTextBox) + + self._button = QtWidgets.QPushButton(self) + self._button.setText('Copy All') + self._logTextBox = logTextBox + + layout = QtWidgets.QVBoxLayout() + # Add the new logging box widget to the layout + layout.addWidget(logTextBox.widget) + layout.addWidget(self._button) + self.setLayout(layout) + settings = Settings.get_default_settings(app._args) + #self.setBaseSize( + self.resize(min(iMAX * settings['message_font_size'], parent.width()), 350) + + # Connect signal to slot + self._button.clicked.connect(self.test) + + def test(self): + # FixMe: 65:8: E1101: Instance of 'QTextEditLogger' has no 'selectAll' member (no-member) + # :66:8: E1101: Instance of 'QTextEditLogger' has no 'copy' member (no-member) + if hasattr(self._logTextBox, 'selectAll'): + self._logTextBox.selectAll() + self._logTextBox.copy() class MainWindow(QtWidgets.QMainWindow): - def __init__(self, settings, tray): + def __init__(self, settings, tray, app): super().__init__() self._settings = settings self._contacts_manager = None self._tray = tray + self._app = app + self._tox = app._tox self._widget_factory = None self._modal_window = None self._plugins_loader = None self.setAcceptDrops(True) self._saved = False self._smiley_window = None - self._profile = self._toxes = self._messenger = None + self._profile = None + self._toxes = None + self._messenger = None self._file_transfer_handler = self._history_loader = self._groups_service = self._calls_manager = None self._should_show_group_peers_list = False self.initUI() + global iMAX + if iMAX == 100: + # take a rough guess of 2/3 the default width at the default font + iMAX = settings['width'] * 2/3 / settings['message_font_size'] + self._me = LogDialog(self, app) + self._pe = None + self._we = None def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader, - file_transfer_handler, history_loader, calls_manager, groups_service, toxes): + file_transfer_handler, history_loader, calls_manager, groups_service, toxes, app): self._widget_factory = widget_factory self._tray = tray self._contacts_manager = contacts_manager @@ -36,6 +192,7 @@ 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) @@ -51,11 +208,19 @@ class MainWindow(QtWidgets.QMainWindow): def setup_menu(self, window): self.menubar = QtWidgets.QMenuBar(window) self.menubar.setObjectName("menubar") - self.menubar.setNativeMenuBar(False) - self.menubar.setMinimumSize(self.width(), 25) - self.menubar.setMaximumSize(self.width(), 25) - self.menubar.setBaseSize(self.width(), 25) - self.menuProfile = QtWidgets.QMenu(self.menubar) + self.menubar.setNativeMenuBar(True) # was False + self.menubar.setMinimumSize(self.width(), 250) + self.menubar.setMaximumSize(self.width(), 32) + self.menubar.setBaseSize(self.width(), 250) + + self.actionTest_tox = QtWidgets.QAction(window) + self.actionTest_tox.setObjectName("actionTest_tox") + self.actionTest_nmap = QtWidgets.QAction(window) + self.actionTest_nmap.setObjectName("actionTest_nmap") + self.actionTest_main = QtWidgets.QAction(window) + self.actionTest_main.setObjectName("actionTest_main") + self.actionQuit_program = QtWidgets.QAction(window) + self.actionQuit_program.setObjectName("actionQuit_program") self.menuProfile = QtWidgets.QMenu(self.menubar) self.menuProfile.setObjectName("menuProfile") @@ -64,13 +229,14 @@ class MainWindow(QtWidgets.QMainWindow): self.menuSettings.setObjectName("menuSettings") self.menuPlugins = QtWidgets.QMenu(self.menubar) self.menuPlugins.setObjectName("menuPlugins") - self.menuAbout = QtWidgets.QMenu(self.menubar) + self.menuAbout = QtWidgets.QMenu(self.menubar) # alignment=QtCore.Qt.AlignRight self.menuAbout.setObjectName("menuAbout") self.actionAdd_friend = QtWidgets.QAction(window) self.actionAdd_friend.setObjectName("actionAdd_friend") - self.actionprofilesettings = QtWidgets.QAction(window) - self.actionprofilesettings.setObjectName("actionprofilesettings") + + self.actionProfile_settings = QtWidgets.QAction(window) + self.actionProfile_settings.setObjectName("actionProfile_settings") self.actionPrivacy_settings = QtWidgets.QAction(window) self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") self.actionInterface_settings = QtWidgets.QAction(window) @@ -81,6 +247,13 @@ 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") @@ -89,6 +262,8 @@ 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) @@ -97,19 +272,30 @@ 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()) @@ -118,22 +304,31 @@ 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.actionSettings.triggered.connect(self.profile_settings) + self.actionProfile_settings.triggered.connect(self.profile_settings) self.actionPrivacy_settings.triggered.connect(self.privacy_settings) self.actionInterface_settings.triggered.connect(self.interface_settings) self.actionNotifications.triggered.connect(self.notification_settings) self.audioSettings.triggered.connect(self.audio_settings) self.videoSettings.triggered.connect(self.video_settings) - self.updateSettings.triggered.connect(self.update_settings) +## self.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): @@ -141,10 +336,17 @@ class MainWindow(QtWidgets.QMainWindow): def event(self, event): if event.type() == QtCore.QEvent.WindowActivate: - self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) + if hasattr(self, '_tray') and self._tray: + self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) self.messages.repaint() return super().event(event) + def status(self, line): + """For now, this uses the unused space on the menubar line + It could be a status line at the bottom, or a statusline with history.""" + self.menuAbout.setTitle(line[:iMAX]) + return line + def retranslateUi(self): self.lockApp.setText(util_ui.tr("Lock")) self.menuPlugins.setTitle(util_ui.tr("Plugins")) @@ -157,18 +359,26 @@ 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.actionprofilesettings.setText(util_ui.tr("Profile")) + self.actionProfile_settings.setText(util_ui.tr("Profile")) self.actionPrivacy_settings.setText(util_ui.tr("Privacy")) self.actionInterface_settings.setText(util_ui.tr("Interface")) self.actionNotifications.setText(util_ui.tr("Notifications")) self.actionNetwork.setText(util_ui.tr("Network")) self.actionAbout_program.setText(util_ui.tr("About program")) + self.actionLog_console.setText(util_ui.tr("Console Log")) + self.actionPython_console.setText(util_ui.tr("Python Console")) + self.actionWeechat_console.setText(util_ui.tr("Weechat Console")) + self.actionTest_tox.setText(util_ui.tr("Bootstrap")) + self.actionTest_nmap.setText(util_ui.tr("Test Nodes")) + self.actionTest_main.setText(util_ui.tr("Test Program")) + self.actionQuit_program.setText(util_ui.tr("Quit program")) self.actionSettings.setText(util_ui.tr("Settings")) self.audioSettings.setText(util_ui.tr("Audio")) self.videoSettings.setText(util_ui.tr("Video")) self.updateSettings.setText(util_ui.tr("Updates")) self.importPlugin.setText(util_ui.tr("Import plugin")) self.reloadPlugins.setText(util_ui.tr("Reload plugins")) + self.reloadToxchat.setText(util_ui.tr("Reload tox.chat")) self.searchLineEdit.setPlaceholderText(util_ui.tr("Search")) self.sendMessageButton.setToolTip(util_ui.tr("Send message")) @@ -180,6 +390,7 @@ 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) @@ -187,6 +398,7 @@ 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) @@ -409,6 +621,7 @@ 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) @@ -441,14 +654,133 @@ 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 on Python.\nVersion: ') - text += '' + '\nGitHub: https://github.com/toxygen-project/toxygen/' + text = util_ui.tr('Toxygen is Tox client written in Python.\nVersion: ') + text += '' + '\nGitHub: https://git.plastiras.org/emdee/toxygen' title = util_ui.tr('About') util_ui.message_box(text, title) @@ -504,13 +836,16 @@ class MainWindow(QtWidgets.QMainWindow): self._modal_window.show() def reload_plugins(self): - if self._plugin_loader is not None: + if hasattr(self, '_plugin_loader') and self._plugin_loader is not None: self._plugin_loader.reload() + def reload_toxchat(self): + pass + @staticmethod def import_plugin(): - directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugin')) - if directory: + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugins')) + if directory and os.path.isdir(directory): src = directory + '/' dest = util.get_plugins_directory() util.copy(src, dest) @@ -523,6 +858,25 @@ 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) @@ -532,9 +886,7 @@ class MainWindow(QtWidgets.QMainWindow): 120)) self.menu.show() - # ----------------------------------------------------------------------------------------------------------------- # Messages, calls and file transfers - # ----------------------------------------------------------------------------------------------------------------- def send_message(self): self._messenger.send_message() @@ -597,9 +949,7 @@ 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) @@ -656,9 +1006,7 @@ 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() @@ -681,9 +1029,13 @@ 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 self._contacts_manager.get_curr_friend() is None: - return + if not hasattr(self._contacts_manager, 'get_curr_friend') or \ + self._contacts_manager.get_curr_friend() is None: + #? return + pass self.search_field = self._widget_factory.create_search_screen(self.messages) x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40 self.search_field.setGeometry(x, y, self.messages.width(), 40) @@ -712,7 +1064,10 @@ class MainWindow(QtWidgets.QMainWindow): def update_gc_invites_button_state(self): invites_count = self._groups_service.group_invites_count - self.groupInvitesPushButton.setVisible(invites_count > 0) - text = util_ui.tr('{} new invites to group chats').format(invites_count) + LOG.debug(f"update_gc_invites_button_state invites_count={invites_count}") + + # Fixme + self.groupInvitesPushButton.setVisible(True) # invites_count > 0 + text = util_ui.tr(f'{invites_count} new invites to group chats') self.groupInvitesPushButton.setText(text) self.resizeEvent() diff --git a/toxygen/ui/main_screen_widgets.py b/toxygen/ui/main_screen_widgets.py index 122561b..064cb09 100644 --- a/toxygen/ui/main_screen_widgets.py +++ b/toxygen/ui/main_screen_widgets.py @@ -1,18 +1,26 @@ -from PyQt5 import QtCore, QtGui, QtWidgets -from ui.widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import urllib import re + +from qtpy import QtCore, QtGui, QtWidgets +from qtpy.QtCore import Signal + +from ui.widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit import utils.util as util import utils.ui as util_ui from stickers.stickers import load_stickers +import logging +LOG = logging.getLogger('app.'+'msw') class MessageArea(QtWidgets.QPlainTextEdit): """User types messages here""" def __init__(self, parent, form): super().__init__(parent) - self._messenger = self._contacts_manager = self._file_transfer_handler = None + self._messenger = None + self._contacts_manager = self._file_transfer_handler = None self.parent = form self.setAcceptDrops(True) self._timer = QtCore.QTimer(self) @@ -31,6 +39,7 @@ 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: @@ -38,15 +47,22 @@ class MessageArea(QtWidgets.QPlainTextEdit): else: if self._timer.isActive(): self._timer.stop() - self._messenger.send_typing(False) - self._messenger.send_message() + try: + self._messenger.send_typing(False) + self._messenger.send_message() + except Exception as e: + LOG.error(f"keyPressEvent ERROR send_message to {self._messenger}") + util_ui.message_box(str(e), + util_ui.tr(f"keyPressEvent ERROR send_message to {self._messenger}")) + elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): self.appendPlainText(self._messenger.get_last_message()) + elif event.key() == QtCore.Qt.Key_Tab and self._contacts_manager.is_active_a_group(): text = self.toPlainText() text_cursor = self.textCursor() pos = text_cursor.position() - current_word = re.split("\s+", text[:pos])[-1] + current_word = re.split(r"\s+", text[:pos])[-1] start_index = text.rindex(current_word, 0, pos) peer_name = self._contacts_manager.get_gc_peer_name(current_word) self.setPlainText(text[:start_index] + peer_name + text[pos:]) @@ -380,8 +396,8 @@ class MainMenuButton(QtWidgets.QPushButton): class ClickableLabel(QtWidgets.QLabel): - - clicked = QtCore.pyqtSignal() + # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' + clicked = Signal() def __init__(self, *args): super().__init__(*args) diff --git a/toxygen/ui/menu.py b/toxygen/ui/menu.py index 8aec578..9f0fc05 100644 --- a/toxygen/ui/menu.py +++ b/toxygen/ui/menu.py @@ -1,23 +1,35 @@ -from PyQt5 import QtCore, QtGui, QtWidgets, uic +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtGui, QtWidgets, uic + +import toxygen_wrapper.tests.support_testing as ts +with ts.ignoreStderr(): # not out + import pyaudio + from user_data.settings import * from utils.util import * from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow -import pyaudio import updater.updater as updater import utils.ui as util_ui -import cv2 +from user_data import settings +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +global oPYA +oPYA = pyaudio.PyAudio() class AddContact(CenteredWidget): """Add contact form""" - def __init__(self, settings, contacts_manager, tox_id=''): + def __init__(self, dsettings, contacts_manager, tox_id=''): super().__init__() - self._settings = settings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings self._contacts_manager = contacts_manager uic.loadUi(get_views_path('add_contact_screen'), self) self._update_ui(tox_id) self._adding = False + self._bootstrap = False def _update_ui(self, tox_id): self.toxIdLineEdit = LineEdit(self) @@ -26,8 +38,15 @@ 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 @@ -40,9 +59,10 @@ class AddContact(CenteredWidget): self._adding = False if send is True: # request was successful - self.close() - else: # print error data + pass + elif send and type(send) == str: # print error data self.errorLabel.setText(send) + self.close() def _retranslate_ui(self): self.setWindowTitle(util_ui.tr('Add contact')) @@ -51,12 +71,61 @@ 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, settings, reset): + def __init__(self, dsettings, reset): super().__init__() - self._settings = settings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings self._reset = reset uic.loadUi(get_views_path('network_settings_screen'), self) self._update_ui() @@ -68,16 +137,20 @@ 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['lan_discovery']) + self.lanCheckBox.setChecked(self._settings['local_discovery_enabled']) self._retranslate_ui() self.proxyCheckBox.stateChanged.connect(lambda x: self._activate_proxy()) self._activate_proxy() @@ -90,11 +163,13 @@ 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("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) + self.warningLabel.setText(util_ui.tr("Changing settings require 'Restart TOX core'")) def _activate_proxy(self): bl = self.proxyCheckBox.isChecked() @@ -113,26 +188,28 @@ 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['lan_discovery'] = self.lanCheckBox.isChecked() + self._settings['local_discovery_enabled'] = self.lanCheckBox.isChecked() self._settings.save() # recreate tox instance self._reset() self.close() except Exception as ex: - log('Exception in restart: ' + str(ex)) + LOG.error('ERROR: Exception in restart: ' + str(ex)) class PrivacySettings(CenteredWidget): """Privacy settings form: history, typing notifications""" - def __init__(self, contacts_manager, settings): + def __init__(self, contacts_manager, dsettings): """ :type contacts_manager: ContactsManager """ super().__init__() + self._app = QtWidgets.QApplication.instance() self._contacts_manager = contacts_manager - self._settings = settings + self._settings = dsettings self.initUI() self.center() @@ -247,9 +324,10 @@ class PrivacySettings(CenteredWidget): class NotificationsSettings(CenteredWidget): """Notifications settings form""" - def __init__(self, setttings): + def __init__(self, dsettings): super().__init__() - self._settings = setttings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings # pylint: disable=undefined-variable uic.loadUi(get_views_path('notifications_settings_screen'), self) self._update_ui() self.center() @@ -279,9 +357,10 @@ class NotificationsSettings(CenteredWidget): class InterfaceSettings(CenteredWidget): """Interface settings form""" - def __init__(self, settings, smiley_loader): + def __init__(self, dsettings, smiley_loader): super().__init__() - self._settings = settings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings self._smiley_loader = smiley_loader uic.loadUi(get_views_path('interface_settings_screen'), self) @@ -289,10 +368,10 @@ class InterfaceSettings(CenteredWidget): self.center() def _update_ui(self): - themes = list(self._settings.built_in_themes().keys()) + themes = list(settings.built_in_themes().keys()) self.themeComboBox.addItems(themes) theme = self._settings['theme'] - if theme in self._settings.built_in_themes().keys(): + if theme in settings.built_in_themes().keys(): index = themes.index(theme) else: index = 0 @@ -312,10 +391,10 @@ class InterfaceSettings(CenteredWidget): index = smiley_packs.index('default') self.smileysPackComboBox.setCurrentIndex(index) - app_closing_setting = self._settings['close_app'] - self.closeRadioButton.setChecked(app_closing_setting == 0) - self.hideRadioButton.setChecked(app_closing_setting == 1) - self.closeToTrayRadioButton.setChecked(app_closing_setting == 2) + self._app_closing_setting = self._settings['close_app'] + self.closeRadioButton.setChecked(self._app_closing_setting == 0) + self.hideRadioButton.setChecked(self._app_closing_setting == 1) + self.closeToTrayRadioButton.setChecked(self._app_closing_setting == 2) self.compactModeCheckBox.setChecked(self._settings['compact_mode']) self.showAvatarsCheckBox.setChecked(self._settings['show_avatars']) @@ -337,7 +416,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")) @@ -360,24 +439,23 @@ 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(), self._settings.built_in_themes()[theme]) + styles_path = join_path(get_styles_directory(), settings.built_in_themes()[theme]) with open(styles_path) as fl: style = fl.read() - app.setStyleSheet(style) + self._app.setStyleSheet(style) except IsADirectoryError: pass self._settings['smileys'] = self.smileysCheckBox.isChecked() restart = False - if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): - self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() - restart = True +# if self._settings['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() @@ -394,9 +472,9 @@ class InterfaceSettings(CenteredWidget): if self._settings['language'] != language: self._settings['language'] = language path = Settings.supported_languages()[language] - app.removeTranslator(app.translator) - app.translator.load(join_path(get_translations_directory(), path)) - app.installTranslator(app.translator) + self._app.removeTranslator(self._app.translator) + self._app.translator.load(join_path(get_translations_directory(), path)) + self._app.installTranslator(self._app.translator) app_closing_setting = 0 if self.hideRadioButton.isChecked(): @@ -415,21 +493,28 @@ class AudioSettings(CenteredWidget): Audio calls settings form """ - def __init__(self, settings): + def __init__(self, dsettings): super().__init__() - self._settings = settings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings self._in_indexes = self._out_indexes = None uic.loadUi(get_views_path('audio_settings_screen'), self) self._update_ui() self.center() def closeEvent(self, event): - self._settings.audio['input'] = self._in_indexes[self.inputDeviceComboBox.currentIndex()] - self._settings.audio['output'] = self._out_indexes[self.outputDeviceComboBox.currentIndex()] - self._settings.save() + if 'audio' not in self._settings: + ex = f"self._settings=id(self._settings) {self._settings}" + LOG.warn('AudioSettings.closeEvent settings error: ' + str(ex)) + else: + self._settings['audio']['input'] = \ + self._in_indexes[self.inputDeviceComboBox.currentIndex()] + self._settings['audio']['output'] = \ + self._out_indexes[self.outputDeviceComboBox.currentIndex()] + self._settings.save() def _update_ui(self): - p = pyaudio.PyAudio() + p = oPYA self._in_indexes, self._out_indexes = [], [] for i in range(p.get_device_count()): device = p.get_device_info_by_index(i) @@ -439,8 +524,11 @@ class AudioSettings(CenteredWidget): if device["maxOutputChannels"]: self.outputDeviceComboBox.addItem(str(device["name"])) self._out_indexes.append(i) - self.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings.audio['input'])) - self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings.audio['output'])) + try: + self.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings['audio']['input'])) + self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings['audio']['output'])) + except: pass + self._retranslate_ui() def _retranslate_ui(self): @@ -463,12 +551,13 @@ class DesktopAreaSelectionWindow(RubberBandWindow): class VideoSettings(CenteredWidget): """ - Audio calls settings form + Video calls settings form """ - def __init__(self, settings): + def __init__(self, dsettings): super().__init__() - self._settings = settings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings uic.loadUi(get_views_path('video_settings_screen'), self) self._devices = self._frame_max_sizes = None self._update_ui() @@ -479,24 +568,35 @@ class VideoSettings(CenteredWidget): if self.deviceComboBox.currentIndex() == 0: return try: - self._settings.video['device'] = self.devices[self.input.currentIndex()] + # AttributeError: 'VideoSettings' object has no attribute 'devices' + # ERROR: Saving video settings error: 'VideoSettings' object has no attribute 'input' + index = self.deviceComboBox.currentIndex() + if index in self._devices: + self._settings['video']['device'] = self._devices[index] + else: + LOG.warn(f"{index} not in deviceComboBox self._devices {self._devices}") text = self.resolutionComboBox.currentText() - self._settings.video['width'] = int(text.split(' ')[0]) - self._settings.video['height'] = int(text.split(' ')[-1]) + if len(text.split(' ')[0]) > 1: + self._settings['video']['width'] = int(text.split(' ')[0]) + self._settings['video']['height'] = int(text.split(' ')[-1]) self._settings.save() except Exception as ex: - print('Saving video settings error: ' + str(ex)) + LOG.error('ERROR: Saving video settings error: ' + str(ex)) def save(self, x, y, width, height): self.desktopAreaSelection = None - self._settings.video['device'] = -1 - self._settings.video['width'] = width - self._settings.video['height'] = height - self._settings.video['x'] = x - self._settings.video['y'] = y + self._settings['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] @@ -505,23 +605,35 @@ class VideoSettings(CenteredWidget): self._frame_max_sizes = [(size.width(), size.height())] desktop = util_ui.tr("Desktop") self.deviceComboBox.addItem(desktop) - for i in range(10): - v = cv2.VideoCapture(i) - if v.isOpened(): - v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) - v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) + with ts.ignoreStdout(): + # was range(10) + for i in map(int, ts.get_video_indexes()): + v = cv2.VideoCapture(i) # pylint: disable=no-member + if v.isOpened(): + v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) # pylint: disable=no-member + v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) # pylint: disable=no-member - width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) - 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)) + width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) # pylint: disable=no-member + height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT)) # pylint: disable=no-member + del v + self._devices.append(i) + self._frame_max_sizes.append((width, height)) + self.deviceComboBox.addItem(util_ui.tr('Device #') + str(i)) + + if 'device' not in self._settings['video']: + LOG.warn(f"'device' not in self._settings['video']: {self._settings}") + self._settings['video']['device'] = self._devices[-1] + iIndex = self._settings['video']['device'] try: - index = self._devices.index(self._settings.video['device']) + index = self._devices.index(iIndex) self.deviceComboBox.setCurrentIndex(index) - except: - print('Video devices error!') + except Exception as e: + # off by one - what's Desktop? + se = f"Video devices index error: index={iIndex} {e}" + LOG.warn(se) + # util_ui.message_box(se, util_ui.tr(f"ERROR: Video devices error")) + self._settings['video']['device'] = self._devices[-1] + self._retranslate_ui() def _retranslate_ui(self): @@ -535,7 +647,7 @@ class VideoSettings(CenteredWidget): def _device_changed(self): index = self.deviceComboBox.currentIndex() self.selectRegionPushButton.setVisible(index == 0) - self.resolutionComboBox.setVisible(index != 0) + self.resolutionComboBox.setVisible(True) # index != 0 width, height = self._frame_max_sizes[index] self.resolutionComboBox.clear() dims = [ @@ -559,6 +671,7 @@ 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() @@ -589,14 +702,24 @@ class PluginsSettings(CenteredWidget): self.open.setText(util_ui.tr("Open selected plugin")) def open_plugin(self): + ind = self.comboBox.currentIndex() - plugin = self.data[ind] - window = self.pl_loader.plugin_window(plugin[-1]) - if window is not None: - self._window = window - self._window.show() - else: - util_ui.message_box(util_ui.tr('No GUI found for this plugin'), util_ui.tr('Error')) + plugin = self.data[ind] # ['SearchPlugin', True, 'Description', 'srch'] + # key in self._plugins and hasattr(self._plugins[key], 'instance'): + window = self._plugin_loader.plugin_window(plugin[-1]) + if window is not None and not hasattr(window, 'show'): + LOG.error(util_ui.tr('ERROR: No show for the plugin: ' +repr(window) +' ' +repr(window))) + util_ui.message_box(util_ui.tr('ERROR: No show for the plugin ' +repr(window)), util_ui.tr('Error')) + elif window is not None: + try: + self._window = window + self._window.show() + except Exception as e: + LOG.error(util_ui.tr('ERROR: Error for the plugin: ' +repr(window) +' ' +str(e))) + util_ui.message_box(util_ui.tr('ERROR: Error for the plugin: ' +repr(window)), util_ui.tr('Error')) + elif window is None: + LOG.warn(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window')) + util_ui.message_box(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window'), util_ui.tr('Error')) def update_list(self): self.comboBox.clear() @@ -635,9 +758,10 @@ class UpdateSettings(CenteredWidget): Updates settings form """ - def __init__(self, settings, version): + def __init__(self, dsettings, version): super().__init__() - self._settings = settings + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings 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 8a46fd0..2c0ddfb 100644 --- a/toxygen/ui/messages_widgets.py +++ b/toxygen/ui/messages_widgets.py @@ -1,14 +1,18 @@ -from wrapper.toxcore_enums_and_consts import * +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import html as h +import re + +from qtpy import QtCore, QtGui, QtWidgets + +from toxygen_wrapper.toxcore_enums_and_consts import * import ui.widgets as widgets import utils.util as util import ui.menu as menu -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): @@ -39,7 +43,16 @@ class MessageBrowser(QtWidgets.QTextBrowser): font.setPixelSize(settings['message_font_size']) font.setBold(False) self.setFont(font) - self.resize(width, self.document().size().height()) + try: + # was self.resize(width, self.document().size().height()) + # guessing QSize + self.resize(QtCore.QSize(width, int(self.document().size().height()))) + except TypeError as e: + # TypeError: arguments did not match any overloaded call: + # resize(self, a0: QSize): argument 1 has unexpected type 'int' + # resize(self, w: int, h: int): argument 2 has unexpected type 'float' + pass + self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) self.anchorClicked.connect(self.on_anchor_clicked) @@ -81,7 +94,8 @@ class MessageBrowser(QtWidgets.QTextBrowser): movie = QtGui.QMovie(self) movie.setFileName(file_name) self.urls[movie] = url - movie.frameChanged[int].connect(lambda x: self.animate(movie)) + # Value 'movie.frameChanged' is unsubscriptable + movie.frameChanged().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 bbae7ff..57f7b95 100644 --- a/toxygen/ui/password_screen.py +++ b/toxygen/ui/password_screen.py @@ -1,7 +1,13 @@ +# -*- 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): @@ -78,6 +84,7 @@ 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 8f2d5ba..6bca903 100644 --- a/toxygen/ui/peer_screen.py +++ b/toxygen/ui/peer_screen.py @@ -1,9 +1,12 @@ +# -*- 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 wrapper.toxcore_enums_and_consts as consts +import toxygen_wrapper.toxcore_enums_and_consts as consts class PeerScreen(CenteredWidget): @@ -29,9 +32,11 @@ class PeerScreen(CenteredWidget): self.statusCircle = StatusCircle(self) self.statusCircle.setGeometry(50, 15, 30, 30) - self.statusCircle.update(self._peer.status) - self.peerNameLabel.setText(self._peer.name) - self.ignorePeerCheckBox.setChecked(self._peer.is_muted) + if self._peer: + self.statusCircle.update(self._peer.status) + self.peerNameLabel.setText(self._peer.name) + self.ignorePeerCheckBox.setChecked(self._peer.is_muted) + self.ignorePeerCheckBox.clicked.connect(self._toggle_ignore) self.sendPrivateMessagePushButton.clicked.connect(self._send_private_message) self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) @@ -40,7 +45,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() @@ -53,7 +58,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 2e55d3d..5b4658f 100644 --- a/toxygen/ui/profile_settings_screen.py +++ b/toxygen/ui/profile_settings_screen.py @@ -1,9 +1,11 @@ +# -*- 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""" @@ -29,7 +31,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(self._profile.status_message) + self.statusMessageLineEdit.setText(str(self._profile.status_message)) self.defaultProfilePushButton.clicked.connect(self._toggle_auto_profile) self.copyToxIdPushButton.clicked.connect(self._copy_tox_id) self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) @@ -69,7 +71,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")) + self.copyPublicKeyPushButton.setText(util_ui.tr("Copy public key" +' (64)')) self._set_default_profile_button_text() @@ -144,7 +146,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 cf252d3..7f30653 100644 --- a/toxygen/ui/self_peer_screen.py +++ b/toxygen/ui/self_peer_screen.py @@ -1,5 +1,8 @@ +# -*- 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 3bfc7f3..c838a0d 100644 --- a/toxygen/ui/tray.py +++ b/toxygen/ui/tray.py @@ -1,13 +1,17 @@ -from PyQt5 import QtWidgets, QtGui, QtCore +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import QtWidgets, QtGui, QtCore +# from PyQt5.QtCore import pyqtSignal as Signal +from qtpy.QtCore import Signal + from utils.ui import tr from utils.util import * from ui.password_screen import UnlockAppScreen import os.path - class SystemTrayIcon(QtWidgets.QSystemTrayIcon): - - leftClicked = QtCore.pyqtSignal() + # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' + leftClicked = Signal() 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 new file mode 100644 index 0000000..0549e90 --- /dev/null +++ b/toxygen/ui/views/add_bootstrap_screen.ui @@ -0,0 +1,99 @@ + + + Form + + + + 0 + 0 + 560 + 320 + + + + + 560 + 320 + + + + + 560 + 320 + + + + Form + + + + + 50 + 10 + 150 + 20 + + + + TextLabel + + + + + + 50 + 70 + 150 + 30 + + + + TextLabel + + + + + + 50 + 110 + 460 + 150 + + + + + + + 50 + 270 + 460 + 30 + + + + PushButton + + + + + true + + + + 220 + 10 + 321 + 31 + + + + Qt::NoContextMenu + + + + + + + + + diff --git a/toxygen/ui/views/group_management_screen.ui b/toxygen/ui/views/group_management_screen.ui index 859754b..de7c21e 100644 --- a/toxygen/ui/views/group_management_screen.ui +++ b/toxygen/ui/views/group_management_screen.ui @@ -7,7 +7,7 @@ 0 0 658 - 238 + 283 @@ -91,11 +91,24 @@ - + 20 180 + 300 + 41 + + + + PushButton + + + + + + 20 + 220 611 41 diff --git a/toxygen/ui/views/interface_settings_screen.ui b/toxygen/ui/views/interface_settings_screen.ui index fb0bcf1..b762903 100644 --- a/toxygen/ui/views/interface_settings_screen.ui +++ b/toxygen/ui/views/interface_settings_screen.ui @@ -77,6 +77,7 @@ TextLabel + diff --git a/toxygen/ui/views/login_screen.ui b/toxygen/ui/views/login_screen.ui index 50ca1e0..d100803 100644 --- a/toxygen/ui/views/login_screen.ui +++ b/toxygen/ui/views/login_screen.ui @@ -36,7 +36,6 @@ - Garuda 16 75 true diff --git a/toxygen/ui/views/network_settings_screen.ui b/toxygen/ui/views/network_settings_screen.ui index aacf1e0..f6e2960 100644 --- a/toxygen/ui/views/network_settings_screen.ui +++ b/toxygen/ui/views/network_settings_screen.ui @@ -7,19 +7,19 @@ 0 0 400 - 500 + 545 400 - 500 + 545 400 - 500 + 545 @@ -135,11 +135,27 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + 30 + 380 + 60 + 20 + + + + Chat Url + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 30 - 370 + 430 340 40 @@ -165,7 +181,7 @@ 30 - 420 + 480 340 65 diff --git a/toxygen/ui/views/peer_screen.ui b/toxygen/ui/views/peer_screen.ui index e8e9e31..086dd18 100644 --- a/toxygen/ui/views/peer_screen.ui +++ b/toxygen/ui/views/peer_screen.ui @@ -76,6 +76,7 @@ GroupBox + diff --git a/toxygen/ui/views/profile_settings_screen.ui b/toxygen/ui/views/profile_settings_screen.ui index ece0083..1c899ab 100644 --- a/toxygen/ui/views/profile_settings_screen.ui +++ b/toxygen/ui/views/profile_settings_screen.ui @@ -7,7 +7,7 @@ 0 0 900 - 702 + 680 @@ -19,7 +19,7 @@ 30 10 161 - 31 + 30 @@ -32,7 +32,7 @@ 30 90 161 - 31 + 30 @@ -45,7 +45,7 @@ 30 50 421 - 31 + 30 @@ -55,7 +55,7 @@ 30 130 421 - 31 + 30 @@ -65,7 +65,7 @@ 520 30 311 - 31 + 30 @@ -75,7 +75,7 @@ 40 180 131 - 21 + 20 @@ -88,7 +88,7 @@ 40 210 831 - 61 + 60 @@ -104,7 +104,7 @@ 40 280 371 - 31 + 30 @@ -117,7 +117,7 @@ 440 280 371 - 31 + 30 @@ -130,7 +130,7 @@ 520 80 321 - 35 + 34 @@ -143,7 +143,7 @@ 520 130 321 - 35 + 34 @@ -156,7 +156,7 @@ 60 380 161 - 31 + 30 @@ -169,7 +169,7 @@ 50 420 421 - 31 + 30 @@ -179,7 +179,7 @@ 50 470 421 - 31 + 30 @@ -189,7 +189,7 @@ 500 420 381 - 21 + 20 @@ -202,7 +202,7 @@ 60 580 381 - 21 + 20 @@ -215,7 +215,7 @@ 40 630 831 - 35 + 34 @@ -228,7 +228,7 @@ 50 520 421 - 35 + 34 @@ -241,7 +241,7 @@ 500 470 381 - 21 + 20 @@ -254,7 +254,7 @@ 40 330 371 - 35 + 34 @@ -267,7 +267,7 @@ 440 330 371 - 35 + 34 diff --git a/toxygen/ui/widgets.py b/toxygen/ui/widgets.py index e7fe623..78e9a0a 100644 --- a/toxygen/ui/widgets.py +++ b/toxygen/ui/widgets.py @@ -1,17 +1,33 @@ -from PyQt5 import QtCore, QtGui, QtWidgets -import utils.ui as util_ui +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtGui, QtWidgets +# from PyQt5.QtCore import pyqtSignal as Signal +from qtpy.QtCore import Signal +import utils.ui as util_ui +import logging + +global LOG +LOG = logging.getLogger('app') class DataLabel(QtWidgets.QLabel): """ Label with elided text """ def setText(self, text): - 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) + try: + text = ''.join('\u25AF' if len(bytes(str(c), 'utf-8')) >= 4 else c for c in str(text)) + except Exception as e: + LOG.error(f"DataLabel::setText: {e}") + return + try: + metrics = QtGui.QFontMetrics(self.font()) + text = metrics.elidedText(str(text), QtCore.Qt.ElideRight, self.width()) + except Exception as e: + # RuntimeError: wrapped C/C++ object of type DataLabel has been deleted + text = str(text) + + super().setText(text) class ComboBox(QtWidgets.QComboBox): @@ -65,8 +81,8 @@ class QRightClickButton(QtWidgets.QPushButton): """ Button with right click support """ - - rightClicked = QtCore.pyqtSignal() + # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' + rightClicked = Signal() def __init__(self, parent=None): super().__init__(parent) diff --git a/toxygen/ui/widgets_factory.py b/toxygen/ui/widgets_factory.py index 128e85e..08861a4 100644 --- a/toxygen/ui/widgets_factory.py +++ b/toxygen/ui/widgets_factory.py @@ -1,3 +1,4 @@ +# -*- 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 * @@ -8,7 +9,6 @@ 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,6 +42,11 @@ 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 329353c..0eb81f3 100644 --- a/toxygen/updater/updater.py +++ b/toxygen/updater/updater.py @@ -1,20 +1,26 @@ +# -*- 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 PyQt5 import QtNetwork, QtCore +from qtpy import QtNetwork, QtCore import subprocess +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +TIMEOUT=10 def connection_available(): + return False try: - urllib.request.urlopen('http://216.58.192.142', timeout=1) # google.com + urllib.request.urlopen('http://216.58.192.142', timeout=TIMEOUT) # google.com return True except: return False - def updater_available(): if is_from_sources(): return os.path.exists(util.curr_directory() + '/toxygen_updater.py') @@ -70,12 +76,11 @@ def download(version): os.chdir(util.curr_directory()) url = get_url(version) params = get_params(url, version) - print('Updating Toxygen') - util.log('Updating Toxygen') + LOG.info('Updating Toxygen') try: subprocess.Popen(params) except Exception as ex: - util.log('Exception: running updater failed with ' + str(ex)) + LOG.error('running updater failed with ' + str(ex)) def send_request(version, settings): @@ -97,7 +102,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: - util.log('TOXYGEN UPDATER ERROR: ' + str(ex)) + LOG.error('TOXYGEN UPDATER ' + str(ex)) return False diff --git a/toxygen/user_data/backup_service.py b/toxygen/user_data/backup_service.py index bb0cef9..9f3a051 100644 --- a/toxygen/user_data/backup_service.py +++ b/toxygen/user_data/backup_service.py @@ -1,6 +1,8 @@ -import os.path -from utils.util import get_profile_name_from_path, join_path +# -*- 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: diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py index 05e2f2d..a6f5df0 100644 --- a/toxygen/user_data/profile_manager.py +++ b/toxygen/user_data/profile_manager.py @@ -1,16 +1,26 @@ +# -*- 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): + def __init__(self, toxes, path, app=None): + assert path self._toxes = toxes self._path = path + assert path + self._app = app self._directory = os.path.dirname(path) self._profile_saved_event = Event() # create /avatars if not exists: @@ -18,18 +28,14 @@ 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: @@ -48,22 +54,33 @@ class ProfileManager: def save_profile(self, data): if self._toxes.has_password(): data = self._toxes.pass_encrypt(data) - with open(self._path, 'wb') as fl: - fl.write(data) - print('Profile saved successfully') + profile_path = self._path.replace('.json', '.tox') + try: + suf = f"{os.getpid()}" + with open(profile_path+suf, 'wb') as fl: + fl.write(data) + stat = os.stat(profile_path+suf) + if hasattr(stat, 'st_blocks'): + assert stat.st_blocks > 0, f"Zero length file {profile_path+suf}" + os.rename(profile_path+suf,profile_path) + LOG_INFO('Profile saved successfully to' +profile_path) + except Exception as e: + LOG_WARN(f"Profile save failed to {profile_path}\n{e}") self._profile_saved_event(data) def export_profile(self, settings, new_path, use_new_path): - path = new_path + os.path.basename(self._path) - with open(self._path, 'rb') as fin: + profile_path = self._path.replace('.json', '.tox') + with open(profile_path, 'rb') as fin: data = fin.read() + path = new_path + os.path.basename(profile_path) with open(path, 'wb') as fout: fout.write(data) - print('Profile exported successfully') - util.copy(self._directory + 'avatars', new_path + 'avatars') + LOG.info('Profile exported successfully to ' +path) + util.copy(os.path.join(self._directory, 'avatars'), + os.path.join(new_path, 'avatars')) if use_new_path: - self._path = new_path + os.path.basename(self._path) + profile_path = os.path.join(new_path, os.path.basename(profile_path)) self._directory = new_path settings.update_path(new_path) @@ -72,7 +89,7 @@ class ProfileManager: """ Find available tox profiles """ - path = Settings.get_default_path() + path = get_user_config_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 71422c2..c87eec3 100644 --- a/toxygen/user_data/settings.py +++ b/toxygen/user_data/settings.py @@ -1,62 +1,208 @@ -import json -from utils.util import * -import pyaudio -from common.event import Event +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +from platform import system +import json + +from utils import util +from utils.util import log, join_path +from common.event import Event +import utils.ui as util_ui +import utils.util as util_utils +import user_data +from toxygen_wrapper.tests import support_testing as ts + +global LOG +import logging +LOG = logging.getLogger('settings') + +def merge_args_into_settings(args, settings): + if args: + if not hasattr(args, 'audio'): + LOG.warn('No audio ' +repr(args)) + settings['audio'] = getattr(args, 'audio') + if not hasattr(args, 'video'): + LOG.warn('No video ' +repr(args)) + settings['video'] = getattr(args, 'video') + for key in settings.keys(): + # proxy_type proxy_port proxy_host + not_key = 'not_' +key + if hasattr(args, key): + val = getattr(args, key) + if type(val) == bytes: + # proxy_host - ascii? + # filenames - ascii? + val = str(val, 'UTF-8') + settings[key] = val + elif hasattr(args, not_key): + val = not getattr(args, not_key) + settings[key] = val + clean_settings(settings) + return + +def clean_settings(self): + # failsafe to ensure C tox is bytes and Py settings is str + + # overrides + self['mirror_mode'] = False + # REQUIRED!! + if not os.path.exists('/proc/sys/net/ipv6'): + LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist') + self['ipv6_enabled'] = False + + if 'proxy_type' in self and self['proxy_type'] == 0: + self['proxy_host'] = '' + self['proxy_port'] = 0 + + if 'proxy_type' in self and self['proxy_type'] != 0 and \ + 'proxy_host' in self and self['proxy_host'] != '' and \ + 'proxy_port' in self and self['proxy_port'] != 0: + if 'udp_enabled' in self and self['udp_enabled']: + # We don't currently support UDP over proxy. + LOG.info("UDP enabled and proxy set: disabling UDP") + self['udp_enabled'] = False + if 'local_discovery_enabled' in self and self['local_discovery_enabled']: + LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled") + self['local_discovery_enabled'] = False + if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']: + LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled") + self['dht_announcements_enabled'] = False + + if 'auto_accept_path' in self and \ + type(self['auto_accept_path']) == bytes: + self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8') + + for key in Settings.get_default_settings(): + if key not in self: continue + if type(self[key]) == bytes: + LOG.warn('bytes setting in: ' +key \ + +' ' + repr(self[key])) + # ascii? + # self[key] = str(self[key], 'utf-8') + LOG.debug("Cleaned settings") + +def get_user_config_path(): + system = util_utils.get_platform() + if system == 'Windows': + return os.path.join(os.getenv('APPDATA'), 'Tox/') + elif system == 'Darwin': + return os.path.join(os.getenv('HOME'), 'Library/Application Support/Tox/') + else: + return os.path.join(os.getenv('HOME'), '.config/tox/') + +def supported_languages(): + return { + 'English': 'en_EN', + 'French': 'fr_FR', + 'Russian': 'ru_RU', + 'Ukrainian': 'uk_UA' + } + +def built_in_themes(): + return { + 'dark': 'dark_style.qss', + 'default': 'style.qss' + } + +#def get_global_settings_path(): +# return os.path.join(get_base_directory(), 'toxygen.json') + +def is_active_profile(profile_path): + sFile = profile_path + '.lock' + if not os.path.isfile(sFile): + return False + try: + import psutil + except Exception as e: + return True + with open(sFile, 'rb') as iFd: + sPid = iFd.read() + if sPid and int(sPid.strip()) in psutil.pids(): + return True + LOG.debug('Unlinking stale lock file ' +sFile) + try: + os.unlink(sFile) + except: + pass + return False class Settings(dict): """ Settings of current profile + global app settings """ - def __init__(self, toxes, path): - self._path = path - self._profile_path = path.replace('.json', '.tox') + def __init__(self, toxes, json_path, app): self._toxes = toxes + self._app = app + self._path = app._path + self._args = app._args + self._oArgs = app._args + self._log = lambda l: LOG.log(self._oArgs.loglevel, l) + self._profile_path = json_path.replace('.json', '.tox') + self._settings_saved_event = Event() - if os.path.isfile(path): - with open(path, 'rb') as fl: - data = fl.read() + path = json_path.replace('.tox', '.json') + if path and os.path.isfile(path): try: - if toxes.is_data_encrypted(data): - data = toxes.pass_decrypt(data) + with open(path, 'rb') as fl: + data = fl.read() + if self._toxes.is_data_encrypted(data): + data = self._toxes.pass_decrypt(data) info = json.loads(str(data, 'utf-8')) + LOG.debug('Parsed settings from: ' + str(path)) except Exception as ex: - info = Settings.get_default_settings() - log('Parsing settings error: ' + str(ex)) - super().__init__(info) - self._upgrade() + title = f"Error opening/parsing settings file:" + text = title +f"\n{path}\n" + LOG.error(text +str(ex)) + util_ui.message_box(text, title) + info = Settings.get_default_settings(app._args) + user_data.settings.clean_settings(info) else: - super().__init__(Settings.get_default_settings()) + LOG.debug('get_default_settings for: ' + repr(path)) + info = Settings.get_default_settings(app._args) + + if not path or not os.path.exists(path): + merge_args_into_settings(app._args, info) + else: + aC = self._changed(app._args, info) + if aC: + title = 'Override profile with commandline - ' + if path: + title += os.path.basename(path) + text = 'Override profile with command-line settings? \n' + # text += '\n'.join([str(key) +'=' +str(val) for + # key,val in self._changed(app._args).items()]) + text += repr(aC) + reply = util_ui.question(text, title) + if reply: + merge_args_into_settings(app._args, info) + info['audio'] = getattr(app._args, 'audio') + info['video'] = getattr(app._args, 'video') + if getattr(app._args, 'trace_enabled'): + info['trace_enabled'] = getattr(app._args, 'trace_enabled') + else: + LOG.warn("app._args, 'trace_enabled") + info['trace_enabled'] = False + super().__init__(info) + self._upgrade() + + LOG.info('Parsed settings from: ' + str(path)) + ex = f"self=id(self) {self}" + LOG.debug(ex) + self.save() self.locked = False self.closing = False self.unlockScreen = False - 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) @@ -64,23 +210,43 @@ class Settings(dict): text = bytes(self._toxes.pass_encrypt(bytes(text, 'utf-8'))) else: text = bytes(text, 'utf-8') - with open(self._path, 'wb') as fl: - fl.write(text) - - self._settings_saved_event(text) + json_path = os.path.join(get_user_config_path(), 'toxygen.json') + tmp = json_path + str(os.getpid()) + try: + with open(tmp, 'wb') as fl: + fl.write(text) + if os.path.exists(json_path+'.bak'): + os.remove(json_path+'.bak') + os.rename(json_path, json_path+'.bak') + os.rename(tmp, json_path) + except Exception as e: + LOG.warn(f'Error saving to {json_path} ' +str(e)) + else: + self._settings_saved_event(text) def close(self): path = self._profile_path + '.lock' if os.path.isfile(path): os.remove(path) - def set_active_profile(self): + def set_active_profile(self, profile_path): """ Mark current profile as active """ - path = self._profile_path + '.lock' - with open(path, 'w') as fl: - fl.write('active') + if not profile_path: + profile_path = self.get_auto_profile() + + path = profile_path + '.lock' + try: + import shutil + except: + pass + else: + shutil.copy2(profile_path, path) + # need to open this with the same perms as _profile_path + # copy profile_path and then write? + with open(path, 'wb') as fl: + fl.write(bytes(str(os.getpid()), 'ascii')) def export(self, path): text = json.dumps(self) @@ -92,32 +258,45 @@ class Settings(dict): self._path = new_path self.save() - # ----------------------------------------------------------------------------------------------------------------- # Static methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod - def get_auto_profile(): - p = Settings.get_global_settings_path() + def get_auto_profile(appdir=None): + if appdir is None: + appdir = ts.get_user_config_path() + # self._path = + p = os.path.join(appdir, 'toxygen.json') if not os.path.isfile(p): return None - with open(p) as fl: - data = fl.read() try: - auto = json.loads(data) + with open(p, 'rb') as fl: + data = fl.read() + if self._toxes.is_data_encrypted(data): + data = self._toxes.pass_decrypt(data) except Exception as ex: - log(str(ex)) + LOG.warn(f"fl.read {p}: {ex}") + return None + try: + auto = json.loads(str(data, 'utf-8')) + except Exception as ex: + LOG.warn(f"json.loads {p}: {ex}") auto = {} if 'profile_path' in auto: path = str(auto['profile_path']) if not os.path.isabs(path): - path = join_path(path, curr_directory(__file__)) + path = join_path(path, os.path.dirname(os.path.realpath(__file__))) if os.path.isfile(path): return path + return None + + @staticmethod + def supported_languages(): + # backwards + return supported_languages() @staticmethod def set_auto_profile(path): - p = Settings.get_global_settings_path() + p = os.path.join(os.path.dirname(path), 'toxygen.json') if os.path.isfile(p): with open(p) as fl: data = fl.read() @@ -130,7 +309,8 @@ class Settings(dict): @staticmethod def reset_auto_profile(): - p = Settings.get_global_settings_path() + appdir = ts.get_user_config_path() + p = os.path.join(appdir, 'toxygen.json') if os.path.isfile(p): with open(p) as fl: data = fl.read() @@ -143,28 +323,37 @@ class Settings(dict): fl.write(json.dumps(data)) @staticmethod - def is_active_profile(profile_path): - return os.path.isfile(profile_path + '.lock') - - @staticmethod - def get_default_settings(): + def get_default_settings(args=None): """ Default profile settings """ - return { - 'theme': 'dark', - 'ipv6_enabled': False, + retval = { + # FixMe: match? /var/local/src/c-toxcore/toxcore/tox.h + 'ipv6_enabled': True, 'udp_enabled': True, + 'trace_enabled': False, + 'local_discovery_enabled': True, + 'dht_announcements_enabled': True, 'proxy_type': 0, - 'proxy_host': '127.0.0.1', - 'proxy_port': 9050, + 'proxy_host': '', + 'proxy_port': 0, 'start_port': 0, 'end_port': 0, 'tcp_port': 0, - 'notifications': True, + 'local_discovery_enabled': True, + 'hole_punching_enabled': False, + # tox_log_cb *log_callback; + 'experimental_thread_safety': False, + # operating_system + + 'theme': 'default', + 'notifications': False, 'sound_notifications': False, 'language': 'English', - 'save_history': False, + 'calls_sound': False, # was True + + 'save_history': True, + 'save_unsent_only': False, 'allow_inline': True, 'allow_auto_accept': True, 'auto_accept_path': None, @@ -175,7 +364,6 @@ class Settings(dict): 'friends_aliases': [], 'show_avatars': False, 'typing_notifications': False, - 'calls_sound': True, 'blocked': [], 'plugins': [], 'notes': {}, @@ -188,53 +376,33 @@ class Settings(dict): 'y': 400, 'message_font_size': 14, 'unread_color': 'red', - 'save_unsent_only': False, 'compact_mode': False, 'identicons': True, - 'show_welcome_screen': True, + 'show_welcome_screen': False, 'close_app': 0, 'font': 'Times New Roman', - 'update': 1, + 'update': 0, 'group_notifications': True, - 'download_nodes_list': False, + 'download_nodes_list': False, # + 'download_nodes_url': 'https://nodes.tox.chat/json', 'notify_all_gc': False, - 'lan_discovery': True, - 'backup_directory': None + 'backup_directory': None, + + 'audio': {'input': -1, + 'output': -1, + 'enabled': True}, + 'video': {'device': -1, + 'width': 320, + 'height': 240, + 'x': 0, + 'y': 0}, + 'current_nodes': None, + 'network': 'new', + 'tray_icon': False, } + return retval - @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() @@ -242,3 +410,19 @@ 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 982f287..84b8636 100644 --- a/toxygen/user_data/toxes.py +++ b/toxygen/user_data/toxes.py @@ -1,3 +1,4 @@ +# -*- 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 d2d7122..f7d20e8 100644 --- a/toxygen/utils/ui.py +++ b/toxygen/utils/ui.py @@ -1,6 +1,8 @@ -from PyQt5 import QtWidgets -import utils.util as util +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtWidgets + +import utils.util as util def tr(s): return QtWidgets.QApplication.translate('Toxygen', s) @@ -36,9 +38,9 @@ def file_dialog(caption, file_filter=None): options=QtWidgets.QFileDialog.DontUseNativeDialog) -def save_file_dialog(caption, filter=None): +def save_file_dialog(caption, file_filter=None): return QtWidgets.QFileDialog.getSaveFileName(None, caption, util.curr_directory(), - filter=filter, + filter=file_filter, options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) diff --git a/toxygen/utils/util.py b/toxygen/utils/util.py index 5bd5c3a..70cc7ff 100644 --- a/toxygen/utils/util.py +++ b/toxygen/utils/util.py @@ -1,10 +1,11 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import datetime import os -import time +import platform +import re import shutil import sys -import re -import platform -import datetime +import time def cached(func): @@ -19,14 +20,26 @@ def cached(func): return wrapped_func - -def log(data): +oFD=None +def log(data=None): + global oFD + if not oFD: + if 'TMPDIR' in os.environ: + logdir = os.environ['TMPDIR'] + else: + logdir = '/tmp' + try: + oFD = open(join_path(logdir, 'toxygen.log'), 'a') + except Exception as ex: + oFD = None + print(f"ERROR: opening toxygen.log: {ex}") + return '' + if data is None: return oFD try: - with open(join_path(curr_directory(), 'logs.log'), 'a') as fl: - fl.write(str(data) + '\n') + oFD.write(str(data) +'\n') except Exception as ex: - print(ex) - + print(f"ERROR: writing to toxygen.log: {ex}") + return data def curr_directory(current_file=None): return os.path.dirname(os.path.realpath(current_file or __file__)) @@ -144,7 +157,6 @@ 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) @@ -168,3 +180,11 @@ 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/wrapper/libtox.py b/toxygen/wrapper/libtox.py deleted file mode 100644 index 01d41f1..0000000 --- a/toxygen/wrapper/libtox.py +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 21b0ebc..0000000 --- a/toxygen/wrapper/tox.py +++ /dev/null @@ -1,2532 +0,0 @@ -# -*- 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 deleted file mode 100644 index 98e1c73..0000000 --- a/toxygen/wrapper/toxav.py +++ /dev/null @@ -1,363 +0,0 @@ -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 deleted file mode 100644 index 3f3977a..0000000 --- a/toxygen/wrapper/toxav_enums.py +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index b34e272..0000000 --- a/toxygen/wrapper/toxcore_enums_and_consts.py +++ /dev/null @@ -1,954 +0,0 @@ -TOX_USER_STATUS = { - 'NONE': 0, - 'AWAY': 1, - 'BUSY': 2, -} - -TOX_MESSAGE_TYPE = { - 'NORMAL': 0, - 'ACTION': 1, -} - -TOX_PROXY_TYPE = { - 'NONE': 0, - 'HTTP': 1, - 'SOCKS5': 2, -} - -TOX_SAVEDATA_TYPE = { - 'NONE': 0, - 'TOX_SAVE': 1, - 'SECRET_KEY': 2, -} - -TOX_ERR_OPTIONS_NEW = { - 'OK': 0, - 'MALLOC': 1, -} - -TOX_ERR_NEW = { - 'OK': 0, - 'NULL': 1, - 'MALLOC': 2, - 'PORT_ALLOC': 3, - 'PROXY_BAD_TYPE': 4, - 'PROXY_BAD_HOST': 5, - 'PROXY_BAD_PORT': 6, - 'PROXY_NOT_FOUND': 7, - 'LOAD_ENCRYPTED': 8, - 'LOAD_BAD_FORMAT': 9, -} - -TOX_ERR_BOOTSTRAP = { - 'OK': 0, - 'NULL': 1, - 'BAD_HOST': 2, - 'BAD_PORT': 3, -} - -TOX_CONNECTION = { - 'NONE': 0, - 'TCP': 1, - 'UDP': 2, -} - -TOX_ERR_SET_INFO = { - 'OK': 0, - 'NULL': 1, - 'TOO_LONG': 2, -} - -TOX_ERR_FRIEND_ADD = { - 'OK': 0, - 'NULL': 1, - 'TOO_LONG': 2, - 'NO_MESSAGE': 3, - 'OWN_KEY': 4, - 'ALREADY_SENT': 5, - 'BAD_CHECKSUM': 6, - 'SET_NEW_NOSPAM': 7, - 'MALLOC': 8, -} - -TOX_ERR_FRIEND_DELETE = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_BY_PUBLIC_KEY = { - 'OK': 0, - 'NULL': 1, - 'NOT_FOUND': 2, -} - -TOX_ERR_FRIEND_GET_PUBLIC_KEY = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_GET_LAST_ONLINE = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_QUERY = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, -} - -TOX_ERR_SET_TYPING = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, -} - -TOX_ERR_FRIEND_SEND_MESSAGE = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'SENDQ': 4, - 'TOO_LONG': 5, - 'EMPTY': 6, -} - -TOX_FILE_KIND = { - 'DATA': 0, - 'AVATAR': 1, -} - -TOX_FILE_CONTROL = { - 'RESUME': 0, - 'PAUSE': 1, - 'CANCEL': 2, -} - -TOX_ERR_FILE_CONTROL = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, - 'FRIEND_NOT_CONNECTED': 2, - 'NOT_FOUND': 3, - 'NOT_PAUSED': 4, - 'DENIED': 5, - 'ALREADY_PAUSED': 6, - 'SENDQ': 7, -} - -TOX_ERR_FILE_SEEK = { - 'OK': 0, - 'FRIEND_NOT_FOUND': 1, - 'FRIEND_NOT_CONNECTED': 2, - 'NOT_FOUND': 3, - 'DENIED': 4, - 'INVALID_POSITION': 5, - 'SENDQ': 6, -} - -TOX_ERR_FILE_GET = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'NOT_FOUND': 3, -} - -TOX_ERR_FILE_SEND = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'NAME_TOO_LONG': 4, - 'TOO_MANY': 5, -} - -TOX_ERR_FILE_SEND_CHUNK = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'NOT_FOUND': 4, - 'NOT_TRANSFERRING': 5, - 'INVALID_LENGTH': 6, - 'SENDQ': 7, - 'WRONG_POSITION': 8, -} - -TOX_ERR_FRIEND_CUSTOM_PACKET = { - 'OK': 0, - 'NULL': 1, - 'FRIEND_NOT_FOUND': 2, - 'FRIEND_NOT_CONNECTED': 3, - 'INVALID': 4, - 'EMPTY': 5, - 'TOO_LONG': 6, - 'SENDQ': 7, -} - -TOX_ERR_GET_PORT = { - 'OK': 0, - 'NOT_BOUND': 1, -} - -TOX_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 deleted file mode 100644 index 31de085..0000000 --- a/toxygen/wrapper/toxencryptsave.py +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index cf795f8..0000000 --- a/toxygen/wrapper/toxencryptsave_enums_and_consts.py +++ /dev/null @@ -1,29 +0,0 @@ -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