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 47d34c0..5e4d717 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,41 @@ +.pylint.err +.pylint.out *.pyc *.pyo -*.ui + +*.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 toxygen/dist *.spec -dist/ +dist toxygen/avatars toxygen/__pycache__ /*.egg-info /*.egg - +html +Toxygen.egg-info +*.tox +.cache +*.db +*~ +Makefile diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml 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/.rsync b/.rsync new file mode 100644 index 0000000..e69de29 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/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a4011e1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,53 @@ +language: python +python: + - "3.5" + - "3.6" +os: + - linux +dist: trusty +notifications: + email: false +before_install: + - sudo apt-get update + - sudo apt-get install -y checkinstall build-essential + - sudo apt-get install portaudio19-dev + - sudo apt-get install libsecret-1-dev + - sudo apt-get install libconfig-dev libvpx-dev check -qq +install: + - pip install sip + - pip install pyqt5 + - pip install pyaudio + - pip install opencv-python + - pip install pydenticon +before_script: +# Opus + - wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz + - tar xzf opus-1.0.3.tar.gz + - cd opus-1.0.3 + - ./configure + - make -j3 + - sudo make install + - cd .. +# Libsodium + - git clone git://github.com/jedisct1/libsodium.git + - cd libsodium + - git checkout tags/1.0.3 + - ./autogen.sh + - ./configure && make -j$(nproc) + - sudo checkinstall --install --pkgname libsodium --pkgversion 1.0.0 --nodoc -y + - sudo ldconfig + - cd .. +# Toxcore + - git clone https://github.com/ingvar1995/toxcore.git --branch=ngc_rebase + - cd toxcore + - mkdir _build && cd _build + - cmake .. + - make -j$(nproc) + - sudo make install + - echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf + - sudo ldconfig + - cd .. + - cd .. +script: + - py.test tests/travis.py + - py.test tests/tests.py diff --git a/MANIFEST.in b/MANIFEST.in index 851e0b6..89e57c6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,9 +10,10 @@ include toxygen/smileys/animated/config.json include toxygen/smileys/starwars/*.gif include toxygen/smileys/starwars/*.png include toxygen/smileys/starwars/config.json -include toxygen/styles/style.qss +include toxygen/smileys/ksk/*.png +include toxygen/smileys/ksk/config.json +include toxygen/styles/*.qss include toxygen/translations/*.qm include toxygen/libs/libtox.dll include toxygen/libs/libsodium.a -include toxygen/libs/libtox64.dll -include toxygen/libs/libsodium64.a \ No newline at end of file +include toxygen/bootstrap/nodes.json diff --git a/README.md b/README.md index dd86dc7..8a15a09 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,147 @@ -# Toxygen -Toxygen is cross-platform [Tox](https://tox.chat/) client written in Python3 +# Toxygen -[![Release](https://img.shields.io/github/release/xveduk/toxygen.svg?style=flat)](https://github.com/xveduk/toxygen/releases/latest) -[![Open issues](https://img.shields.io/github/issues/xveduk/toxygen.svg?style=flat)](https://github.com/xveduk/toxygen/issues) -[![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](https://raw.githubusercontent.com/xveduk/toxygen/master/LICENSE.md) +Toxygen is powerful cross-platform [Tox](https://tox.chat/) client +for Tox and IRC/weechat written in pure Python3. -### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) +### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) -### Supported OS: -- Windows -- Linux +### Supported OS: Linux and Windows (only Linux is tested at the moment) -###Features -- [x] 1v1 messages -- [x] File transfers -- [x] Audio -- [x] Plugins support -- [x] Chat history -- [x] Emoticons -- [x] Stickers -- [x] Screenshots -- [x] Name lookups (toxme.io support) -- [x] Save file encryption -- [x] Profile import and export -- [x] Faux offline messaging -- [x] Faux offline file transfers -- [x] Inline images -- [x] Message splitting -- [x] Proxy support -- [x] Avatars -- [x] Multiprofile -- [x] Multilingual -- [x] Sound notifications -- [x] Contact aliases -- [x] Contact blocking -- [x] Typing notifications -- [x] Changing nospam -- [x] File resuming -- [x] Read receipts -- [ ] Video -- [ ] Desktop sharing -- [ ] Group chats +### Features: -###Downloads -[Releases](https://github.com/xveduk/toxygen/releases) +- PyQt5, PyQt6, and maybe PySide2, PySide6 via qtpy +- IRC via weechat /relay +- NGC groups +- 1v1 messages +- File transfers +- Audio calls +- Video calls +- Group chats +- Plugins support +- Desktop sharing +- Chat history +- Emoticons +- Stickers +- Screenshots +- Save file encryption +- Profile import and export +- Faux offline messaging +- Faux offline file transfers +- Inline images +- Message splitting +- Proxy support - runs over tor, without DNS leaks +- Avatars +- Multiprofile +- Multilingual +- Sound notifications +- Contact aliases +- Contact blocking +- Typing notifications +- Changing nospam +- File resuming +- Read receipts +- uses gevent -[Download last stable version](https://github.com/xveduk/toxygen/archive/master.zip) - -[Download develop version](https://github.com/xveduk/toxygen/archive/develop.zip) - -###Screenshots +### Screenshots *Toxygen on Ubuntu and Windows* ![Ubuntu](/docs/ubuntu.png) ![Windows](/docs/windows.png) +Windows was working but is not currently being tested. AV is working +but the video is garbled: we're unsure of naming the AV devices +from the commandline. We need to get a working echobot that supports SOCKS5; +we were working on one in https://git.plastiras.org/emdee/toxygen_wrapper -###Docs -[Check /docs/ for more info](/docs/) +## Forked +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/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/compile.md b/docs/compile.md index 6b6ab73..b4f6810 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -1,10 +1,19 @@ -#Compile Toxygen +# Compile Toxygen You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/) -Install PyInstaller: -``pip3 install pyinstaller`` +Use Dockerfile and build script from `build` directory: -``pyinstaller --windowed --icon images/icon.ico main.py`` +1. Build image: +``` +docker build -t toxygen . +``` -Don't forget to copy /images/, /sounds/, /translations/, /styles/, /smileys/, /stickers/ (and /libs/libtox.dll on Windows) to /dist/main/ +2. Run container: +``` +docker run -it toxygen bash +``` + +3. Execute `build.sh` script: + +```./build.sh``` diff --git a/docs/contact.md b/docs/contact.md new file mode 100644 index 0000000..5eb2fa6 --- /dev/null +++ b/docs/contact.md @@ -0,0 +1,6 @@ +# Contact us: + +1) https://git.plastiras.org/emdee/toxygen/issues + +2) Use Toxygen Tox Group (NGC) - +ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA diff --git a/docs/contributing.md b/docs/contributing.md index 8b1e7fa..b2cebf4 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,20 +1,25 @@ -#Issues +# Issues Help us find all bugs in Toxygen! Please provide following info: - OS - Toxygen version -- Toxygen executable info - .py or precompiled binary +- Toxygen executable info - python executable (.py), precompiled binary, from package etc. - Steps to reproduce the bug -Want to see new feature in Toxygen? [Ask for it!](https://github.com/xveduk/toxygen/issues) +Want to see new feature in Toxygen? +[Ask for it!](https://git.plastiras.org/emdee/toxygen/issues) -#Pull requests +# Pull requests -Developer? Feel free to open pull request. Our dev team is small so we glad to get help. -Don't know what to do? Improve UI, fix [issues](https://github.com/xveduk/toxygen/issues) or implement features from our TODO list. +Developer? Feel free to open pull request. Our dev team is small so we glad to get help. +Don't know what to do? Improve UI, fix +[issues](https://git.plastiras.org/emdee/toxygen/issues) +or implement features from our TODO list. You can find our TODO's in code, issues list and [here](/README.md). Also you can implement [plugins](/docs/plugins.md) for Toxygen. -#Translations +Note that we have a lot of branches for different purposes. Master branch is for stable versions (releases) only, so I recommend to open PR's to develop branch. Development of next Toxygen version usually goes there. Other branches used for implementing different tasks such as file transfers improvements or audio calls implementation etc. -Help us translate Toxygen! Translation can be created using pyside-lupdate (``pyside-lupdate toxygen.pro``) and QT Linguist. \ No newline at end of file +# Translations + +Help us translate Toxygen! Translation can be created using pylupdate (``pylupdate5 toxygen.pro``) and QT Linguist. diff --git a/docs/install.md b/docs/install.md index 6142c9c..7d2b773 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,59 +1,51 @@ # How to install Toxygen -## Use precompiled binary: -[Check our releases page](https://github.com/xveduk/toxygen/releases) +### Linux -##Using pip3 - -### Windows (32-bit interpreter) - -``pip3.4 install toxygen`` -Run app using ``toxygen`` command. - -##Linux - -1. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/) +1. Install [c-toxcore](https://github.com/TokTok/c-toxcore/) 2. Install PortAudio: ``sudo apt-get install portaudio19-dev`` -3. Install toxygen: -``sudo pip3.4 install toxygen`` -4 Run toxygen using ``toxygen`` command. +3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5`` +4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python`` +5. Install [toxygen](https://git.plastiras.org/emdee/toxygen/) +6. Run toxygen using ``toxygen`` command. ## From source code (recommended for developers) ### Windows -1. [Download and install latest Python 3.4](https://www.python.org/downloads/windows/) -2. [Install PySide](https://pypi.python.org/pypi/PySide/1.2.4) (recommended) or [PyQt4](https://riverbankcomputing.com/software/pyqt/download) -3. Install PyAudio: ``pip3.4 install pyaudio`` -4. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip) -5. Unpack archive -6. Download latest libtox.dll build, download latest libsodium.a build, put it into \src\libs\ -7. Run \src\main.py. - -[libtox.dll for 32-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86_shared_release.zip) - -[libtox.dll for 64-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86-64_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86-64_shared_release.zip) - -[libsodium.a for 32-bit Python](https://build.tox.chat/view/libsodium/job/libsodium_build_windows_x86_static_release/lastSuccessfulBuild/artifact/libsodium_build_windows_x86_static_release.zip) - -[libsodium.a for 64-bit Python](https://build.tox.chat/view/libsodium/job/libsodium_build_windows_x86-64_static_release/lastSuccessfulBuild/artifact/libsodium_build_windows_x86-64_static_release.zip) +Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly recommended to use 64-bit Python. +1. [Download and install latest Python 3 64-bit](https://www.python.org/downloads/windows/) +2. Install PyQt5: ``pip install pyqt5`` +3. Install PyAudio: ``pip install pyaudio`` +4. Install numpy: ``pip install numpy`` +5. Install OpenCV: ``pip install opencv-python`` +6. git clone --depth=1 https://git.plastiras.org/emdee/toxygen/ +7. I don't know +8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\ +9. Run \toxygen\main.py. ### Linux -Dependencies: - -1. Install latest Python3.4: +1. Install latest Python3: ``sudo apt-get install python3`` -2. [Install PySide](https://wiki.qt.io/PySide_Binaries_Linux) (recommended), using terminal - ``sudo apt-get install python3-pyside``, or install [PyQt4](https://riverbankcomputing.com/software/pyqt/download). -3. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/) -4. Install PyAudio: -``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` -5. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip) -6. Unpack archive -7. Run app: -``python3.4 main.py`` +2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` +3. Install [toxcore](https://github.com/TokTok/c-toxcore) with toxav support) +4. Install PyAudio: ``sudo apt-get install portaudio19-dev python3-pyaudio`` (or ``sudo pip3 install pyaudio``) +5. Install toxygen_wrapper https://git.plastiras.org/emdee/toxygen_wrapper +6. Install the rest of the requirements: ``sudo pip3 install -m requirements.txt`` +7. git clone --depth=1 [toxygen](https://git.plastiras.org/emdee/toxygen/) +8. Look in the Makefile for the install target and type +`` +make install +`` +You should set the PIP_EXE_MSYS and PYTHON_EXE_MSYS variables and it does +`` + ${PIP_EXE_MSYS} --python ${PYTHON_EXE_MSYS} install \ + --target ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/ \ + --upgrade . +`` +9. Run app: +``python3 ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/bin/toxygen`` -## Compile Toxygen -Check [compile.md](/docs/compile.md) for more info diff --git a/docs/plugin_api.md b/docs/plugin_api.md index 1c7971c..32a27f8 100644 --- a/docs/plugin_api.md +++ b/docs/plugin_api.md @@ -1,6 +1,6 @@ -#Plugins API +# Plugins API -In Toxygen plugin is single python (supported Python 3.0 - 3.4) module (.py file) and directory with data associated with it. +In Toxygen plugin is single python module (.py file) and directory with data associated with it. Every module must contain one class derived from PluginSuperClass defined in [plugin_super_class.py](/src/plugins/plugin_super_class.py). Instance of this class will be created by PluginLoader class (defined in [plugin_support.py](/src/plugin_support.py) ). This class can enable/disable plugins and send data to it. Every plugin has its own full name and unique short name (1-5 symbols). Main app can get it using special methods. @@ -12,12 +12,13 @@ All plugin's data should be stored in following structure: |---plugin_short_name.py |---/plugin_short_name/ |---settings.json + |---readme.txt |---logs.txt |---other_files ``` Plugin MUST override: -- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxEncryptSave instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save. +- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxES instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save. Plugin can override following methods: - get_description - this method should return plugin description. @@ -44,13 +45,13 @@ Import statement will not work in case you import module that wasn't previously About GUI: -It's strictly recommended to support both PySide and PyQt4 in GUI. Plugin can have no GUI at all. +GUI is available via PyQt5. Plugin can have no GUI at all. Exceptions: Plugin's methods MUST NOT raise exceptions. -#Examples +# Examples -You can find examples in [official repo](https://github.com/ingvar1995/toxygen_plugins) +You can find examples in [official repo](https://git.plastiras.org/emdee/toxygen_plugins) diff --git a/docs/plugins.md b/docs/plugins.md index ee73415..98fbac8 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,22 +1,22 @@ -#Plugins +# Plugins -Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 module (.py file) and directory with plugin's data which provide some additional functionality. +Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.5 - 3.6 module (.py file) and directory with plugin's data which provide some additional functionality. -#How to write plugin +# How to write plugin Check [Plugin API](/docs/plugin_api.md) for more info -#How to install plugin +# How to install plugin Toxygen comes without preinstalled plugins. -1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins -> Import plugin) -2. Restart Toxygen +1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins => Import plugin) +2. Restart Toxygen or choose Plugins => Reload plugins in menu. -##Note: /src/plugins/ should contain plugin_super_class.py and __init__.py +## Note: /src/plugins/ should contain plugin_super_class.py and __init__.py -#Plugins list +# Plugins list WARNING: It is unsecure to install plugin not from this list! -[Main repo](https://github.com/ingvar1995/toxygen_plugins) \ No newline at end of file +[Main repo](https://github.com/toxygen-project/toxygen_plugins) \ No newline at end of file diff --git a/docs/smileys_and_stickers.md b/docs/smileys_and_stickers.md index 53a360b..8705ba8 100644 --- a/docs/smileys_and_stickers.md +++ b/docs/smileys_and_stickers.md @@ -1,4 +1,4 @@ -#Smileys +# Smileys Toxygen support smileys. Smiley is small picture which replaces some symbol or combination of symbols. If you want to create your own smiley pack, create directory in src/smileys/. This directory must contain images with smileys and config.json. Example of config.json: @@ -6,8 +6,8 @@ Toxygen support smileys. Smiley is small picture which replaces some symbol or c Animated smileys (.gif) are supported too. -#Stickers +# Stickers -Sticker is inline image. If you want to create your own smiley pack, create directory in src/stickers/ and place your stickers there. +Sticker is inline image. If you want to create your own sticker pack, create directory in src/stickers/ and place your stickers there. -Users can import plugins and stickers packs using menu: Settings -> Interface \ No newline at end of file +Users can import smileys and stickers using menu: Settings -> Interface 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/docs/ubuntu.png b/docs/ubuntu.png old mode 100755 new mode 100644 index cd80444..67951a5 Binary files a/docs/ubuntu.png and b/docs/ubuntu.png differ diff --git a/docs/windows.png b/docs/windows.png old mode 100755 new mode 100644 index d4ed323..f13f4c0 Binary files a/docs/windows.png and b/docs/windows.png differ diff --git a/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 670538b..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -from setuptools import setup -from setuptools.command.install import install -from platform import system -from subprocess import call -from toxygen.util import program_version - - -version = program_version + '.0' - -MODULES = ['PyAudio'] - -if system() == 'Windows': - MODULES.append('PySide') - - -class InstallScript(install): - """This class configures Toxygen after installation""" - - def run(self): - install.run(self) - OS = system() - if OS == 'Windows': - call(["toxygen", "--configure"]) - elif OS == 'Linux': - call(["toxygen", "--clean"]) - -setup(name='Toxygen', - version=version, - description='Toxygen - Tox client', - long_description='Toxygen is powerful Tox client written in Python3', - url='https://github.com/xveduk/toxygen/', - keywords='toxygen tox messenger', - author='Ingvar', - maintainer='Ingvar', - license='GPL3', - packages=['toxygen', 'toxygen.plugins', 'toxygen.styles'], - install_requires=MODULES, - include_package_data=True, - classifiers=[ - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - ], - entry_points={ - 'console_scripts': ['toxygen=toxygen.main:main'], - }, - cmdclass={ - 'install': InstallScript, - }, - ) diff --git a/setup.py.dst b/setup.py.dst 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/tests.py b/tests/tests.py index c9f5ca6..e3c9b6b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,70 +1,18 @@ -from toxygen.bootstrap import node_generator -from toxygen.profile import * -from toxygen.settings import ProfileHelper -from toxygen.tox_dns import tox_dns -import toxygen.toxencryptsave as encr +from toxygen.middleware.tox_factory import * -class TestProfile: - - def test_search(self): - arr = ProfileHelper.find_profiles() - assert len(arr) >= 2 - - def test_open(self): - data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile() - assert data - +# TODO: add new tests class TestTox: - def test_loading(self): - data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile() - settings = Settings.get_default_settings() - tox = tox_factory(data, settings) - for data in node_generator(): - tox.bootstrap(*data) - del tox - def test_creation(self): - name = b'Toxygen User' - status_message = b'Toxing on Toxygen' + name = 'Toxygen User' + status_message = 'Toxing on Toxygen' tox = tox_factory() tox.self_set_name(name) tox.self_set_status_message(status_message) data = tox.get_savedata() del tox tox = tox_factory(data) - assert tox.self_get_name() == str(name, 'utf-8') - assert tox.self_get_status_message() == str(status_message, 'utf-8') - - def test_friend_list(self): - data = ProfileHelper(Settings.get_default_path(), 'bob').open_profile() - settings = Settings.get_default_settings() - tox = tox_factory(data, settings) - s = tox.self_get_friend_list() - size = tox.self_get_friend_list_size() - assert size <= 2 - assert len(s) <= 2 - del tox - - -class TestDNS: - - def test_dns(self): - bot_id = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5' - tox_id = tox_dns('groupbot@toxme.io') - assert tox_id == bot_id - - -class TestEncryption: - - def test_encr_decr(self): - with open(settings.Settings.get_default_path() + '/alice.tox', 'rb') as fl: - data = fl.read() - lib = encr.ToxEncryptSave() - lib.set_password('easypassword') - copy_data = data[:] - data = lib.pass_encrypt(data) - data = lib.pass_decrypt(data) - assert copy_data == data + assert tox.self_get_name() == name + assert tox.self_get_status_message() == status_message diff --git a/tests/travis.py b/tests/travis.py new file mode 100644 index 0000000..af8f83f --- /dev/null +++ b/tests/travis.py @@ -0,0 +1,4 @@ +class TestToxygen: + + def test_main(self): + 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 new file mode 100644 index 0000000..4326849 --- /dev/null +++ b/toxygen/app.py @@ -0,0 +1,1050 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +import sys +import traceback +import logging +from random import shuffle +import threading +from time import sleep, time +from copy import deepcopy + +# used only in loop +import gevent + +from qtpy import QtWidgets, QtGui, QtCore +from qtpy.QtCore import QTimer +from qtpy.QtWidgets import QApplication + +__version__ = "1.0.0" + +try: + import coloredlogs + if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' + # https://pypi.org/project/coloredlogs/ +except ImportError as e: + coloredlogs = False + +try: + # https://github.com/pyqtconsole/pyqtconsole + from pyqtconsole.console import PythonConsole +except Exception as e: + PythonConsole = None + +try: + import qdarkstylexxx +except ImportError: + qdarkstyle = None + +from middleware import threads +import middleware.callbacks as callbacks +import updater.updater as updater +from middleware.tox_factory import tox_factory +import toxygen_wrapper.toxencryptsave as tox_encrypt_save +import user_data.toxes +from user_data import settings +from user_data.settings import get_user_config_path, merge_args_into_settings +from user_data.settings import Settings +from user_data.profile_manager import ProfileManager + +from plugin_support.plugin_support import PluginLoader + +import ui.password_screen as password_screen +from ui.login_screen import LoginScreen +from ui.main_screen import MainWindow +from ui import tray + +import utils.ui as util_ui +import utils.util as util +from av.calls_manager import CallsManager +from common.provider import Provider +from contacts.contact_provider import ContactProvider +from contacts.contacts_manager import ContactsManager +from contacts.friend_factory import FriendFactory +from contacts.group_factory import GroupFactory +from contacts.group_peer_factory import GroupPeerFactory +from contacts.profile import Profile +from file_transfers.file_transfers_handler import FileTransfersHandler +from file_transfers.file_transfers_messages_service import FileTransfersMessagesService +from groups.groups_service import GroupsService +from history.database import Database +from history.history import History +from messenger.messenger import Messenger +from network.tox_dns import ToxDns +from smileys.smileys import SmileyLoader +from ui.create_profile_screen import CreateProfileScreen +from ui.items_factories import MessagesItemsFactory, ContactItemsFactory +from ui.widgets_factory import WidgetsFactory +from user_data.backup_service import BackupService +import styles.style # TODO: dynamic loading + +import toxygen_wrapper.tests.support_testing as ts + +global LOG +LOG = logging.getLogger('app') + +IDLE_PERIOD = 0.10 +iNODES=8 +bSHOW_TRAY=False + +def setup_logging(oArgs) -> None: + global LOG + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S', + fmt='%(levelname)s:%(name)s %(message)s') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + if coloredlogs: + aKw = dict(level=oArgs.loglevel, + logger=LOG, + fmt='%(name)s %(levelname)s %(message)s') + aKw['stream'] = sys.stdout + coloredlogs.install(**aKw) + + else: + aKw = dict(level=oArgs.loglevel, + format='%(name)s %(levelname)-4s %(message)s') + aKw['stream'] = sys.stdout + logging.basicConfig(**aKw) + + if oArgs.logfile: + oFd = open(oArgs.logfile, 'wt') + setattr(oArgs, 'log_oFd', oFd) + oHandler = logging.StreamHandler(stream=oFd) + LOG.addHandler(oHandler) + + LOG.setLevel(oArgs.loglevel) + LOG.trace = lambda l: LOG.log(0, repr(l)) + LOG.info(f"Setting loglevel to {oArgs.loglevel}") + + if oArgs.loglevel < 20: + # opencv debug + sys.OpenCV_LOADER_DEBUG = True + +#? with ignoreStderr(): for png +# silence logging PyQt5.uic.uiparser +logging.getLogger('PyQt5.uic').setLevel(logging.ERROR) +logging.getLogger('PyQt5.uic.uiparser').setLevel(logging.ERROR) +logging.getLogger('PyQt5.uic.properties').setLevel(logging.ERROR) + +global iI +iI = 0 + +sSTYLE = """ +.QWidget {font-family Helvetica;} +.QCheckBox { font-family Helvetica;} +.QComboBox { font-family Helvetica;} +.QGroupBox { font-family Helvetica;} +.QLabel {font-family Helvetica;} +.QLineEdit { font-family Helvetica;} +.QListWidget { font-family Helvetica;} +.QListWidgetItem { font-family Helvetica;} +.QMainWindow {font-family Helvetica;} +.QMenu {font-family Helvetica;} +.QMenuBar {font-family Helvetica;} +.QPlainText {font-family Courier; weight: 75;} +.QPlainTextEdit {font-family Courier;} +.QPushButton {font-family Helvetica;} +.QRadioButton { font-family Helvetica; } +.QText {font-family Courier; weight: 75; } +.QTextBrowser {font-family Courier; weight: 75; } +.QTextSingleLine {font-family Courier; weight: 75; } +.QToolBar { font-weight: bold; } +""" +class App: + + def __init__(self, version, oArgs): + global LOG + self._args = oArgs + self.oArgs = oArgs + self._path = path_to_profile = oArgs.profile + uri = oArgs.uri + logfile = oArgs.logfile + loglevel = oArgs.loglevel + + setup_logging(oArgs) + # sys.stderr.write( 'Command line args: ' +repr(oArgs) +'\n') + LOG.info("Command line: " +' '.join(sys.argv[1:])) + LOG.debug(f'oArgs = {oArgs}') + LOG.info("Starting toxygen version " +version) + + self._version = version + self._tox = None + self._app = self._settings = self._profile_manager = None + self._plugin_loader = self._messenger = None + self._tox = self._ms = self._init = self._main_loop = self._av_loop = None + self._uri = self._toxes = self._tray = None + self._file_transfer_handler = self._contacts_provider = None + self._friend_factory = self._calls_manager = None + self._contacts_manager = self._smiley_loader = None + self._group_peer_factory = self._tox_dns = self._backup_service = None + self._group_factory = self._groups_service = self._profile = None + if uri is not None and uri.startswith('tox:'): + self._uri = uri[4:] + self._history = None + self.bAppExiting = False + + # Public methods + + def set_trace(self) -> None: + """unused""" + LOG.debug('pdb.set_trace ') + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + import pdb; pdb.set_trace() + + def ten(self, i=0) -> None: + """unused""" + global iI + iI += 1 + if logging.getLogger('app').getEffectiveLevel() != 10: + sys.stderr.write('CHANGED '+str(logging.getLogger().level+'\n')) + LOG.setLevel(10) + LOG.root.setLevel(10) + logging.getLogger('app').setLevel(10) + #sys.stderr.write(f"ten '+str(iI)+' {i}"+' '+repr(LOG) +'\n') + #LOG.debug('ten '+str(iI)) + + def iMain(self) -> int: + """ + Main function of app. loads login screen if needed and starts main screen + """ + self._app = QApplication([]) + self._load_icon() + + # is this still needed? + if util.get_platform() == 'Linux' and \ + hasattr(QtCore.Qt, 'AA_X11InitThreads'): + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) + + self._load_base_style() + + encrypt_save = tox_encrypt_save.ToxEncryptSave() + self._toxes = user_data.toxes.ToxES(encrypt_save) + try: + # this throws everything as errors + if not self._select_and_load_profile(): + return 2 + if hasattr(self._args, 'update') and self._args.update: + if self._try_to_update(): return 3 + + self._load_app_styles() + if self._args.language != 'English': + # (Pdb) Fatal Python error: Segmentation fault + self._load_app_translations() + self._create_dependencies() + + self._start_threads(True) + + if self._uri is not None: + self._ms.add_contact(self._uri) + except Exception as e: + LOG.error(f"Error loading profile: {e}") + sys.stderr.write(' iMain(): ' +f"Error loading profile: {e}" \ + +'\n' + traceback.format_exc()+'\n') + util_ui.message_box(str(e), + util_ui.tr('Error loading profile')) + return 4 + + self._app.lastWindowClosed.connect(self._app.quit) + try: + self._execute_app() + self.quit() + retval = 0 + except KeyboardInterrupt: + retval = 0 + except Exception: + retval = 1 + + return retval + + # App executing + + def _execute_app(self) -> None: + LOG.debug("_execute_app") + + while True: + try: + self._app.exec_() + except Exception as ex: + LOG.error('Unhandled exception: ' + str(ex)) + else: + break + + def quit(self, retval=0) -> None: + LOG.debug("quit") + self._stop_app() + + # failsafe: segfaults on exit - maybe it's Qt + if hasattr(self, '_tox'): + if self._tox and hasattr(self._tox, 'kill'): + LOG.debug(f"quit: Killing {self._tox}") + self._tox.kill() + del self._tox + + if hasattr(self, '_app'): + self._app.quit() + del self._app.quit + del self._app + + sys.stderr.write('quit raising SystemExit' +'\n') + # hanging on gevents + # Thread 1 "python3.9" received signal SIGSEGV, Segmentation fault. + #44 0x00007ffff7fb2f93 in () at /usr/lib/python3.9/site-packages/greenlet/_greenlet.cpython-39-x86_64-linux-gnu.so + #45 0x00007ffff7fb31ef in () at /usr/lib/python3.9/site-packages/greenlet/_greenlet.cpython-39-x86_64-linux-gnu.so + #46 0x00007ffff452165c in hb_shape_plan_create_cached2 () at /usr/lib64/libharfbuzz.so.0 + + raise SystemExit(retval) + + def _stop_app(self) -> None: + LOG.debug("_stop_app") + self._save_profile() + self._history.save_history() + + self._plugin_loader.stop() + try: + self._stop_threads(is_app_closing=True) + except (Exception, RuntimeError): + # RuntimeError: cannot join current thread + pass + # I think there are threads still running here leading to a SEGV + # File "/usr/lib/python3.11/threading.py", line 1401 in run + # File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner + # File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap + + if hasattr(self, '_tray') and self._tray: + self._tray.hide() + self._settings.close() + + self.bAppExiting = True + LOG.debug(f"stop_app: Killing {self._tox}") + self._kill_toxav() + self._kill_tox() + del self._tox + + oArgs = self._args + if hasattr(oArgs, 'log_oFd'): + LOG.debug(f"Closing {oArgs.log_oFd}") + oArgs.log_oFd.close() + delattr(oArgs, 'log_oFd') + + # App loading + + def _load_base_style(self) -> None: + if self._args.theme in ['', 'default']: return + + if qdarkstyle: + LOG.debug("_load_base_style qdarkstyle " +self._args.theme) + # QDarkStyleSheet + if self._args.theme == 'light': + from qdarkstyle.light.palette import LightPalette + style = qdarkstyle.load_stylesheet(palette=LightPalette) + else: + from qdarkstyle.dark.palette import DarkPalette + style = qdarkstyle.load_stylesheet(palette=DarkPalette) + else: + LOG.debug("_load_base_style qss " +self._args.theme) + name = self._args.theme + '.qss' + with open(util.join_path(util.get_styles_directory(), name)) as fl: + style = fl.read() + style += '\n' +sSTYLE + self._app.setStyleSheet(style) + + def _load_app_styles(self) -> None: + LOG.debug(f"_load_app_styles {list(settings.built_in_themes().keys())}") + # application color scheme + if self._settings['theme'] in ['', 'default']: return + for theme in settings.built_in_themes().keys(): + if self._settings['theme'] != theme: + continue + if qdarkstyle: + LOG.debug("_load_base_style qdarkstyle " +self._args.theme) + # QDarkStyleSheet + if self._args.theme == 'light': + from qdarkstyle.light.palette import LightPalette + style = qdarkstyle.load_stylesheet(palette=LightPalette) + else: + from qdarkstyle.dark.palette import DarkPalette + style = qdarkstyle.load_stylesheet(palette=DarkPalette) + else: + theme_path = settings.built_in_themes()[theme] + file_path = util.join_path(util.get_styles_directory(), theme_path) + if not os.path.isfile(file_path): + LOG.warn('_load_app_styles: no theme file ' + file_path) + continue + with open(file_path) as fl: + style = fl.read() + LOG.debug('_load_app_styles: loading theme file ' + file_path) + style += '\n' +sSTYLE + self._app.setStyleSheet(style) + LOG.info('_load_app_styles: loaded theme ' +self._args.theme) + break + + def _load_login_screen_translations(self) -> None: + LOG.debug("_load_login_screen_translations") + current_language, supported_languages = self._get_languages() + if current_language not in supported_languages: + return + lang_path = supported_languages[current_language] + translator = QtCore.QTranslator() + translator.load(util.get_translations_directory() + lang_path) + self._app.installTranslator(translator) + self._app.translator = translator + + def _load_icon(self) -> None: + LOG.debug("_load_icon") + icon_file = os.path.join(util.get_images_directory(), 'icon.png') + self._app.setWindowIcon(QtGui.QIcon(icon_file)) + + @staticmethod + def _get_languages() -> tuple: + LOG.debug("_get_languages") + current_locale = QtCore.QLocale() + curr_language = current_locale.languageToString(current_locale.language()) + supported_languages = settings.supported_languages() + + return curr_language, supported_languages + + def _load_app_translations(self) -> None: + LOG.debug("_load_app_translations") + lang = settings.supported_languages()[self._settings['language']] + translator = QtCore.QTranslator() + translator.load(os.path.join(util.get_translations_directory(), lang)) + self._app.installTranslator(translator) + self._app.translator = translator + + def _select_and_load_profile(self) -> bool: + LOG.debug("_select_and_load_profile: " +repr(self._path)) + + if self._path is not None: + # toxygen was started with path to profile + try: + assert os.path.exists(self._path), f"FNF {self._path}" + self._load_existing_profile(self._path) + except Exception as e: + LOG.error('_load_existing_profile failed: ' + str(e)) + title = 'Loading the profile failed ' + if self._path: + title += os.path.basename(self._path) + text = 'Loading the profile failed - \n' +str(e) + if 'Dis' == 'Abled': + text += '\nLoading the profile failed - \n' \ + +str(e) +'\nContinue with a default profile?' + reply = util_ui.question(text, title) + if not reply: + LOG.debug('_load_existing_profile not continuing ') + raise + LOG.debug('_load_existing_profile continuing ') + # drop through + else: + util_ui.message_box(text, title) + raise + else: + auto_profile = Settings.get_auto_profile() + if auto_profile is None: # no default profile + LOG.debug('_select_and_load_profile no default profile ') + result = self._select_profile() + if result is None: + LOG.debug('no selected profile ') + return False + if result.is_new_profile(): # create new profile + if not self._create_new_profile(result.profile_path): + LOG.warn('no new profile ') + return False + LOG.debug('created new profile ') + else: # load existing profile + self._load_existing_profile(result.profile_path) + # drop through + self._path = result.profile_path + else: # default profile + LOG.debug('loading default profile ') + self._path = auto_profile + self._load_existing_profile(auto_profile) + + if settings.is_active_profile(self._path): # profile is in use + LOG.warn(f"_select_and_load_profile active: {self._path}") + profile_name = util.get_profile_name_from_path(self._path) + title = util_ui.tr('Profile {}').format(profile_name) + text = util_ui.tr( + 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?') + reply = util_ui.question(text, title) + if not reply: + return False + + # is self._path right - was pathless + self._settings.set_active_profile(self._path) + + return True + + # Threads + + def _start_threads(self, initial_start=True) -> None: + LOG.debug(f"_start_threads before: {threading.enumerate()}") + # init thread + self._init = threads.InitThread(self._tox, + self._plugin_loader, + self._settings, + self, + initial_start) + self._init.start() + def te(): return [t.name for t in threading.enumerate()] + LOG.debug(f"_start_threads init: {te()}") + + # starting threads for tox iterate and toxav iterate + self._main_loop = threads.ToxIterateThread(self._tox, app=self) + self._main_loop.start() + + self._av_loop = threads.ToxAVIterateThread(self._tox.AV) + self._av_loop.start() + + if initial_start: + threads.start_file_transfer_thread() + LOG.debug(f"_start_threads after: {[t.name for t in threading.enumerate()]}") + + def _stop_threads(self, is_app_closing=True) -> None: + LOG.debug("_stop_threads") + self._init.stop_thread(1.0) + + self._av_loop.stop_thread() + self._main_loop.stop_thread() + + if is_app_closing: + threads.stop_file_transfer_thread() + + def iterate(self, n=100) -> None: + interval = self._tox.iteration_interval() + for i in range(n): + self._tox.iterate() + # Cooperative yield, allow gevent to monitor file handles via libevent + gevent.sleep(interval / 1000.0) +#? sleep(interval / 1000.0) + + # Profiles + + def _select_profile(self): + LOG.debug("_select_profile") + if self._args.language != 'English': + self._load_login_screen_translations() + ls = LoginScreen() + profiles = ProfileManager.find_profiles() + ls.update_select(profiles) + ls.show() + self._app.exec_() + return ls.result + + def _load_existing_profile(self, profile_path) -> None: + profile_path = profile_path.replace('.json', '.tox') + LOG.info("_load_existing_profile " +repr(profile_path)) + assert os.path.exists(profile_path), profile_path + self._profile_manager = ProfileManager(self._toxes, profile_path, app=self) + data = self._profile_manager.open_profile() + if self._toxes.is_data_encrypted(data): + LOG.debug("_entering password") + data = self._enter_password(data) + LOG.debug("_entered password") + json_file = profile_path.replace('.tox', '.json') + if os.path.exists(json_file): + LOG.debug("creating _settings from: " +json_file) + self._settings = Settings(self._toxes, json_file, self) + else: + self._settings = Settings.get_default_settings() + + self._tox = self._create_tox(data, self._settings) + LOG.debug("created _tox") + + def _create_new_profile(self, profile_name) -> bool: + LOG.info("_create_new_profile " + profile_name) + result = self._get_create_profile_screen_result() + if result is None: + return False + if result.save_into_default_folder: + profile_path = util.join_path(get_user_config_path(), profile_name + '.tox') + else: + profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') + if os.path.isfile(profile_path): + util_ui.message_box(util_ui.tr('Profile with this name already exists'), + util_ui.tr('Error')) + return False + name = profile_name or 'toxygen_user' + assert self._args + self._path = profile_path + if result.password: + self._toxes.set_password(result.password) + self._settings = Settings(self._toxes, + self._path.replace('.tox', '.json'), + app=self) + self._tox = self._create_tox(None, + self._settings) + self._tox.self_set_name(name if name else 'Toxygen User') + self._tox.self_set_status_message('Toxing on Toxygen') + + self._profile_manager = ProfileManager(self._toxes, profile_path) + try: + self._save_profile() + except Exception as ex: + #? print(ex) + LOG.error('Profile creation exception: ' + str(ex)) + text = util_ui.tr('Profile saving error! Does Toxygen have permission to write to this directory?') + util_ui.message_box(text, util_ui.tr('Error')) + + return False + current_language, supported_languages = self._get_languages() + if current_language in supported_languages: + self._settings['language'] = current_language + self._settings.save() + + return True + + def _get_create_profile_screen_result(self): + LOG.debug("_get_create_profile_screen_result") + cps = CreateProfileScreen() + cps.show() + self._app.exec_() + + return cps.result + + def _save_profile(self, data=None) -> None: + LOG.debug("_save_profile") + data = data or self._tox.get_savedata() + self._profile_manager.save_profile(data) + + # Other private methods + + def _enter_password(self, data): + """ + Show password screen + """ + LOG.debug("_enter_password") + p = password_screen.PasswordScreen(self._toxes, data) + p.show() + self._app.lastWindowClosed.connect(self._app.quit) + self._app.exec_() + if p.result is not None: + return p.result + self._force_exit(0) + return None + + def _reset(self) -> None: + LOG.debug("_reset") + """ + Create new tox instance (new network settings) + :return: tox instance + """ + self._contacts_manager.reset_contacts_statuses() + self._stop_threads(False) + data = self._tox.get_savedata() + self._save_profile(data) + self._kill_toxav() + self._kill_tox() + try: + # create new tox instance + self._tox = self._create_tox(data, self._settings) + assert self._tox + self._start_threads(False) + + tox_savers = [self._friend_factory, self._group_factory, + self._plugin_loader, self._contacts_manager, + self._contacts_provider, self._messenger, + self._file_transfer_handler, + self._groups_service, self._profile] + for tox_saver in tox_savers: + tox_saver.set_tox(self._tox) + + self._calls_manager.set_toxav(self._tox.AV) + self._contacts_manager.update_friends_numbers() + self._contacts_manager.update_groups_lists() + self._contacts_manager.update_groups_numbers() + + self._init_callbacks() + except BaseException as e: + LOG.error(f"_reset : {e}") + LOG.debug('_reset: ' \ + +'\n' + traceback.format_exc()) + title = util_ui.tr('Reset Error') + text = util_ui.tr('Error:') + str(e) + util_ui.message_box(text, title) + + def _create_dependencies(self) -> None: + LOG.info(f"_create_dependencies toxygen version {self._version}") + if hasattr(self._args, 'update') and self._args.update: + self._backup_service = BackupService(self._settings, + self._profile_manager) + self._smiley_loader = SmileyLoader(self._settings) + self._tox_dns = ToxDns(self._settings) + self._ms = MainWindow(self._settings, self._tray, self) + + db_path = self._path.replace('.tox', '.db') + db = Database(db_path, self._toxes) + if os.path.exists(db_path) and hasattr(db, 'open'): + db.open() + + assert self._tox + + contact_items_factory = ContactItemsFactory(self._settings, self._ms) + self._friend_factory = FriendFactory(self._profile_manager, + self._settings, + self._tox, + db, + contact_items_factory) + self._group_factory = GroupFactory(self._profile_manager, + self._settings, + self._tox, + db, + contact_items_factory) + self._group_peer_factory = GroupPeerFactory(self._tox, + self._profile_manager, + db, + contact_items_factory) + self._contacts_provider = ContactProvider(self._tox, + self._friend_factory, + self._group_factory, + self._group_peer_factory, + app=self) + self._profile = Profile(self._profile_manager, + self._tox, + self._ms, + self._contacts_provider, + self._reset) + self._init_profile() + self._plugin_loader = PluginLoader(self._settings, self) + history = None + messages_items_factory = MessagesItemsFactory(self._settings, + self._plugin_loader, + self._smiley_loader, + self._ms, + lambda m: history.delete_message(m)) + history = History(self._contacts_provider, db, + self._settings, self._ms, messages_items_factory) + self._contacts_manager = ContactsManager(self._tox, + self._settings, + self._ms, + self._profile_manager, + self._contacts_provider, + history, + self._tox_dns, + messages_items_factory) + history.set_contacts_manager(self._contacts_manager) + self._history = history + self._calls_manager = CallsManager(self._tox.AV, + self._settings, + self._ms, + self._contacts_manager, + self) + self._messenger = Messenger(self._tox, + self._plugin_loader, self._ms, self._contacts_manager, + self._contacts_provider, messages_items_factory, self._profile, + self._calls_manager) + file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, + self._profile, self._ms) + self._file_transfer_handler = FileTransfersHandler(self._tox, self._settings, self._contacts_provider, + file_transfers_message_service, self._profile) + messages_items_factory.set_file_transfers_handler(self._file_transfer_handler) + widgets_factory = None + widgets_factory_provider = Provider(lambda: widgets_factory) + self._groups_service = GroupsService(self._tox, + self._contacts_manager, + self._contacts_provider, + self._ms, + widgets_factory_provider) + widgets_factory = WidgetsFactory(self._settings, + self._profile, + self._profile_manager, + self._contacts_manager, + self._file_transfer_handler, + self._smiley_loader, + self._plugin_loader, + self._toxes, + self._version, + self._groups_service, + history, + self._contacts_provider) + if bSHOW_TRAY: + self._tray = tray.init_tray(self._profile, + self._settings, + self._ms, self._toxes) + self._ms.set_dependencies(widgets_factory, + self._tray, + self._contacts_manager, + self._messenger, + self._profile, + self._plugin_loader, + self._file_transfer_handler, + history, + self._calls_manager, + self._groups_service, self._toxes, self) + + if bSHOW_TRAY: # broken + # the tray icon does not die with the app + self._tray.show() + self._ms.show() + + # FixMe: + self._log = lambda line: LOG.log(self._args.loglevel, + self._ms.status(line)) + # self._ms._log = self._log # was used in callbacks.py + + if False: + self.status_handler = logging.Handler() + self.status_handler.setLevel(logging.INFO) # self._args.loglevel + self.status_handler.handle = self._ms.status + + self._init_callbacks() + LOG.info("_create_dependencies toxygen version " +self._version) + + def _try_to_update(self): + LOG.debug("_try_to_update") + updating = updater.start_update_if_needed(self._version, self._settings) + if updating: + LOG.info("Updating toxygen version " +self._version) + self._save_profile() + self._settings.close() + self._kill_toxav() + self._kill_tox() + return updating + + def _create_tox(self, data, settings_): + LOG.info("_create_tox calling tox_factory") + assert self._args + retval = tox_factory(data=data, settings=settings_, + args=self._args, app=self) + LOG.debug("_create_tox succeeded") + self._tox = retval + return retval + + def _force_exit(self, retval=0) -> None: + LOG.debug("_force_exit") + sys.exit(0) + + def _init_callbacks(self, ms=None) -> None: + LOG.debug("_init_callbacks") + # this will block if you are not connected + callbacks.init_callbacks(self._tox, self._profile, self._settings, + self._plugin_loader, self._contacts_manager, + self._calls_manager, + self._file_transfer_handler, self._ms, + self._tray, + self._messenger, self._groups_service, + self._contacts_provider, self._ms) + + def _init_profile(self) -> None: + LOG.debug("_init_profile") + if not self._profile.has_avatar(): + self._profile.reset_avatar(self._settings['identicons']) + + def _kill_toxav(self) -> None: +# LOG_debug("_kill_toxav") + self._calls_manager.set_toxav(None) + self._tox.AV.kill() + + def _kill_tox(self) -> None: +# LOG.debug("_kill_tox") + self._tox.kill() + + def loop(self, n) -> None: + """ + Im guessing - there are 4 sleeps - time, tox, and Qt gevent + """ + interval = self._tox.iteration_interval() + for i in range(n): + self._tox.iterate() + #? QtCore.QThread.msleep(interval) + # Cooperative yield, allow gevent to monitor file handles via libevent + gevent.sleep(interval / 1000.0) + # NO? + QtCore.QCoreApplication.processEvents() + + def _test_tox(self) -> None: + self.test_net(iMax=8) + self._ms.log_console() + + def test_net(self, lElts=None, oThread=None, iMax=4) -> None: + + # bootstrap + LOG.debug('test_net: Calling generate_nodes: udp') + lNodes = ts.generate_nodes(oArgs=self._args, + ipv='ipv4', + udp_not_tcp=True) + self._settings['current_nodes_udp'] = lNodes + if not lNodes: + LOG.warn('empty generate_nodes udp') + LOG.debug('test_net: Calling generate_nodes: tcp') + lNodes = ts.generate_nodes(oArgs=self._args, + ipv='ipv4', + udp_not_tcp=False) + self._settings['current_nodes_tcp'] = lNodes + if not lNodes: + LOG.warn('empty generate_nodes tcp') + + # if oThread and oThread._stop_thread: return + LOG.debug("test_net network=" +self._args.network +' iMax=' +str(iMax)) + if self._args.network not in ['local', 'localnew', 'newlocal']: + b = ts.bAreWeConnected() + if b is None: + i = os.system('ip route|grep ^def') + if i > 0: + b = False + else: + b = True + if not b: + LOG.warn("No default route for network " +self._args.network) + text = 'You have no default route - are you connected?' + reply = util_ui.question(text, "Are you connected?") + if not reply: return + iMax = 1 + else: + LOG.debug("Have default route for network " +self._args.network) + + lUdpElts = self._settings['current_nodes_udp'] + if self._args.proxy_type <= 0 and not lUdpElts: + title = 'test_net Error' + text = 'Error: ' + str('No UDP nodes') + util_ui.message_box(text, title) + return + lTcpElts = self._settings['current_nodes_tcp'] + if self._args.proxy_type > 0 and not lTcpElts: + title = 'test_net Error' + text = 'Error: ' + str('No TCP nodes') + util_ui.message_box(text, title) + return + LOG.debug(f"test_net {self._args.network} lenU={len(lUdpElts)} lenT={len(lTcpElts)} iMax={iMax}") + i = 0 + while i < iMax: + # if oThread and oThread._stop_thread: return + i = i + 1 + LOG.debug(f"bootstrapping status proxy={self._args.proxy_type} # {i}") + if self._args.proxy_type == 0: + self._test_bootstrap(lUdpElts) + else: + self._test_bootstrap([lUdpElts[0]]) + LOG.debug(f"relaying status # {i}") + self._test_relays(self._settings['current_nodes_tcp']) + status = self._tox.self_get_connection_status() + if status > 0: + LOG.info(f"Connected # {i}" +' : ' +repr(status)) + break + LOG.trace(f"Connected status #{i}: {status}") + + def _test_env(self) -> None: + _settings = self._settings + if 'proxy_type' not in _settings or _settings['proxy_type'] == 0 or \ + not _settings['proxy_host'] or not _settings['proxy_port']: + env = dict( prot = 'ipv4') + lElts = self._settings['current_nodes_udp'] + elif _settings['proxy_type'] == 2: + env = dict(prot = 'socks5', + https_proxy='', \ + socks_proxy='socks5://' \ + +_settings['proxy_host'] +':' \ + +str(_settings['proxy_port'])) + lElts = self._settings['current_nodes_tcp'] + elif _settings['proxy_type'] == 1: + env = dict(prot = 'https', + socks_proxy='', \ + https_proxy='http://' \ + +_settings['proxy_host'] +':' \ + +str(_settings['proxy_port'])) + lElts = _settings['current_nodes_tcp'] +# LOG.debug(f"test_env {len(lElts)}") + return env + + def _test_bootstrap(self, lElts=None) -> None: + if lElts is None: + lElts = self._settings['current_nodes_udp'] + LOG.debug(f"_test_bootstrap #Elts={len(lElts)}") + if not lElts: + return + shuffle(lElts) + ts.bootstrap_udp(lElts[:iNODES], [self._tox]) + LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) + + def _test_relays(self, lElts=None) -> None: + if lElts is None: + lElts = self._settings['current_nodes_tcp'] + shuffle(lElts) + LOG.debug(f"_test_relays {len(lElts)}") + ts.bootstrap_tcp(lElts[:iNODES], [self._tox]) + + def _test_nmap(self, lElts=None) -> None: + LOG.debug("_test_nmap") + if not self._tox: return + title = 'Extended Test Suite' + text = 'Run the Extended Test Suite?\nThe program may freeze for 1-10 minutes.' + i = os.system('ip route|grep ^def >/dev/null') + if i > 0: + text += '\nYou have no default route - are you connected?' + reply = util_ui.question(text, title) + if not reply: return + + if self._args.proxy_type == 0: + sProt = "udp4" + else: + sProt = "tcp4" + if lElts is None: + if self._args.proxy_type == 0: + lElts = self._settings['current_nodes_udp'] + else: + lElts = self._settings['current_nodes_tcp'] + shuffle(lElts) + try: + ts.bootstrap_iNmapInfo(lElts, self._args, sProt) + except Exception as e: + LOG.error(f"test_nmap ' +' : {e}") + LOG.error('_test_nmap(): ' \ + +'\n' + traceback.format_exc()) + title = 'Test Suite Error' + text = 'Error: ' + str(e) + util_ui.message_box(text, title) + + # LOG.info("Connected status: " +repr(self._tox.self_get_connection_status())) + self._ms.log_console() + + def _test_main(self) -> None: + from toxygen_toxygen_wrapper.toxygen_wrapper.tests.tests_wrapper import main as tests_main + LOG.debug("_test_main") + if not self._tox: return + title = 'Extended Test Suite' + text = 'Run the Extended Test Suite?\nThe program may freeze for 20-60 minutes.' + reply = util_ui.question(text, title) + if reply: + if hasattr(self._args, 'proxy_type') and self._args.proxy_type: + lArgs = ['--proxy_host', self._args.proxy_host, + '--proxy_port', str(self._args.proxy_port), + '--proxy_type', str(self._args.proxy_type), ] + else: + lArgs = list() + try: + tests_main(lArgs) + except Exception as e: + LOG.error(f"_test_socks(): {e}") + LOG.error('_test_socks(): ' \ + +'\n' + traceback.format_exc()) + title = 'Extended Test Suite Error' + text = 'Error:' + str(e) + util_ui.message_box(text, title) + self._ms.log_console() + +#? unused +class GEventProcessing: + """Interoperability class between Qt/gevent that allows processing gevent + tasks during Qt idle periods.""" + def __init__(self, idle_period=IDLE_PERIOD): + # Limit the IDLE handler's frequency while still allow for gevent + # to trigger a microthread anytime + self._idle_period = idle_period + # IDLE timer: on_idle is called whenever no Qt events left for + # processing + self._timer = QTimer() + self._timer.timeout.connect(self.process_events) + self._timer.start(0) + def __enter__(self) -> None: + pass + + def __exit__(self, *exc_info) -> None: + self._timer.stop() + + def process_events(self, idle_period=None) -> None: + if idle_period is None: + idle_period = self._idle_period + # Cooperative yield, allow gevent to monitor file handles via libevent + gevent.sleep(idle_period) + #? QtCore.QCoreApplication.processEvents() diff --git a/toxygen/av/__init__.py b/toxygen/av/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/av/call.py b/toxygen/av/call.py new file mode 100644 index 0000000..73caa25 --- /dev/null +++ b/toxygen/av/call.py @@ -0,0 +1,54 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class Call: + + def __init__(self, out_audio, out_video, in_audio=False, in_video=False): + self._in_audio = in_audio + self._in_video = in_video + self._out_audio = out_audio + self._out_video = out_video + self._is_active = False + + def get_is_active(self): + return self._is_active + + def set_is_active(self, value): + self._is_active = value + + is_active = property(get_is_active, set_is_active) + + # Audio + + def get_in_audio(self): + return self._in_audio + + def set_in_audio(self, value): + self._in_audio = value + + in_audio = property(get_in_audio, set_in_audio) + + def get_out_audio(self): + return self._out_audio + + def set_out_audio(self, value): + self._out_audio = value + + out_audio = property(get_out_audio, set_out_audio) + + # Video + + def get_in_video(self): + return self._in_video + + def set_in_video(self, value): + self._in_video = value + + in_video = property(get_in_video, set_in_video) + + def get_out_video(self): + return self._out_video + + def set_out_video(self, value): + self._out_video = value + + out_video = property(get_out_video, set_out_video) diff --git a/toxygen/av/calls.py b/toxygen/av/calls.py new file mode 100644 index 0000000..9b40fc1 --- /dev/null +++ b/toxygen/av/calls.py @@ -0,0 +1,587 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import time +import threading +import logging +import itertools + +from toxygen_wrapper.toxav_enums import * +from toxygen_wrapper.tests import support_testing as ts +from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +with ts.ignoreStderr(): + import pyaudio +from av import screen_sharing +from av.call import Call +import common.tox_save +from middleware.threads import BaseQThread + +from utils import ui as util_ui +from middleware.threads import invoke_in_main_thread +# from middleware.threads import BaseThread + +sleep = time.sleep + +global LOG +LOG = logging.getLogger('app.'+__name__) + +TIMER_TIMEOUT = 30.0 +iFPS = 25 + +class AudioThread(BaseQThread): + def __init__(self, av, name=''): + super().__init__() + self.av = av + self._name = name + + def join(self, ito=ts.iTHREAD_TIMEOUT): + LOG_DEBUG(f"AudioThread join {self}") + # dunno + + def run(self) -> None: + LOG_DEBUG('AudioThread run: ') + # maybe not needed + while not self._stop_thread: + self.av.send_audio() + sleep(100.0 / 1000.0) + +class VideoThread(BaseQThread): + def __init__(self, av, name=''): + super().__init__() + self.av = av + self._name = name + + def join(self, ito=ts.iTHREAD_TIMEOUT): + LOG_DEBUG(f"VideoThread join {self}") + # dunno + + def run(self) -> None: + LOG_DEBUG('VideoThread run: ') + # maybe not needed + while not self._stop_thread: + self.av.send_video() + sleep(100.0 / 1000.0) + +class AV(common.tox_save.ToxAvSave): + + def __init__(self, toxav, settings): + super().__init__(toxav) + self._toxav = toxav + self._settings = settings + self._running = True + s = settings + if 'video' not in s: + LOG.warn("AV.__init__ 'video' not in s" ) + LOG.debug(f"AV.__init__ {s}" ) + elif 'device' not in s['video']: + LOG.warn("AV.__init__ 'device' not in s.video" ) + LOG.debug(f"AV.__init__ {s['video']}" ) + + self._calls = {} # dict: key - friend number, value - Call instance + + self._audio = None + self._audio_stream = None + self._audio_thread = None + self._audio_running = False + self._out_stream = None + + self._audio_channels = 1 + self._audio_duration = 60 + self._audio_rate_pa = 48000 + self._audio_rate_tox = 48000 + self._audio_rate_pa = 48000 + self._audio_krate_tox_audio = self._audio_rate_tox // 1000 + self._audio_krate_tox_video = 5000 + self._audio_sample_count_pa = self._audio_rate_pa * self._audio_channels * self._audio_duration // 1000 + self._audio_sample_count_tox = self._audio_rate_tox * self._audio_channels * self._audio_duration // 1000 + + self._video = None + self._video_thread = None + self._video_running = None + + self._video_width = 320 + self._video_height = 240 + + # was iOutput = self._settings._args.audio['output'] + iInput = self._settings['audio']['input'] + self.lPaSampleratesI = ts.lSdSamplerates(iInput) + iOutput = self._settings['audio']['output'] + self.lPaSampleratesO = ts.lSdSamplerates(iOutput) + + global oPYA + oPYA = self._audio = pyaudio.PyAudio() + + def stop(self) -> None: + LOG_DEBUG(f"AV.CA stop {self._video_thread}") + self._running = False + self.stop_audio_thread() + self.stop_video_thread() + + def __contains__(self, friend_number:int) -> bool: + return friend_number in self._calls + + # Calls + + def __call__(self, friend_number, audio, video): + """Call friend with specified number""" + if friend_number in self._calls: + LOG.warn(f"__call__ already has {friend_number}") + return + if self._audio_krate_tox_audio not in ts.lToxSampleratesK: + LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") + + try: + self._toxav.call(friend_number, + self._audio_krate_tox_audio if audio else 0, + self._audio_krate_tox_video if video else 0) + except Exception as e: + LOG.warn(f"_toxav.call already has {friend_number}") + return + self._calls[friend_number] = Call(audio, video) + threading.Timer(TIMER_TIMEOUT, + lambda: self.finish_not_started_call(friend_number)).start() + + def accept_call(self, friend_number, audio_enabled, video_enabled): + # obsolete + self.call_accept_call(friend_number, audio_enabled, video_enabled) + + def call_accept_call(self, friend_number, audio_enabled, video_enabled) -> None: + # called from CM.accept_call in a try: + LOG.debug(f"call_accept_call from F={friend_number} R={self._running}" + + f" A={audio_enabled} V={video_enabled}") + # import pdb; pdb.set_trace() - gets into q Qt exec_ problem + # ts.trepan_handler() + + if self._audio_krate_tox_audio not in ts.lToxSampleratesK: + LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}") + if self._running: + self._calls[friend_number] = Call(audio_enabled, video_enabled) + # audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + # video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + try: + self._toxav.answer(friend_number, + self._audio_krate_tox_audio if audio_enabled else 0, + self._audio_krate_tox_video if video_enabled else 0) + except Exception as e: + LOG.error(f"AV accept_call error from {friend_number} {self._running} {e}") + raise + if video_enabled: + # may raise + self.start_video_thread() + if audio_enabled: + LOG.debug(f"calls accept_call calling start_audio_thread F={friend_number}") + # may raise + self.start_audio_thread() + + def finish_call(self, friend_number, by_friend=False) -> None: + LOG.debug(f"finish_call {friend_number}") + if friend_number in self._calls: + del self._calls[friend_number] + try: + # AttributeError: 'int' object has no attribute 'out_audio' + if not len(list(filter(lambda c: c.out_audio, self._calls))): + self.stop_audio_thread() + if not len(list(filter(lambda c: c.out_video, self._calls))): + self.stop_video_thread() + except Exception as e: + LOG.error(f"finish_call FixMe: {e}") + # dunno + self.stop_audio_thread() + self.stop_video_thread() + if not by_friend: + LOG.debug(f"finish_call before call_control {friend_number}") + self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) + LOG.debug(f"finish_call after call_control {friend_number}") + + def finish_not_started_call(self, friend_number:int) -> None: + if friend_number in self: + call = self._calls[friend_number] + if not call.is_active: + self.finish_call(friend_number) + + def toxav_call_state_cb(self, friend_number, state) -> None: + """ + New call state + """ + LOG.debug(f"toxav_call_state_cb {friend_number}") + call = self._calls[friend_number] + call.is_active = True + + call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A'] > 0 + call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V'] > 0 + + if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio: + self.start_audio_thread() + + if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video: + self.start_video_thread() + + def is_video_call(self, number) -> bool: + return number in self and self._calls[number].in_video + + # Threads + + def start_audio_thread(self, bSTREAM_CALLBACK=False) -> None: + """ + Start audio sending + from a callback + """ + # called from call_accept_call in an try: from CM.accept_call + global oPYA + # was iInput = self._settings._args.audio['input'] + iInput = self._settings['audio']['input'] + if self._audio_thread is not None: + LOG_WARN(f"start_audio_thread device={iInput}") + return + LOG_DEBUG(f"start_audio_thread device={iInput}") + lPaSamplerates = ts.lSdSamplerates(iInput) + if not(len(lPaSamplerates)): + e = f"No sample rates for device: audio[input]={iInput}" + LOG_WARN(f"start_audio_thread {e}") + #?? dunno - cancel call? - no let the user do it + # return + # just guessing here in case that's a false negative + lPaSamplerates = [round(oPYA.get_device_info_by_index(iInput)['defaultSampleRate'])] + if lPaSamplerates and self._audio_rate_pa in lPaSamplerates: + pass + elif lPaSamplerates: + LOG_WARN(f"Setting audio_rate to: {lPaSamplerates[0]}") + self._audio_rate_pa = lPaSamplerates[0] + elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iInput): + self._audio_rate_pa = oPYA.get_device_info_by_index(iInput)['defaultSampleRate'] + LOG_WARN(f"setting to defaultSampleRate") + else: + LOG_WARN(f"{self._audio_rate_pa} not in {lPaSamplerates}") + # a float is in here - must it be int? + if type(self._audio_rate_pa) == float: + self._audio_rate_pa = round(self._audio_rate_pa) + try: + if self._audio_rate_pa not in lPaSamplerates: + LOG_WARN(f"PAudio sampling rate was {self._audio_rate_pa} changed to {lPaSamplerates[0]}") + LOG_DEBUG(f"lPaSamplerates={lPaSamplerates}") + self._audio_rate_pa = lPaSamplerates[0] + else: + LOG_DEBUG( f"start_audio_thread framerate: {self._audio_rate_pa}" \ + +f" device: {iInput}" + +f" supported: {lPaSamplerates}") + + if bSTREAM_CALLBACK: + # why would you not call a thread? + self._audio_stream = oPYA.open(format=pyaudio.paInt16, + rate=self._audio_rate_pa, + channels=self._audio_channels, + input=True, + input_device_index=iInput, + frames_per_buffer=self._audio_sample_count_pa * 10, + stream_callback=self.send_audio_data) + self._audio_running = True + self._audio_stream.start_stream() + while self._audio_stream.is_active(): + sleep(0.1) + self._audio_stream.stop_stream() + self._audio_stream.close() + else: + LOG_DEBUG( f"start_audio_thread starting thread {self._audio_rate_pa}") + self._audio_stream = oPYA.open(format=pyaudio.paInt16, + rate=self._audio_rate_pa, + channels=self._audio_channels, + input=True, + input_device_index=iInput, + frames_per_buffer=self._audio_sample_count_pa * 10) + self._audio_running = True + self._audio_thread = AudioThread(self, + name='_audio_thread') + self._audio_thread.start() + LOG_DEBUG( f"start_audio_thread started thread name='_audio_thread'") + + except Exception as e: + LOG_ERROR(f"Starting self._audio.open {e}") + LOG_DEBUG(repr(dict(format=pyaudio.paInt16, + rate=self._audio_rate_pa, + channels=self._audio_channels, + input=True, + input_device_index=iInput, + frames_per_buffer=self._audio_sample_count_pa * 10))) + # catcher in place in calls_manager? yes accept_call + # calls_manager._call.toxav_call_state_cb(friend_number, mask) + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Starting self._audio.open")) + return + else: + LOG_DEBUG(f"start_audio_thread {self._audio_stream}") + + def stop_audio_thread(self) -> None: + LOG_DEBUG(f"stop_audio_thread {self._audio_stream}") + + if self._audio_thread is None: + return + self._audio_running = False + self._audio_thread._stop_thread = True + + self._audio_thread = None + self._audio_stream = None + self._audio = None + + if self._out_stream is not None: + self._out_stream.stop_stream() + self._out_stream.close() + self._out_stream = None + + def start_video_thread(self) -> None: + if self._video_thread is not None: + return + s = self._settings + if 'video' not in s: + LOG.warn("AV.__init__ 'video' not in s" ) + LOG.debug(f"start_video_thread {s}" ) + raise RuntimeError("start_video_thread not 'video' in s)" ) + if 'device' not in s['video']: + LOG.error("start_video_thread not 'device' in s['video']" ) + LOG.debug(f"start_video_thread {s['video']}" ) + raise RuntimeError("start_video_thread not 'device' ins s['video']" ) + self._video_width = s['video']['width'] + self._video_height = s['video']['height'] + + # dunno + if s['video']['device'] == -1: + self._video = screen_sharing.DesktopGrabber(s['video']['x'], + s['video']['y'], + s['video']['width'], + s['video']['height']) + else: + with ts.ignoreStdout(): import cv2 + if s['video']['device'] == 0: + # webcam + self._video = cv2.VideoCapture(s['video']['device'], cv2.DSHOW) + else: + self._video = cv2.VideoCapture(s['video']['device']) + self._video.set(cv2.CAP_PROP_FPS, iFPS) + self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width) + self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height) +# self._video.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) + if self._video is None: + LOG.error("start_video_thread " \ + +f" device: {s['video']['device']}" \ + +f" supported: {s['video']['width']} {s['video']['height']}") + return + LOG.info("start_video_thread " \ + +f" device: {s['video']['device']}" \ + +f" supported: {s['video']['width']} {s['video']['height']}") + + self._video_running = True + self._video_thread = VideoThread(self, + name='_video_thread') + self._video_thread.start() + + def stop_video_thread(self) -> None: + LOG_DEBUG(f"stop_video_thread {self._video_thread}") + if self._video_thread is None: + return + + self._video_thread._stop_thread = True + self._video_running = False + i = 0 + while i < ts.iTHREAD_JOINS: + self._video_thread.join(ts.iTHREAD_TIMEOUT) + try: + if not self._video_thread.is_alive(): break + except: + break + i = i + 1 + else: + LOG.warn("self._video_thread.is_alive BLOCKED") + self._video_thread = None + self._video = None + # Incoming chunks + + def audio_chunk(self, samples, channels_count, rate) -> None: + """ + Incoming chunk + """ + # from callback + if self._out_stream is None: + # was iOutput = self._settings._args.audio['output'] + iOutput = self._settings['audio']['output'] + if self.lPaSampleratesO and rate in self.lPaSampleratesO: + LOG_DEBUG(f"Using rate {rate} in self.lPaSampleratesO") + elif self.lPaSampleratesO: + LOG_WARN(f"{rate} not in {self.lPaSampleratesO}") + LOG_WARN(f"Setting audio_rate to: {self.lPaSampleratesO[0]}") + rate = self.lPaSampleratesO[0] + elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iOutput): + rate = round(oPYA.get_device_info_by_index(iOutput)['defaultSampleRate']) + LOG_WARN(f"Setting rate to {rate} empty self.lPaSampleratesO") + else: + LOG_WARN(f"Using rate {rate} empty self.lPaSampleratesO") + if type(rate) == float: + rate = round(rate) + # test output device? + # [Errno -9985] Device unavailable + try: + with ts.ignoreStderr(): + self._out_stream = oPYA.open(format=pyaudio.paInt16, + channels=channels_count, + rate=rate, + output_device_index=iOutput, + output=True) + except Exception as e: + LOG_ERROR(f"Error playing audio_chunk creating self._out_stream output_device_index={iOutput} {e}") + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Error Chunking audio")) + # dunno + self.stop() + return + + iOutput = self._settings['audio']['output'] +#trace LOG_DEBUG(f"audio_chunk output_device_index={iOutput} rate={rate} channels={channels_count}") + try: + self._out_stream.write(samples) + except Exception as e: + # OSError: [Errno -9999] Unanticipated host error + LOG_WARN(f"audio_chunk output_device_index={iOutput} {e}") + + # AV sending + + def send_audio_data(self, data, count, *largs, **kwargs) -> None: + # callback + pcm = data + # :param sampling_rate: Audio sampling rate used in this frame. + try: + if self._toxav is None: + LOG_ERROR("_toxav not initialized") + return + if self._audio_rate_tox not in ts.lToxSamplerates: + LOG_WARN(f"ToxAudio sampling rate was {self._audio_rate_tox} changed to {ts.lToxSamplerates[0]}") + self._audio_rate_tox = ts.lToxSamplerates[0] + + for friend_num in self._calls: + if self._calls[friend_num].out_audio: + # app.av.calls ERROR Error send_audio audio_send_frame: This client is currently not in a call with the friend. + self._toxav.audio_send_frame(friend_num, + pcm, + count, + self._audio_channels, + self._audio_rate_tox) + + except Exception as e: + LOG.error(f"Error send_audio_data audio_send_frame: {e}") + LOG.debug(f"send_audio_data self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}") + self.stop_audio_thread() + invoke_in_main_thread(util_ui.message_box, + str(e), + util_ui.tr("Error send_audio_data audio_send_frame")) + #? stop ? endcall? + + def send_audio(self) -> None: + """ + This method sends audio to friends + """ + i=0 + count = self._audio_sample_count_tox + LOG_DEBUG(f"send_audio stream={self._audio_stream}") + while self._audio_running: + try: + pcm = self._audio_stream.read(count, exception_on_overflow=False) + if not pcm: + sleep(0.1) + else: + self.send_audio_data(pcm, count) + except: + LOG_DEBUG(f"error send_audio {i}") + else: + LOG_TRACE(f"send_audio {i}") + i += 1 + sleep(0.01) + + def send_video(self) -> None: + """ + This method sends video to friends + """ +# LOG_DEBUG(f"send_video thread={threading.current_thread().name}" +# +f" self._video_running={self._video_running}" +# +f" device: {self._settings['video']['device']}" ) + while self._video_running: + try: + result, frame = self._video.read() + if not result: + LOG_WARN(f"send_video video_send_frame _video.read result={result}") + break + if frame is None: + LOG_WARN(f"send_video video_send_frame _video.read result={result} frame={frame}") + continue + + LOG_TRACE(f"send_video video_send_frame _video.read result={result}") + height, width, channels = frame.shape + friends = [] + for friend_num in self._calls: + if self._calls[friend_num].out_video: + friends.append(friend_num) + if len(friends) == 0: + LOG_WARN(f"send_video video_send_frame no friends") + else: + LOG_TRACE(f"send_video video_send_frame {friends}") + friend_num = friends[0] + try: + y, u, v = self.convert_bgr_to_yuv(frame) + self._toxav.video_send_frame(friend_num, width, height, y, u, v) + except Exception as e: + LOG_WARN(f"send_video video_send_frame ERROR {e}") + pass + + except Exception as e: + LOG_ERROR(f"send_video video_send_frame {e}") + pass + + sleep( 1.0/iFPS) + + def convert_bgr_to_yuv(self, frame) -> tuple: + """ + :param frame: input bgr frame + :return y, u, v: y, u, v values of frame + + How this function works: + OpenCV creates YUV420 frame from BGR + This frame has following structure and size: + width, height - dim of input frame + width, height * 1.5 - dim of output frame + + width + ------------------------- + | | + | Y | height + | | + ------------------------- + | | | + | U even | U odd | height // 4 + | | | + ------------------------- + | | | + | V even | V odd | height // 4 + | | | + ------------------------- + + width // 2 width // 2 + + Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable() + Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes + """ + with ts.ignoreStdout(): + import cv2 + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) + + y = frame[:self._video_height, :] + y = list(itertools.chain.from_iterable(y)) + + import numpy as np + # was np.int + u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) + u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2] + u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:] + u = list(itertools.chain.from_iterable(u)) + v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32) + v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2] + v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:] + v = list(itertools.chain.from_iterable(v)) + + return bytes(y), bytes(u), bytes(v) diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py new file mode 100644 index 0000000..d0d6683 --- /dev/null +++ b/toxygen/av/calls_manager.py @@ -0,0 +1,184 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import sys +import threading +import traceback +import logging + +from qtpy import QtCore + +import av.calls +from messenger.messages import * +from ui import av_widgets +import common.event as event +import utils.ui as util_ui +from toxygen_wrapper.tests import support_testing as ts + +global LOG +LOG = logging.getLogger('app.'+__name__) + +class CallsManager: + + def __init__(self, toxav, settings, main_screen, contacts_manager, app=None): + self._callav = av.calls.AV(toxav, settings) # object with data about calls + self._call = self._callav + self._call_widgets = {} # dict of incoming call widgets + self._incoming_calls = set() + self._settings = settings + self._main_screen = main_screen + self._contacts_manager = contacts_manager + self._call_started_event = event.Event() # friend_number, audio, video, is_outgoing + self._call_finished_event = event.Event() # friend_number, is_declined + self._app = app + + def set_toxav(self, toxav) -> None: + self._callav.set_toxav(toxav) + + # Events + + def get_call_started_event(self): + return self._call_started_event + + call_started_event = property(get_call_started_event) + + def get_call_finished_event(self): + return self._call_finished_event + + call_finished_event = property(get_call_finished_event) + + # AV support + + def call_click(self, audio=True, video=False) -> None: + """User clicked audio button in main window""" + num = self._contacts_manager.get_active_number() + if not self._contacts_manager.is_active_a_friend(): + return + if num not in self._callav and self._contacts_manager.is_active_online(): # start call + if not self._settings['audio']['enabled']: + return + self._callav(num, audio, video) + self._main_screen.active_call() + self._call_started_event(num, audio, video, True) + elif num in self._callav: # finish or cancel call if you call with active friend + self.stop_call(num, False) + + def incoming_call(self, audio, video, friend_number) -> None: + """ + Incoming call from friend. + """ + LOG.debug(f"CM incoming_call {friend_number}") + # if not self._settings['audio']['enabled']: return + friend = self._contacts_manager.get_friend_by_number(friend_number) + self._call_started_event(friend_number, audio, video, False) + self._incoming_calls.add(friend_number) + if friend_number == self._contacts_manager.get_active_number(): + self._main_screen.incoming_call() + else: + friend.actions = True + text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") + self._call_widgets[friend_number] = self._get_incoming_call_widget(friend_number, text, friend.name) + self._call_widgets[friend_number].set_pixmap(friend.get_pixmap()) + self._call_widgets[friend_number].show() + + def accept_call(self, friend_number, audio, video) -> None: + """ + Accept incoming call with audio or video + Called from a thread + """ + + LOG.debug(f"CM accept_call from friend_number={friend_number} {audio} {video}") + sys.stdout.flush() + + try: + self._main_screen.active_call() + # failsafe added somewhere this was being left up + self.close_call(friend_number) + QtCore.QCoreApplication.processEvents() + + self._callav.call_accept_call(friend_number, audio, video) + LOG.debug(f"accept_call _call.accept_call CALLED f={friend_number}") + except Exception as e: + # + LOG.error(f"accept_call _call.accept_call ERROR for {friend_number} {e}") + LOG.debug(traceback.print_exc()) + self._main_screen.call_finished() + if hasattr(self._main_screen, '_settings') and \ + 'audio' in self._main_screen._settings and \ + 'input' in self._main_screen._settings['audio']: + iInput = self._settings['audio']['input'] + iOutput = self._settings['audio']['output'] + iVideo = self._settings['video']['device'] + LOG.debug(f"iInput={iInput} iOutput={iOutput} iVideo={iVideo}") + elif hasattr(self._main_screen, '_settings') and \ + hasattr(self._main_screen._settings, 'audio') and \ + 'input' not in self._main_screen._settings['audio']: + LOG.warn(f"'audio' not in {self._main_screen._settings}") + elif hasattr(self._main_screen, '_settings') and \ + hasattr(self._main_screen._settings, 'audio') and \ + 'input' not in self._main_screen._settings['audio']: + LOG.warn(f"'audio' not in {self._main_screen._settings}") + else: + LOG.warn(f"_settings not in self._main_screen") + util_ui.message_box(str(e), + util_ui.tr('ERROR Accepting call from {friend_number}')) + finally: + # does not terminate call - just the av_widget + LOG.debug(f"CM.accept_call close av_widget") + self.close_call(friend_number) + LOG.debug(f" closed self._call_widgets[{friend_number}]") + + def close_call(self, friend_number:int) -> None: + # refactored out from above because the accept window not getting + # taken down in some accept audio calls + LOG.debug(f"close_call {friend_number}") + try: + if friend_number in self._call_widgets: + self._call_widgets[friend_number].close() + del self._call_widgets[friend_number] + if friend_number in self._incoming_calls: + self._incoming_calls.remove(friend_number) + except Exception as e: + # RuntimeError: wrapped C/C++ object of type IncomingCallWidget has been deleted + + LOG.warn(f" closed self._call_widgets[{friend_number}] {e}") + # invoke_in_main_thread(QtCore.QCoreApplication.processEvents) + QtCore.QCoreApplication.processEvents() + + + def stop_call(self, friend_number, by_friend) -> None: + """ + Stop call with friend + """ + LOG.debug(f"CM.stop_call friend={friend_number}") + if friend_number in self._incoming_calls: + self._incoming_calls.remove(friend_number) + is_declined = True + else: + is_declined = False + if friend_number in self._call_widgets: + LOG.debug(f"CM.stop_call _call_widgets close") + self.close_call(friend_number) + + LOG.debug(f"CM.stop_call _main_screen.call_finished") + self._main_screen.call_finished() + self._callav.finish_call(friend_number, by_friend) # finish or decline call + is_video = self._callav.is_video_call(friend_number) + if is_video: + def destroy_window(): + #??? FixMe + with ts.ignoreStdout(): import cv2 + cv2.destroyWindow(str(friend_number)) + LOG.debug(f"CM.stop_call destroy_window") + threading.Timer(2.0, destroy_window).start() + + LOG.debug(f"CM.stop_call _call_finished_event") + self._call_finished_event(friend_number, is_declined) + + def friend_exit(self, friend_number:int) -> None: + if friend_number in self._callav: + self._callav.finish_call(friend_number, True) + + # Private methods + + def _get_incoming_call_widget(self, friend_number, text, friend_name): + return av_widgets.IncomingCallWidget(self._settings, self, friend_number, text, friend_name) diff --git a/toxygen/av/screen_sharing.py b/toxygen/av/screen_sharing.py new file mode 100644 index 0000000..e0f783b --- /dev/null +++ b/toxygen/av/screen_sharing.py @@ -0,0 +1,23 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import QtWidgets + +class DesktopGrabber: + + def __init__(self, x, y, width, height): + self._x = x + self._y = y + self._width = width + self._height = height + self._width -= width % 4 + self._height -= height % 4 + self._screen = QtWidgets.QApplication.primaryScreen() + + def read(self) -> tuple: + pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height) + image = pixmap.toImage() + s = image.bits().asstring(self._width * self._height * 4) + import numpy as np + arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4)) + + return True, arr diff --git a/toxygen/avwidgets.py b/toxygen/avwidgets.py deleted file mode 100644 index 511fd8c..0000000 --- a/toxygen/avwidgets.py +++ /dev/null @@ -1,139 +0,0 @@ -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -import widgets -import profile -import util -import pyaudio -import wave -import settings -from util import curr_directory - - -class IncomingCallWidget(widgets.CenteredWidget): - - def __init__(self, friend_number, text, name): - super(IncomingCallWidget, self).__init__() - self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint) - self.resize(QtCore.QSize(500, 270)) - self.avatar_label = QtGui.QLabel(self) - self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) - self.avatar_label.setScaledContents(False) - self.name = widgets.DataLabel(self) - self.name.setGeometry(QtCore.QRect(90, 20, 300, 25)) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(16) - font.setBold(True) - self.name.setFont(font) - self.call_type = widgets.DataLabel(self) - self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25)) - self.call_type.setFont(font) - self.accept_audio = QtGui.QPushButton(self) - self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150)) - self.accept_video = QtGui.QPushButton(self) - self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150)) - self.decline = QtGui.QPushButton(self) - self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_audio.png') - icon = QtGui.QIcon(pixmap) - self.accept_audio.setIcon(icon) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_video.png') - icon = QtGui.QIcon(pixmap) - self.accept_video.setIcon(icon) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/decline_call.png') - icon = QtGui.QIcon(pixmap) - self.decline.setIcon(icon) - self.accept_audio.setIconSize(QtCore.QSize(150, 150)) - self.accept_video.setIconSize(QtCore.QSize(140, 140)) - self.decline.setIconSize(QtCore.QSize(140, 140)) - self.accept_audio.setStyleSheet("QPushButton { border: none }") - self.accept_video.setStyleSheet("QPushButton { border: none }") - self.decline.setStyleSheet("QPushButton { border: none }") - self.setWindowTitle(text) - self.name.setText(name) - self.call_type.setText(text) - pr = profile.Profile.get_instance() - self.accept_audio.clicked.connect(lambda: pr.accept_call(friend_number, True, False) or self.stop()) - # self.accept_video.clicked.connect(lambda: pr.start_call(friend_number, True, True)) - self.decline.clicked.connect(lambda: pr.stop_call(friend_number, False) or self.stop()) - - class SoundPlay(QtCore.QThread): - - def __init__(self): - QtCore.QThread.__init__(self) - - def run(self): - class AudioFile: - chunk = 1024 - - def __init__(self, fl): - self.stop = False - self.fl = fl - self.wf = wave.open(self.fl, 'rb') - self.p = pyaudio.PyAudio() - self.stream = self.p.open( - format=self.p.get_format_from_width(self.wf.getsampwidth()), - channels=self.wf.getnchannels(), - rate=self.wf.getframerate(), - output=True) - - def play(self): - while not self.stop: - data = self.wf.readframes(self.chunk) - while data and not self.stop: - self.stream.write(data) - data = self.wf.readframes(self.chunk) - self.wf = wave.open(self.fl, 'rb') - - def close(self): - self.stream.close() - self.p.terminate() - - self.a = AudioFile(curr_directory() + '/sounds/call.wav') - self.a.play() - self.a.close() - - if settings.Settings.get_instance()['calls_sound']: - self.thread = SoundPlay() - self.thread.start() - else: - self.thread = None - - def stop(self): - if self.thread is not None: - self.thread.a.stop = True - self.thread.wait() - self.close() - - def set_pixmap(self, pixmap): - self.avatar_label.setPixmap(pixmap) - - -class AudioMessageRecorder(widgets.CenteredWidget): - - def __init__(self, friend_number, name): - super(AudioMessageRecorder, self).__init__() - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(10, 20, 250, 20)) - text = QtGui.QApplication.translate("MenuWindow", "Send audio message to friend {}", None, QtGui.QApplication.UnicodeUTF8) - self.label.setText(text.format(name)) - self.record = QtGui.QPushButton(self) - self.record.setGeometry(QtCore.QRect(20, 100, 150, 150)) - - self.record.setText(QtGui.QApplication.translate("MenuWindow", "Start recording", None, - QtGui.QApplication.UnicodeUTF8)) - self.record.clicked.connect(self.start_or_stop_recording) - self.recording = False - self.friend_num = friend_number - - def start_or_stop_recording(self): - if not self.recording: - self.recording = True - self.record.setText(QtGui.QApplication.translate("MenuWindow", "Stop recording", None, - QtGui.QApplication.UnicodeUTF8)) - else: - self.close() - - diff --git a/toxygen/bootstrap.py b/toxygen/bootstrap.py deleted file mode 100644 index 89534b7..0000000 --- a/toxygen/bootstrap.py +++ /dev/null @@ -1,83 +0,0 @@ -import random - - -class Node: - def __init__(self, ip, port, tox_key, rand): - self._ip, self._port, self._tox_key, self.rand = ip, port, tox_key, rand - - def get_data(self): - return bytes(self._ip, 'utf-8'), self._port, self._tox_key - - -def node_generator(): - nodes = [] - ips = [ - "144.76.60.215", "23.226.230.47", "195.154.119.113", "biribiri.org", - "46.38.239.179", "178.62.250.138", "130.133.110.14", "104.167.101.29", - "205.185.116.116", "198.98.51.198", "80.232.246.79", "108.61.165.198", - "212.71.252.109", "194.249.212.109", "185.25.116.107", "192.99.168.140", - "46.101.197.175", "95.215.46.114", "5.189.176.217", "148.251.23.146", - "104.223.122.15", "78.47.114.252", "d4rk4.ru", "81.4.110.149", - "95.31.20.151", "104.233.104.126", "51.254.84.212", "home.vikingmakt.com.br", - "5.135.59.163", "185.58.206.164", "188.244.38.183", "mrflibble.c4.ee", - "82.211.31.116", "128.199.199.197", "103.230.156.174", "91.121.66.124", - "92.54.84.70", "tox1.privacydragon.me" - ] - ports = [ - 33445, 33445, 33445, 33445, - 33445, 33445, 33445, 33445, - 33445, 33445, 33445, 33445, - 33445, 33445, 33445, 33445, - 443, 33445, 5190, 2306, - 33445, 33445, 1813, 33445, - 33445, 33445, 33445, 33445, - 33445, 33445, 33445, 33445, - 33445, 33445, 33445, 33445, - 33445, 33445 - ] - ids = [ - "04119E835DF3E78BACF0F84235B300546AF8B936F035185E2A8E9E0A67C8924F", - "A09162D68618E742FFBCA1C2C70385E6679604B2D80EA6E84AD0996A1AC8A074", - "E398A69646B8CEACA9F0B84F553726C1C49270558C57DF5F3C368F05A7D71354", - "F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67", - "F5A1A38EFB6BD3C2C8AF8B10D85F0F89E931704D349F1D0720C3C4059AF2440A", - "788236D34978D1D5BD822F0A5BEBD2C53C64CC31CD3149350EE27D4D9A2F9B6B", - "461FA3776EF0FA655F1A05477DF1B3B614F7D6B124F7DB1DD4FE3C08B03B640F", - "5918AC3C06955962A75AD7DF4F80A5D7C34F7DB9E1498D2E0495DE35B3FE8A57", - "A179B09749AC826FF01F37A9613F6B57118AE014D4196A0E1105A98F93A54702", - "1D5A5F2F5D6233058BF0259B09622FB40B482E4FA0931EB8FD3AB8E7BF7DAF6F", - "CF6CECA0A14A31717CC8501DA51BE27742B70746956E6676FF423A529F91ED5D", - "8E7D0B859922EF569298B4D261A8CCB5FEA14FB91ED412A7603A585A25698832", - "C4CEB8C7AC607C6B374E2E782B3C00EA3A63B80D4910B8649CCACDD19F260819", - "3CEE1F054081E7A011234883BC4FC39F661A55B73637A5AC293DDF1251D9432B", - "DA4E4ED4B697F2E9B000EEFE3A34B554ACD3F45F5C96EAEA2516DD7FF9AF7B43", - "6A4D0607A296838434A6A7DDF99F50EF9D60A2C510BBF31FE538A25CB6B4652F", - "CD133B521159541FB1D326DE9850F5E56A6C724B5B8E5EB5CD8D950408E95707", - "5823FB947FF24CF83DDFAC3F3BAA18F96EA2018B16CC08429CB97FA502F40C23", - "2B2137E094F743AC8BD44652C55F41DFACC502F125E99E4FE24D40537489E32F", - "7AED21F94D82B05774F697B209628CD5A9AD17E0C073D9329076A4C28ED28147", - "0FB96EEBFB1650DDB52E70CF773DDFCABE25A95CC3BB50FC251082E4B63EF82A", - "1C5293AEF2114717547B39DA8EA6F1E331E5E358B35F9B6B5F19317911C5F976", - "53737F6D47FA6BD2808F378E339AF45BF86F39B64E79D6D491C53A1D522E7039", - "9E7BD4793FFECA7F32238FA2361040C09025ED3333744483CA6F3039BFF0211E", - "9CA69BB74DE7C056D1CC6B16AB8A0A38725C0349D187D8996766958584D39340", - "EDEE8F2E839A57820DE3DA4156D88350E53D4161447068A3457EE8F59F362414", - "AEC204B9A4501412D5F0BB67D9C81B5DB3EE6ADA64122D32A3E9B093D544327D", - "188E072676404ED833A4E947DC1D223DF8EFEBE5F5258573A236573688FB9761", - "2D320F971EF2CA18004416C2AAE7BA52BF7949DB34EA8E2E21AF67BD367BE211", - "24156472041E5F220D1FA11D9DF32F7AD697D59845701CDD7BE7D1785EB9DB39", - "15A0F9684E2423F9F46CFA5A50B562AE42525580D840CC50E518192BF333EE38", - "FAAB17014F42F7F20949F61E55F66A73C230876812A9737F5F6D2DCE4D9E4207", - "AF97B76392A6474AF2FD269220FDCF4127D86A42EF3A242DF53A40A268A2CD7C", - "B05C8869DBB4EDDD308F43C1A974A20A725A36EACCA123862FDE9945BF9D3E09", - "5C4C7A60183D668E5BD8B3780D1288203E2F1BAE4EEF03278019E21F86174C1D", - "4E3F7D37295664BBD0741B6DBCB6431D6CD77FC4105338C2FC31567BF5C8224A", - "5625A62618CB4FCA70E147A71B29695F38CC65FF0CBD68AD46254585BE564802", - "31910C0497D347FF160D6F3A6C0E317BAFA71E8E03BC4CBB2A185C9D4FB8B31E" - ] - for i in range(len(ips)): - nodes.append(Node(ips[i], ports[i], ids[i], random.randint(0, 1000000))) - arr = sorted(nodes, key=lambda x: x.rand)[:4] - for elem in arr: - yield elem.get_data() - diff --git a/toxygen/bootstrap/__init__.py b/toxygen/bootstrap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py new file mode 100644 index 0000000..6d64783 --- /dev/null +++ b/toxygen/bootstrap/bootstrap.py @@ -0,0 +1,48 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import random +import logging + +from qtpy import QtCore +try: + import certifi + from io import BytesIO +except ImportError: + certifi = None + +from user_data.settings import get_user_config_path +from utils.util import * + +from toxygen_wrapper.tests.support_testing import _get_nodes_path +from toxygen_wrapper.tests.support_http import download_url +import toxygen_wrapper.tests.support_testing as ts + +global LOG +LOG = logging.getLogger('app.'+'bootstrap') + +def download_nodes_list(settings, oArgs) -> str: + if not settings['download_nodes_list']: + return '' + if not ts.bAreWeConnected(): + return '' + url = settings['download_nodes_url'] + path = _get_nodes_path(oArgs=oArgs) + # dont download blindly so we can edit the file and not block on startup + if os.path.isfile(path): + with open(path, 'rt') as fl: + result = fl.read() + return result + LOG.debug("downloading list of nodes") + result = download_url(url, settings._app._settings) + if not result: + LOG.warn("failed downloading list of nodes") + return '' + LOG.info("downloaded list of nodes") + _save_nodes(result, settings._app) + return result + +def _save_nodes(nodes, app) -> None: + if not nodes: + return + with open(_get_nodes_path(app._args), 'wb') as fl: + LOG.info("Saving nodes to " +_get_nodes_path(app._args)) + fl.write(nodes) diff --git a/toxygen/bootstrap/nodes.json b/toxygen/bootstrap/nodes.json new file mode 100644 index 0000000..5314998 --- /dev/null +++ b/toxygen/bootstrap/nodes.json @@ -0,0 +1 @@ +{"nodes":[{"ipv4":"80.211.19.83","ipv6":"-","port":33445,"public_key":"A2D7BF17C10A12C339B9F4E8DD77DEEE8457D580535A6F0D0F9AF04B8B4C4420","status_udp":true,"status_tcp":true}]} \ No newline at end of file diff --git a/toxygen/callbacks.py b/toxygen/callbacks.py deleted file mode 100644 index fc13b99..0000000 --- a/toxygen/callbacks.py +++ /dev/null @@ -1,325 +0,0 @@ -try: - from PySide import QtCore -except ImportError: - from PyQt4 import QtCore -from notifications import * -from settings import Settings -from profile import Profile -from toxcore_enums_and_consts import * -from toxav_enums import * -from tox import bin_to_string -from plugin_support import PluginLoader - - -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)) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - current user -# ----------------------------------------------------------------------------------------------------------------- - - -def self_connection_status(tox_link): - """ - Current user changed connection status (offline, UDP, TCP) - """ - def wrapped(tox, connection, user_data): - print('Connection status: ', str(connection)) - profile = Profile.get_instance() - if profile.status is None: - status = tox_link.self_get_status() - invoke_in_main_thread(profile.set_status, status) - elif connection == TOX_CONNECTION['NONE']: - invoke_in_main_thread(profile.set_status, None) - return wrapped - - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - friends -# ----------------------------------------------------------------------------------------------------------------- - - -def friend_status(tox, friend_num, new_status, user_data): - """ - Check friend's status (none, busy, away) - """ - print("Friend's #{} status changed!".format(friend_num)) - profile = Profile.get_instance() - friend = profile.get_friend_by_number(friend_num) - if friend.status is None and Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) - invoke_in_main_thread(friend.set_status, new_status) - invoke_in_main_thread(profile.send_files, friend_num) - invoke_in_main_thread(profile.update_filtration) - - -def friend_connection_status(tox, friend_num, new_status, user_data): - """ - Check friend's connection status (offline, udp, tcp) - """ - print("Friend #{} connection status: {}".format(friend_num, new_status)) - profile = Profile.get_instance() - friend = profile.get_friend_by_number(friend_num) - if new_status == TOX_CONNECTION['NONE']: - invoke_in_main_thread(profile.friend_exit, friend_num) - invoke_in_main_thread(profile.update_filtration) - if Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) - elif friend.status is None: - invoke_in_main_thread(profile.send_avatar, friend_num) - invoke_in_main_thread(PluginLoader.get_instance().friend_online, friend_num) - - -def friend_name(tox, friend_num, name, size, user_data): - """ - Friend changed his name - """ - profile = Profile.get_instance() - print('New name friend #' + str(friend_num)) - invoke_in_main_thread(profile.new_name, friend_num, name) - - -def friend_status_message(tox, friend_num, status_message, size, user_data): - """ - :return: function for callback friend_status_message. It updates friend's status message - and calls window repaint - """ - profile = Profile.get_instance() - friend = profile.get_friend_by_number(friend_num) - invoke_in_main_thread(friend.set_status_message, status_message) - print('User #{} has new status'.format(friend_num)) - invoke_in_main_thread(profile.send_messages, friend_num) - if profile.get_active_number() == friend_num: - invoke_in_main_thread(profile.set_active) - - -def friend_message(window, tray): - """ - New message from friend - """ - def wrapped(tox, friend_number, message_type, message, size, user_data): - profile = Profile.get_instance() - settings = Settings.get_instance() - message = str(message, 'utf-8') - invoke_in_main_thread(profile.new_message, friend_number, message_type, message) - if not window.isActiveWindow(): - friend = profile.get_friend_by_number(friend_number) - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: - invoke_in_main_thread(tray_notification, friend.name, message, tray, window) - if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['MESSAGE']) - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png')) - return wrapped - - -def friend_request(tox, public_key, message, message_size, user_data): - """ - Called when user get new friend request - """ - print('Friend request') - profile = Profile.get_instance() - key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) - tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) - if tox_id not in Settings.get_instance()['blocked']: - invoke_in_main_thread(profile.process_friend_request, tox_id, str(message, 'utf-8')) - - -def friend_typing(tox, friend_number, typing, user_data): - invoke_in_main_thread(Profile.get_instance().friend_typing, friend_number, typing) - - -def friend_read_receipt(tox, friend_number, message_id, user_data): - profile = Profile.get_instance() - profile.get_friend_by_number(friend_number).dec_receipt() - if friend_number == profile.get_active_number(): - invoke_in_main_thread(profile.receipt) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - file transfers -# ----------------------------------------------------------------------------------------------------------------- - - -def tox_file_recv(window, tray): - """ - New incoming file - """ - def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data): - profile = Profile.get_instance() - settings = Settings.get_instance() - if file_type == TOX_FILE_KIND['DATA']: - print('File') - try: - file_name = str(file_name[:file_name_size], 'utf-8') - except: - file_name = 'toxygen_file' - invoke_in_main_thread(profile.incoming_file_transfer, - friend_number, - file_number, - size, - file_name) - if not window.isActiveWindow(): - friend = profile.get_friend_by_number(friend_number) - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: - file_from = QtGui.QApplication.translate("Callback", "File from", None, QtGui.QApplication.UnicodeUTF8) - invoke_in_main_thread(tray_notification, file_from + ' ' + friend.name, file_name, tray, window) - if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER']) - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png')) - else: # AVATAR - print('Avatar') - invoke_in_main_thread(profile.incoming_avatar, - friend_number, - file_number, - size) - return wrapped - - -def file_recv_chunk(tox, friend_number, file_number, position, chunk, length, user_data): - """ - Incoming chunk - """ - if not length: - invoke_in_main_thread(Profile.get_instance().incoming_chunk, - friend_number, - file_number, - position, - None) - else: - Profile.get_instance().incoming_chunk(friend_number, file_number, position, chunk[:length]) - - -def file_chunk_request(tox, friend_number, file_number, position, size, user_data): - """ - Outgoing chunk - """ - if size: - Profile.get_instance().outgoing_chunk(friend_number, file_number, position, size) - else: - invoke_in_main_thread(Profile.get_instance().outgoing_chunk, - friend_number, - file_number, - position, - size) - - -def file_recv_control(tox, friend_number, file_number, file_control, user_data): - """ - Friend cancelled, paused or resumed file transfer - """ - if file_control == TOX_FILE_CONTROL['CANCEL']: - invoke_in_main_thread(Profile.get_instance().cancel_transfer, friend_number, file_number, True) - elif file_control == TOX_FILE_CONTROL['PAUSE']: - invoke_in_main_thread(Profile.get_instance().pause_transfer, friend_number, file_number, True) - elif file_control == TOX_FILE_CONTROL['RESUME']: - invoke_in_main_thread(Profile.get_instance().resume_transfer, friend_number, file_number, True) - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - custom packets -# ----------------------------------------------------------------------------------------------------------------- - - -def lossless_packet(tox, friend_number, data, length, user_data): - """ - Incoming lossless packet - """ - plugin = PluginLoader.get_instance() - invoke_in_main_thread(plugin.callback_lossless, friend_number, data, length) - - -def lossy_packet(tox, friend_number, data, length, user_data): - """ - Incoming lossy packet - """ - plugin = PluginLoader.get_instance() - invoke_in_main_thread(plugin.callback_lossy, friend_number, data, length) - - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - audio -# ----------------------------------------------------------------------------------------------------------------- - -def call_state(toxav, friend_number, mask, user_data): - """ - New call state - """ - print(friend_number, mask) - if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: - invoke_in_main_thread(Profile.get_instance().stop_call, friend_number, True) - else: - Profile.get_instance().call.toxav_call_state_cb(friend_number, mask) - - -def call(toxav, friend_number, audio, video, user_data): - """ - Incoming call from friend - """ - print(friend_number, audio, video) - invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number) - - -def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data): - """ - New audio chunk - """ - # print(audio_samples_per_channel, audio_channels_count, rate) - Profile.get_instance().call.chunk( - bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), - audio_channels_count, - rate) - - -# ----------------------------------------------------------------------------------------------------------------- -# Callbacks - initialization -# ----------------------------------------------------------------------------------------------------------------- - - -def init_callbacks(tox, window, tray): - """ - Initialization of all callbacks. - :param tox: tox instance - :param window: main window - :param tray: tray (for notifications) - """ - tox.callback_self_connection_status(self_connection_status(tox), 0) - - tox.callback_friend_status(friend_status, 0) - tox.callback_friend_message(friend_message(window, tray), 0) - tox.callback_friend_connection_status(friend_connection_status, 0) - tox.callback_friend_name(friend_name, 0) - tox.callback_friend_status_message(friend_status_message, 0) - tox.callback_friend_request(friend_request, 0) - tox.callback_friend_typing(friend_typing, 0) - tox.callback_friend_read_receipt(friend_read_receipt, 0) - - tox.callback_file_recv(tox_file_recv(window, tray), 0) - tox.callback_file_recv_chunk(file_recv_chunk, 0) - tox.callback_file_chunk_request(file_chunk_request, 0) - tox.callback_file_recv_control(file_recv_control, 0) - - toxav = tox.AV - toxav.callback_call_state(call_state, 0) - toxav.callback_call(call, 0) - toxav.callback_audio_receive_frame(callback_audio, 0) - - tox.callback_friend_lossless_packet(lossless_packet, 0) - tox.callback_friend_lossy_packet(lossy_packet, 0) - diff --git a/toxygen/calls.py b/toxygen/calls.py deleted file mode 100644 index 16cef47..0000000 --- a/toxygen/calls.py +++ /dev/null @@ -1,144 +0,0 @@ -import pyaudio -import time -import threading -import settings -from toxav_enums import * -# TODO: play sound until outgoing call will be started or cancelled and add timeout -# TODO: add widget for call - -CALL_TYPE = { - 'NONE': 0, - 'AUDIO': 1, - 'VIDEO': 2 -} - - -class AV: - - def __init__(self, toxav): - self._toxav = toxav - self._running = True - - self._calls = {} # dict: key - friend number, value - call type - - self._audio = None - self._audio_stream = None - self._audio_thread = None - self._audio_running = False - self._out_stream = None - - self._audio_rate = 8000 - self._audio_channels = 1 - self._audio_duration = 60 - self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000 - - def __contains__(self, friend_number): - return friend_number in self._calls - - def __call__(self, friend_number, audio, video): - """Call friend with specified number""" - self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0) - self._calls[friend_number] = CALL_TYPE['AUDIO'] - self.start_audio_thread() - - def finish_call(self, friend_number, by_friend=False): - - if not by_friend: - self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) - if friend_number in self._calls: - del self._calls[friend_number] - if not len(self._calls): - self.stop_audio_thread() - - def stop(self): - self._running = False - self.stop_audio_thread() - - def start_audio_thread(self): - """ - Start audio sending - """ - if self._audio_thread is not None: - return - - self._audio_running = True - - self._audio = pyaudio.PyAudio() - self._audio_stream = self._audio.open(format=pyaudio.paInt16, - rate=self._audio_rate, - channels=self._audio_channels, - input=True, - input_device_index=settings.Settings.get_instance().audio['input'], - frames_per_buffer=self._audio_sample_count * 10) - - self._audio_thread = threading.Thread(target=self.send_audio) - self._audio_thread.start() - - def stop_audio_thread(self): - - if self._audio_thread is None: - return - - self._audio_running = False - - self._audio_thread.join() - - self._audio_thread = None - self._audio_stream = None - self._audio = None - - if self._out_stream is not None: - self._out_stream.stop_stream() - self._out_stream.close() - self._out_stream = None - - def chunk(self, samples, channels_count, rate): - """ - Incoming chunk - """ - - if self._out_stream is None: - self._out_stream = self._audio.open(format=pyaudio.paInt16, - channels=channels_count, - rate=rate, - output_device_index=settings.Settings.get_instance().audio['output'], - output=True) - self._out_stream.write(samples) - - def send_audio(self): - """ - This method sends audio to friends - """ - - while self._audio_running: - try: - pcm = self._audio_stream.read(self._audio_sample_count) - if pcm: - for friend in self._calls: - if self._calls[friend] & 1: - try: - self._toxav.audio_send_frame(friend, pcm, self._audio_sample_count, - self._audio_channels, self._audio_rate) - except: - pass - except: - pass - - time.sleep(0.01) - - def accept_call(self, friend_number, audio_enabled, video_enabled): - - if self._running: - self._calls[friend_number] = int(video_enabled) * 2 + int(audio_enabled) - self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0) - self.start_audio_thread() - - def toxav_call_state_cb(self, friend_number, state): - """ - New call state - """ - if self._running: - - if state & TOXAV_FRIEND_CALL_STATE['ACCEPTING_A']: - self._calls[friend_number] |= 1 - diff --git a/toxygen/common/__init__.py b/toxygen/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/common/event.py b/toxygen/common/event.py new file mode 100644 index 0000000..f51a51f --- /dev/null +++ b/toxygen/common/event.py @@ -0,0 +1,26 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class Event: + + def __init__(self): + self._callbacks = set() + + def __iadd__(self, callback): + self.add_callback(callback) + + return self + + def __isub__(self, callback): + self.remove_callback(callback) + + return self + + def __call__(self, *args, **kwargs): + for callback in self._callbacks: + callback(*args, **kwargs) + + def add_callback(self, callback): + self._callbacks.add(callback) + + def remove_callback(self, callback): + self._callbacks.discard(callback) diff --git a/toxygen/common/provider.py b/toxygen/common/provider.py new file mode 100644 index 0000000..687fd9a --- /dev/null +++ b/toxygen/common/provider.py @@ -0,0 +1,13 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class Provider: + + def __init__(self, get_item_action): + self._get_item_action = get_item_action + self._item = None + + def get_item(self): + if self._item is None: + self._item = self._get_item_action() + + return self._item diff --git a/toxygen/common/tox_save.py b/toxygen/common/tox_save.py new file mode 100644 index 0000000..45563b2 --- /dev/null +++ b/toxygen/common/tox_save.py @@ -0,0 +1,18 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class ToxSave: + + def __init__(self, tox): + self._tox = tox + + def set_tox(self, tox): + self._tox = tox + + +class ToxAvSave: + + def __init__(self, toxav): + self._toxav = toxav + + def set_toxav(self, toxav): + self._toxav = toxav diff --git a/toxygen/contact.py b/toxygen/contact.py deleted file mode 100644 index f4b3f9a..0000000 --- a/toxygen/contact.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from settings import * -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE - - -class Contact: - """ - Class encapsulating TOX contact - Properties: name (alias of contact or name), status_message, status (connection status) - widget - widget for update - """ - - def __init__(self, name, status_message, widget, tox_id): - """ - :param name: name, example: 'Toxygen user' - :param status_message: status message, example: 'Toxing on Toxygen' - :param widget: ContactItem instance - :param tox_id: tox id of contact - """ - self._name, self._status_message = name, status_message - self._status, self._widget = None, widget - self._widget.name.setText(name) - self._widget.status_message.setText(status_message) - self._tox_id = tox_id - self.load_avatar() - - # ----------------------------------------------------------------------------------------------------------------- - # name - current name or alias of user - # ----------------------------------------------------------------------------------------------------------------- - - def get_name(self): - return self._name - - def set_name(self, value): - self._name = str(value, 'utf-8') - self._widget.name.setText(self._name) - self._widget.name.repaint() - - name = property(get_name, set_name) - - # ----------------------------------------------------------------------------------------------------------------- - # Status message - # ----------------------------------------------------------------------------------------------------------------- - - def get_status_message(self): - return self._status_message - - def set_status_message(self, value): - self._status_message = str(value, 'utf-8') - self._widget.status_message.setText(self._status_message) - self._widget.status_message.repaint() - - status_message = property(get_status_message, set_status_message) - - # ----------------------------------------------------------------------------------------------------------------- - # Status - # ----------------------------------------------------------------------------------------------------------------- - - def get_status(self): - return self._status - - def set_status(self, value): - self._status = value - self._widget.connection_status.update(value) - - status = property(get_status, set_status) - - # ----------------------------------------------------------------------------------------------------------------- - # TOX ID. WARNING: for friend it will return public key, for profile - full address - # ----------------------------------------------------------------------------------------------------------------- - - def get_tox_id(self): - return self._tox_id - - tox_id = property(get_tox_id) - - # ----------------------------------------------------------------------------------------------------------------- - # Avatars - # ----------------------------------------------------------------------------------------------------------------- - - def load_avatar(self): - """ - Tries to load avatar of contact or uses default avatar - """ - avatar_path = '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - os.chdir(ProfileHelper.get_path() + 'avatars/') - if not os.path.isfile(avatar_path): # load default image - avatar_path = 'avatar.png' - os.chdir(curr_directory() + '/images/') - width = self._widget.avatar_label.width() - pixmap = QtGui.QPixmap(QtCore.QSize(width, width)) - pixmap.load(avatar_path) - self._widget.avatar_label.setScaledContents(False) - self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio)) - self._widget.avatar_label.repaint() - - def reset_avatar(self): - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if os.path.isfile(avatar_path): - os.remove(avatar_path) - self.load_avatar() - - def set_avatar(self, avatar): - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - with open(avatar_path, 'wb') as f: - f.write(avatar) - self.load_avatar() - - def get_pixmap(self): - return self._widget.avatar_label.pixmap() diff --git a/toxygen/contacts/__init__.py b/toxygen/contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/contacts/basecontact.py b/toxygen/contacts/basecontact.py new file mode 100644 index 0000000..b4b33f1 --- /dev/null +++ b/toxygen/contacts/basecontact.py @@ -0,0 +1,181 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from user_data.settings import * +from qtpy import QtCore, QtGui +from toxygen_wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE +import utils.util as util +import common.event as event +import contacts.common as common + + +class BaseContact: + """ + Class encapsulating TOX contact + Properties: name (alias of contact or name), status_message, status (connection status) + widget - widget for update, tox id (or public key) + Base class for all contacts. + """ + + def __init__(self, profile_manager, name, status_message, widget, tox_id, kind=''): + """ + :param name: name, example: 'Toxygen user' + :param status_message: status message, example: 'Toxing on Toxygen' + :param widget: ContactItem instance + :param tox_id: tox id of contact + :param kind: one of ['bot', 'friend', 'group', 'invite', 'grouppeer', ''] + """ + self._profile_manager = profile_manager + self._name, self._status_message = name, status_message + self._kind = kind + self._status, self._widget = None, widget + self._tox_id = tox_id + + self._name_changed_event = event.Event() + self._status_message_changed_event = event.Event() + self._status_changed_event = event.Event() + self._avatar_changed_event = event.Event() + self.init_widget() + + # Name - current name or alias of user + + def get_name(self): + return self._name + + def set_name(self, value): + if self._name == value: + return + self._name = value + self._widget.name.setText(self._name) + self._widget.name.repaint() + self._name_changed_event(self._name) + + name = property(get_name, set_name) + + def get_name_changed_event(self): + return self._name_changed_event + + name_changed_event = property(get_name_changed_event) + + # Status message + + def get_status_message(self): + return self._status_message + + def set_status_message(self, value): + if self._status_message == value: + return + self._status_message = value + self._widget.status_message.setText(self._status_message) + self._widget.status_message.repaint() + self._status_message_changed_event(self._status_message) + + status_message = property(get_status_message, set_status_message) + + def get_status_message_changed_event(self): + return self._status_message_changed_event + + status_message_changed_event = property(get_status_message_changed_event) + + # Status + + def get_status(self): + return self._status + + def set_status(self, value): + if self._status == value: + return + self._status = value + self._widget.connection_status.update(value) + self._status_changed_event(self._status) + + status = property(get_status, set_status) + + def get_status_changed_event(self): + return self._status_changed_event + + status_changed_event = property(get_status_changed_event) + + # TOX ID. WARNING: for friend it will return public key, for profile - full address + + def get_tox_id(self): + return self._tox_id + + tox_id = property(get_tox_id) + + # Avatars + + def load_avatar(self): + """ + Tries to load avatar of contact or uses default avatar + """ + try: + avatar_path = self.get_avatar_path() + width = self._widget.avatar_label.width() + pixmap = QtGui.QPixmap(avatar_path) + self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation)) + self._widget.avatar_label.repaint() + self._avatar_changed_event(avatar_path) + except Exception as e: + pass + + def reset_avatar(self, generate_new): + avatar_path = self.get_avatar_path() + if os.path.isfile(avatar_path) and not avatar_path == self._get_default_avatar_path(): + os.remove(avatar_path) + if generate_new: + self.set_avatar(common.generate_avatar(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])) + else: + self.load_avatar() + + def set_avatar(self, avatar): + avatar_path = self.get_contact_avatar_path() + with open(avatar_path, 'wb') as f: + f.write(avatar) + self.load_avatar() + + def get_pixmap(self): + return self._widget.avatar_label.pixmap() + + def get_avatar_path(self): + avatar_path = self.get_contact_avatar_path() + if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image + avatar_path = self._get_default_avatar_path() + + return avatar_path + + def get_contact_avatar_path(self): + directory = util.join_path(self._profile_manager.get_dir(), 'avatars') + + return util.join_path(directory, '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])) + + def has_avatar(self): + path = self.get_contact_avatar_path() + + return util.file_exists(path) + + def get_avatar_changed_event(self): + return self._avatar_changed_event + + avatar_changed_event = property(get_avatar_changed_event) + + # Widgets + + def init_widget(self): + # File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contacts_manager.py", line 252, in filtration_and_sorting + # contact.set_widget(item_widget) + # File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact.py", line 320, in set_widget + if not self._widget: + LOG.warn("BC.init_widget self._widget is NULL") + return + self._widget.name.setText(self._name) + self._widget.status_message.setText(self._status_message) + if hasattr(self._widget, 'kind'): + self._widget.kind.setText(self._kind) + self._widget.connection_status.update(self._status) + self.load_avatar() + + # Private methods + + @staticmethod + def _get_default_avatar_path(): + return util.join_path(util.get_images_directory(), 'avatar.png') diff --git a/toxygen/contacts/common.py b/toxygen/contacts/common.py new file mode 100644 index 0000000..bd46c32 --- /dev/null +++ b/toxygen/contacts/common.py @@ -0,0 +1,48 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import hashlib + +from pydenticon import Generator + +# Typing notifications + +class BaseTypingNotificationHandler: + + DEFAULT_HANDLER = None + + def __init__(self): + pass + + def send(self, tox, is_typing): + pass + + +class FriendTypingNotificationHandler(BaseTypingNotificationHandler): + + def __init__(self, friend_number:int): + super().__init__() + self._friend_number = friend_number + + def send(self, tox, is_typing): + tox.self_set_typing(self._friend_number, is_typing) + + +BaseTypingNotificationHandler.DEFAULT_HANDLER = BaseTypingNotificationHandler() + + +# Identicons support + + +def generate_avatar(public_key): + foreground = ['rgb(45,79,255)', 'rgb(185, 66, 244)', 'rgb(185, 66, 244)', + 'rgb(254,180,44)', 'rgb(252, 2, 2)', 'rgb(109, 198, 0)', + 'rgb(226,121,234)', 'rgb(130, 135, 124)', + 'rgb(30,179,253)', 'rgb(160, 157, 0)', + 'rgb(232,77,65)', 'rgb(102, 4, 4)', + 'rgb(49,203,115)', + 'rgb(141,69,170)'] + generator = Generator(5, 5, foreground=foreground, background='rgba(42,42,42,0)') + digest = hashlib.sha256(public_key.encode('utf-8')).hexdigest() + identicon = generator.generate(digest, 220, 220, padding=(10, 10, 10, 10)) + + return identicon diff --git a/toxygen/contacts/contact.py b/toxygen/contacts/contact.py new file mode 100644 index 0000000..70b9318 --- /dev/null +++ b/toxygen/contacts/contact.py @@ -0,0 +1,320 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from history.database import TIMEOUT, \ + SAVE_MESSAGES, MESSAGE_AUTHOR + +from contacts import basecontact, common +from messenger.messages import * +from contacts.contact_menu import * +from file_transfers import file_transfers as ft +import re + +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +class Contact(basecontact.BaseContact): + """ + Class encapsulating TOX contact + Properties: number, message getter, history etc. Base class for friend and gc classes + """ + + def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id): + """ + :param message_getter: gets messages from db + :param number: number of friend. + """ + super().__init__(profile_manager, name, status_message, widget, tox_id) + self._number = number + self._new_messages = False + self._visible = True + self._alias = False + self._message_getter = message_getter + self._corr = [] + self._unsaved_messages = 0 + self._history_loaded = self._new_actions = False + self._curr_text = self._search_string = '' + self._search_index = 0 + + def __del__(self): + self.set_visibility(False) + del self._widget + if hasattr(self, '_message_getter'): + del self._message_getter + + # History support + + def load_corr(self, first_time=True): + """ + :param first_time: friend became active, load first part of messages + """ + try: + if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')): + return + if self._message_getter is None: + return + data = list(self._message_getter.get(PAGE_SIZE)) + if data is not None and len(data): + data.reverse() + else: + return + data = list(map(lambda p: self._get_text_message(p), data)) + self._corr = data + self._corr + except: + pass + finally: + self._history_loaded = True + + def load_all_corr(self): + """ + Get all chat history from db for current friend + """ + if self._message_getter is None: + return + data = list(self._message_getter.get_all()) + if data is not None and len(data): + data.reverse() + data = list(map(lambda p: self._get_text_message(p), data)) + self._corr = data + self._corr + self._history_loaded = True + + def get_corr_for_saving(self): + """ + Get data to save in db + :return: list of unsaved messages or [] + """ + messages = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr)) + return messages[-self._unsaved_messages:] if self._unsaved_messages else [] + + def get_corr(self): + return self._corr[:] + + def append_message(self, message): + """ + :param message: text or file transfer message + """ + self._corr.append(message) + if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): + self._unsaved_messages += 1 + + def get_last_message_text(self): + messages = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) + and m.author.type != MESSAGE_AUTHOR['FRIEND'], self._corr)) + if messages: + return messages[-1].text + else: + return '' + + def remove_messages_widgets(self): + for message in self._corr: + message.remove_widget() + + def get_message(self, _filter): + return list(filter(lambda m: _filter(m), self._corr))[0] + + @staticmethod + def _get_text_message(params): + (message, author_type, author_name, unix_time, message_type, unique_id) = params + author = MessageAuthor(author_name, author_type) + + return TextMessage(message, author, unix_time, message_type, unique_id) + + # Unsent messages + + def get_unsent_messages(self): + """ + :return list of unsent messages + """ + messages = filter(lambda m: m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr) + return list(messages) + + def get_unsent_messages_for_saving(self): + """ + :return list of unsent messages for saving + """ +# and m.tox_message_id == tox_message_id, + messages = filter(lambda m: m.author is not None + and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], + self._corr) + # was message = list(...)[0] + return list(messages) + + def mark_as_sent(self, tox_message_id): + try: + message = list(filter(lambda m: m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'] + and m.tox_message_id == tox_message_id, self._corr))[0] + message.mark_as_sent() + except Exception as ex: + # wrapped C/C++ object of type QLabel has been deleted + LOG.error(f"Mark as sent: {ex}") + + # Message deletion + + def delete_message(self, message_id): + elem = list(filter(lambda m: m.message_id == message_id, self._corr))[0] + tmp = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr)) + if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages: + self._unsaved_messages -= 1 + self._corr.remove(elem) + self._message_getter.delete_one() + self._search_index = 0 + + def delete_old_messages(self): + """ + Delete old messages (reduces RAM usage if messages saving is not enabled) + """ + def save_message(m): + if m.type == MESSAGE_TYPE['FILE_TRANSFER'] and (m.state not in ACTIVE_FILE_TRANSFERS): + return True + return m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'] + + old = filter(save_message, self._corr[:-SAVE_MESSAGES]) + self._corr = list(old) + self._corr[-SAVE_MESSAGES:] + text_messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr) + self._unsaved_messages = min(self._unsaved_messages, len(list(text_messages))) + self._search_index = 0 + + def clear_corr(self, save_unsent=False): + """ + Clear messages list + """ + if hasattr(self, '_message_getter'): + del self._message_getter + self._search_index = 0 + # don't delete data about active file transfer + if not save_unsent: + self._corr = list(filter(lambda m: m.type == MESSAGE_TYPE['FILE_TRANSFER'] and + m.state in ft.ACTIVE_FILE_TRANSFERS, self._corr)) + self._unsaved_messages = 0 + else: + self._corr = list(filter(lambda m: (m.type == MESSAGE_TYPE['FILE_TRANSFER'] + and m.state in ft.ACTIVE_FILE_TRANSFERS) + or (m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']) + and m.author.type == MESSAGE_AUTHOR['NOT_SENT']), + self._corr)) + self._unsaved_messages = len(self.get_unsent_messages()) + + # Chat history search + + def search_string(self, search_string): + self._search_string, self._search_index = search_string, 0 + return self.search_prev() + + def search_prev(self): + while True: + l = len(self._corr) + for i in range(self._search_index - 1, -l - 1, -1): + if self._corr[i].type not in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): + continue + message = self._corr[i].text + if re.search(self._search_string, message, re.IGNORECASE) is not None: + self._search_index = i + return i + self._search_index = -l + self.load_corr(False) + if len(self._corr) == l: + return None # not found + + def search_next(self): + if not self._search_index: + return None + for i in range(self._search_index + 1, 0): + if self._corr[i].type not in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): + continue + message = self._corr[i].text + if re.search(self._search_string, message, re.IGNORECASE) is not None: + self._search_index = i + return i + return None # not found + + # Current text - text from message area + + def get_curr_text(self): + return self._curr_text + + def set_curr_text(self, value): + self._curr_text = value + + curr_text = property(get_curr_text, set_curr_text) + + # Alias support + + def set_name(self, value): + """ + Set new name or ignore if alias exists + :param value: new name + """ + if not self._alias: + super().set_name(value) + + def set_alias(self, alias): + self._alias = bool(alias) + + def has_alias(self): + return self._alias + + # Visibility in friends' list + + def get_visibility(self): + return self._visible + + def set_visibility(self, value): + self._visible = value + + visibility = property(get_visibility, set_visibility) + + # Unread messages and other actions from friend + + def get_actions(self): + return self._new_actions + + def set_actions(self, value): + self._new_actions = value + self._widget.connection_status.update(self.status, value) + + actions = property(get_actions, set_actions) # unread messages, incoming files, av calls + + def get_messages(self): + return self._new_messages + + def inc_messages(self): + self._new_messages += 1 + self._new_actions = True + self._widget.connection_status.update(self.status, True) + self._widget.messages.update(self._new_messages) + + def reset_messages(self): + self._new_actions = False + self._new_messages = 0 + self._widget.messages.update(self._new_messages) + self._widget.connection_status.update(self.status, False) + + messages = property(get_messages) + + # Friend's or group's number (can be used in toxcore) + + def get_number(self): + return self._number + + def set_number(self, value): + self._number = value + + number = property(get_number, set_number) + + # Typing notifications + + def get_typing_notification_handler(self): + return common.BaseTypingNotificationHandler.DEFAULT_HANDLER + + typing_notification_handler = property(get_typing_notification_handler) + + # Context menu support + + def get_context_menu_generator(self): + return BaseContactMenuGenerator(self) + + # Filtration support + + def set_widget(self, widget): + self._widget = widget + self.init_widget() diff --git a/toxygen/contacts/contact_menu.py b/toxygen/contacts/contact_menu.py new file mode 100644 index 0000000..6f45ca6 --- /dev/null +++ b/toxygen/contacts/contact_menu.py @@ -0,0 +1,229 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtWidgets + +import utils.ui as util_ui +from toxygen_wrapper.toxcore_enums_and_consts import * + +global LOG +import logging +LOG = logging.getLogger('app') + +# Builder + +def _create_menu(menu_name, parent): + menu_name = menu_name or '' + + return QtWidgets.QMenu(menu_name) if parent is None else parent.addMenu(menu_name) + + +class ContactMenuBuilder: + + def __init__(self): + self._actions = {} + self._submenus = {} + self._name = None + self._index = 0 + + def with_name(self, name): + self._name = name + + return self + + def with_action(self, text, handler): + self._add_action(text, handler) + + return self + + def with_optional_action(self, text, handler, show_action): + if show_action: + self._add_action(text, handler) + + return self + + def with_actions(self, actions): + for action in actions: + (text, handler) = action + self._add_action(text, handler) + + return self + + def with_submenu(self, submenu_builder): + self._add_submenu(submenu_builder) + + return self + + def with_optional_submenu(self, submenu_builder): + if submenu_builder is not None: + self._add_submenu(submenu_builder) + + return self + + def build(self, parent=None): + menu = _create_menu(self._name, parent) + + for i in range(self._index): + if i in self._actions: + text, handler = self._actions[i] + action = menu.addAction(text) + action.triggered.connect(handler) + else: + submenu_builder = self._submenus[i] + submenu = submenu_builder.build(menu) + menu.addMenu(submenu) + + return menu + + def _add_submenu(self, submenu): + self._submenus[self._index] = submenu + self._index += 1 + + def _add_action(self, text, handler): + self._actions[self._index] = (text, handler) + self._index += 1 + +# Generators + + +class BaseContactMenuGenerator: + + def __init__(self, contact): + self._contact = contact + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + return ContactMenuBuilder().build() + + # Private methods + + def _generate_copy_menu_builder(self, main_screen): + copy_menu_builder = ContactMenuBuilder() + (copy_menu_builder + .with_name(util_ui.tr('Copy')) + .with_action(util_ui.tr('Name'), lambda: main_screen.copy_text(self._contact.name)) + .with_action(util_ui.tr("Status message"), lambda: main_screen.copy_text(self._contact.status_message)) + .with_action(util_ui.tr("Public key"), lambda: main_screen.copy_text(self._contact.tox_id)) + ) + + return copy_menu_builder + + def _generate_history_menu_builder(self, history_loader, main_screen): + history_menu_builder = ContactMenuBuilder() + (history_menu_builder + .with_name(util_ui.tr("Chat history")) + .with_action(util_ui.tr("Clear history"), lambda: history_loader.clear_history(self._contact) + or main_screen.messages.clear()) + .with_action(util_ui.tr("Export as text"), lambda: history_loader.export_history(self._contact)) + .with_action(util_ui.tr("Export as HTML"), lambda: history_loader.export_history(self._contact, False)) + ) + + return history_menu_builder + + +class FriendMenuGenerator(BaseContactMenuGenerator): + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen) + copy_menu_builder = self._generate_copy_menu_builder(main_screen) + plugins_menu_builder = self._generate_plugins_menu_builder(plugin_loader, number) + groups_menu_builder = self._generate_groups_menu(contacts_manager, groups_service) + + allowed = self._contact.tox_id in settings['auto_accept_from_friends'] + auto = util_ui.tr("Disallow auto accept") if allowed else util_ui.tr('Allow auto accept') + + builder = ContactMenuBuilder() + menu = (builder + .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) + .with_submenu(history_menu_builder) + .with_submenu(copy_menu_builder) + .with_action(auto, lambda: main_screen.auto_accept(number, not allowed)) + .with_action(util_ui.tr("Remove friend"), lambda: main_screen.remove_friend(number)) + .with_action(util_ui.tr("Block friend"), lambda: main_screen.block_friend(number)) + .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) + .with_optional_submenu(plugins_menu_builder) + .with_optional_submenu(groups_menu_builder) + ).build() + + return menu + + # Private methods + + @staticmethod + def _generate_plugins_menu_builder(plugin_loader, number): + if plugin_loader is None: + return None + plugins_actions = plugin_loader.get_menu(number) + if not len(plugins_actions): + return None + plugins_menu_builder = ContactMenuBuilder() + (plugins_menu_builder + .with_name(util_ui.tr('Plugins')) + .with_actions(plugins_actions) + ) + + return plugins_menu_builder + + def _generate_groups_menu(self, contacts_manager, groups_service): + chats = contacts_manager.get_group_chats() + LOG.debug(f"_generate_groups_menu len(chats)={len(chats)} or self._contact.status={self._contact.status}") + if not len(chats) or self._contact.status is None: + #? return None + pass + groups_menu_builder = ContactMenuBuilder() + (groups_menu_builder + .with_name(util_ui.tr("Invite to group")) + .with_actions([(g.name, lambda: groups_service.invite_friend(self._contact.number, g.number)) for g in chats]) + ) + + return groups_menu_builder + + +class GroupMenuGenerator(BaseContactMenuGenerator): + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + copy_menu_builder = self._generate_copy_menu_builder(main_screen) + history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen) + + builder = ContactMenuBuilder() + menu = (builder + .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) + .with_submenu(copy_menu_builder) + .with_submenu(history_menu_builder) + .with_optional_action(util_ui.tr("Manage group"), + lambda: groups_service.show_group_management_screen(self._contact), + self._contact.is_self_founder()) + .with_optional_action(util_ui.tr("Group settings"), + lambda: groups_service.show_group_settings_screen(self._contact), + not self._contact.is_self_founder()) + .with_optional_action(util_ui.tr("Set topic"), + lambda: groups_service.set_group_topic(self._contact), + self._contact.is_self_moderator_or_founder()) +# .with_action(util_ui.tr("Bans list"), +# lambda: groups_service.show_bans_list(self._contact)) + .with_action(util_ui.tr("Reconnect to group"), + lambda: groups_service.reconnect_to_group(self._contact.number)) + .with_optional_action(util_ui.tr("Disconnect from group"), + lambda: groups_service.disconnect_from_group(self._contact.number), + self._contact.status is not None) + .with_action(util_ui.tr("Leave group"), lambda: groups_service.leave_group(self._contact.number)) + .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) + ).build() + + return menu + + +class GroupPeerMenuGenerator(BaseContactMenuGenerator): + + def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): + copy_menu_builder = self._generate_copy_menu_builder(main_screen) + history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen) + + builder = ContactMenuBuilder() + menu = (builder + .with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number)) + .with_submenu(copy_menu_builder) + .with_submenu(history_menu_builder) + .with_action(util_ui.tr("Quit chat"), + lambda: contacts_manager.remove_group_peer(self._contact)) + .with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact)) + ).build() + + return menu diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py new file mode 100644 index 0000000..0c5a61d --- /dev/null +++ b/toxygen/contacts/contact_provider.py @@ -0,0 +1,166 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import common.tox_save as tox_save + +global LOG +import logging +LOG = logging.getLogger(__name__) + +# callbacks can be called in any thread so were being careful +from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +class ContactProvider(tox_save.ToxSave): + + def __init__(self, tox, friend_factory, group_factory, group_peer_factory, app=None): + super().__init__(tox) + self._friend_factory = friend_factory + self._group_factory = group_factory + self._group_peer_factory = group_peer_factory + self._cache = {} # key - contact's public key, value - contact instance + self._app = app + + # Friends + + def get_friend_by_number(self, friend_number:int): + try: + public_key = self._tox.friend_get_public_key(friend_number) + except Exception as e: + LOG_WARN(f"CP.get_friend_by_number NO {friend_number} {e} ") + return None + return self.get_friend_by_public_key(public_key) + + def get_friend_by_public_key(self, public_key): + friend = self._get_contact_from_cache(public_key) + if friend is not None: + return friend + friend = self._friend_factory.create_friend_by_public_key(public_key) + if friend is None: + LOG_WARN(f"CP.get_friend_by_public_key NULL {friend} ") + else: + self._add_to_cache(public_key, friend) + LOG_DEBUG(f"CP.get_friend_by_public_key ADDED {friend} ") + return friend + + def get_all_friends(self) -> list: + if self._app and self._app.bAppExiting: + return [] + try: + friend_numbers = self._tox.self_get_friend_list() + except Exception as e: + LOG_WARN(f"CP.get_all_friends EXCEPTION {e} ") + return [] + friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) + return list(friends) + + # Groups + + def get_all_groups(self): + """from callbacks""" + try: + len_groups = self._tox.group_get_number_groups() + group_numbers = range(len_groups) + except Exception as e: + return None + groups = list(map(lambda n: self.get_group_by_number(n), group_numbers)) + # failsafe in case there are bogus None groups? + fgroups = list(filter(lambda x: x, groups)) + if len(fgroups) != len_groups: + LOG_WARN(f"CP.are there are bogus None groups in libtoxcore? {len(fgroups)} != {len_groups}") + for group_num in group_numbers: + group = self.get_group_by_number(group_num) + if group is None: + LOG_ERROR(f"There are bogus None groups in libtoxcore {group_num}!") + # fixme: do something + groups = fgroups + return groups + + def get_group_by_number(self, group_number): + group = None + try: +# LOG_DEBUG(f"CP.CP.group_get_number {group_number} ") + # original code + chat_id = self._tox.group_get_chat_id(group_number) + if chat_id is None: + LOG_ERROR(f"get_group_by_number NULL chat_id ({group_number})") + elif chat_id == '-1': + LOG_ERROR(f"get_group_by_number <0 chat_id ({group_number})") + else: + LOG_INFO(f"CP.group_get_number {group_number} {chat_id}") + group = self.get_group_by_chat_id(chat_id) + if group is None or group == '-1': + LOG_WARN(f"CP.get_group_by_number leaving {group} ({group_number})") + #? iRet = self._tox.group_leave(group_number) + # invoke in main thread? + # self._contacts_manager.delete_group(group_number) + return group + except Exception as e: + LOG_WARN(f"CP.group_get_number {group_number} {e}") + return None + + def get_group_by_chat_id(self, chat_id): + group = self._get_contact_from_cache(chat_id) + if group is not None: + return group + group = self._group_factory.create_group_by_chat_id(chat_id) + if group is None: + LOG_ERROR(f"get_group_by_chat_id NULL chat_id={chat_id}") + else: + self._add_to_cache(chat_id, group) + + return group + + def get_group_by_public_key(self, public_key): + group = self._get_contact_from_cache(public_key) + if group is not None: + return group + group = self._group_factory.create_group_by_public_key(public_key) + if group is None: + LOG_WARN(f"get_group_by_public_key NULL group public_key={public_key}") + else: + self._add_to_cache(public_key, group) + + return group + + # Group peers + + def get_all_group_peers(self): + return [] + + def get_group_peer_by_id(self, group, peer_id): + peer = group.get_peer_by_id(peer_id) + if peer is not None: + return self._get_group_peer(group, peer) + LOG_WARN(f"get_group_peer_by_id peer_id={peer_id}") + return None + + def get_group_peer_by_public_key(self, group, public_key): + peer = group.get_peer_by_public_key(public_key) + if peer is not None: + return self._get_group_peer(group, peer) + LOG_WARN(f"get_group_peer_by_public_key public_key={public_key}") + return None + + # All contacts + + def get_all(self): + return self.get_all_friends() + self.get_all_groups() + self.get_all_group_peers() + + # Caching + + def clear_cache(self): + self._cache.clear() + + def remove_contact_from_cache(self, contact_public_key): + if contact_public_key in self._cache: + del self._cache[contact_public_key] + + # Private methods + + def _get_contact_from_cache(self, public_key): + return self._cache[public_key] if public_key in self._cache else None + + def _add_to_cache(self, public_key, contact): + self._cache[public_key] = contact + + def _get_group_peer(self, group, peer): + return self._group_peer_factory.create_group_peer(group, peer) diff --git a/toxygen/contacts/contacts_manager.py b/toxygen/contacts/contacts_manager.py new file mode 100644 index 0000000..6f0dae8 --- /dev/null +++ b/toxygen/contacts/contacts_manager.py @@ -0,0 +1,670 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import logging + +from contacts.friend import Friend +from contacts.group_chat import GroupChat +from messenger.messages import * +from common.tox_save import ToxSave +from contacts.group_peer_contact import GroupPeerContact +from groups.group_peer import GroupChatPeer +from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE +import toxygen_wrapper.toxcore_enums_and_consts as enums + +# LOG=util.log +global LOG +LOG = logging.getLogger('app.'+__name__) + +UINT32_MAX = 2 ** 32 -1 + +def set_contact_kind(contact) -> None: + bInvite = len(contact.name) == enums.TOX_PUBLIC_KEY_SIZE * 2 and \ + contact.status_message == '' + bBot = not bInvite and contact.name.lower().endswith(' bot') + if type(contact) == Friend and bInvite: + contact._kind = 'invite' + elif type(contact) == Friend and bBot: + contact._kind = 'bot' + elif type(contact) == Friend: + contact._kind = 'friend' + elif type(contact) == GroupChat: + contact._kind = 'group' + elif type(contact) == GroupChatPeer: + contact._kind = 'grouppeer' + +class ContactsManager(ToxSave): + """ + Represents contacts list. + """ + + def __init__(self, tox, settings, screen, profile_manager, contact_provider, history, tox_dns, + messages_items_factory): + super().__init__(tox) + self._settings = settings + self._screen = screen + self._ms = screen + self._profile_manager = profile_manager + self._contact_provider = contact_provider + self._tox_dns = tox_dns + self._messages_items_factory = messages_items_factory + self._messages = screen.messages + self._contacts = [] + self._active_contact = -1 + self._active_contact_changed = Event() + self._sorting = settings['sorting'] + self._filter_string = '' + screen.contacts_filter.setCurrentIndex(int(self._sorting)) + self._history = history + self._load_contacts() + + def _log(self, s) -> None: + try: + self._ms._log(s) + except: pass + + def get_contact(self, num): + if num < 0 or num >= len(self._contacts): + return None + return self._contacts[num] + + def get_curr_contact(self): + return self._contacts[self._active_contact] if self._active_contact + 1 else None + + def save_profile(self) -> None: + data = self._tox.get_savedata() + self._profile_manager.save_profile(data) + + def is_friend_active(self, friend_number:int) -> bool: + if not self.is_active_a_friend(): + return False + + return self.get_curr_contact().number == friend_number + + def is_group_active(self, group_number) -> bool: + if self.is_active_a_friend(): + return False + + return self.get_curr_contact().number == group_number + + def is_contact_active(self, contact) -> bool: + if self._active_contact == -1: +# LOG.debug("No self._active_contact") + return False + if self._active_contact >= len(self._contacts): + LOG.warn(f"ERROR _active_contact={self._active_contact} >= contacts len={len(self._contacts)}") + return False + if not self._contacts[self._active_contact]: + LOG.warn(f"ERROR NULL {self._contacts[self._active_contact]} {contact.tox_id}") + return False + + if not hasattr(contact, 'tox_id'): + LOG.warn(f"ERROR is_contact_active no contact.tox_id {type(contact)} contact={contact}") + return False + + return self._contacts[self._active_contact].tox_id == contact.tox_id + + # Reconnection support + + def reset_contacts_statuses(self) -> None: + for contact in self._contacts: + contact.status = None + + # Work with active friend + + def get_active(self): + return self._active_contact + + def set_active(self, value): + """ + Change current active friend or update info + :param value: number of new active friend in friend's list + """ + if value is None and self._active_contact == -1: # nothing to update + return + if value == -1: # all friends were deleted + self._screen.account_name.setText('') + self._screen.account_status.setText('') + self._screen.account_status.setToolTip('') + self._active_contact = -1 + self._screen.account_avatar.setHidden(True) + self._messages.clear() + self._screen.messageEdit.clear() + return + try: + self._screen.typing.setVisible(False) + current_contact = self.get_curr_contact() + if current_contact is not None: + # TODO: send when needed + current_contact.typing_notification_handler.send(self._tox, False) + current_contact.remove_messages_widgets() # TODO: if required + self._unsubscribe_from_events(current_contact) + + if self._active_contact >= 0 and self._active_contact != value: + try: + current_contact.curr_text = self._screen.messageEdit.toPlainText() + except: + pass + + # IndexError: list index out of range + if value >= len(self._contacts): + LOG.warn("CM.set_active value too big: {{self._contacts}}") + return + contact = self._contacts[value] + self._subscribe_to_events(contact) + contact.remove_invalid_unsent_files() + if self._active_contact != value: + self._screen.messageEdit.setPlainText(contact.curr_text) + self._active_contact = value + contact.reset_messages() + if not self._settings['save_history']: + contact.delete_old_messages() + self._messages.clear() + contact.load_corr() + corr = contact.get_corr()[-PAGE_SIZE:] + for message in corr: + if message.type == MESSAGE_TYPE['FILE_TRANSFER']: + self._messages_items_factory.create_file_transfer_item(message) + elif message.type == MESSAGE_TYPE['INLINE']: + self._messages_items_factory.create_inline_item(message) + else: + self._messages_items_factory.create_message_item(message) + self._messages.scrollToBottom() + # if value in self._call: + # self._screen.active_call() + # elif value in self._incoming_calls: + # self._screen.incoming_call() + # else: + # self._screen.call_finished() + self._set_current_contact_data(contact) + self._active_contact_changed(contact) + except Exception as e: # no friend found. ignore + LOG.warn(f"CM.set_active EXCEPTION value:{value} len={len(self._contacts)} {e}") + # gulp raise + + active_contact = property(get_active, set_active) + + def get_active_contact_changed(self): + return self._active_contact_changed + + active_contact_changed = property(get_active_contact_changed) + + def update(self): + if self._active_contact + 1: + self.set_active(self._active_contact) + + def is_active_a_friend(self): + return type(self.get_curr_contact()) is Friend + + def is_active_a_group(self): + return type(self.get_curr_contact()) is GroupChat + + def is_active_a_group_chat_peer(self): + return type(self.get_curr_contact()) is GroupPeerContact + + # Filtration + + def filtration_and_sorting(self, sorting=0, filter_str=''): + """ + Filtration of friends list + :param sorting: 0 - no sorting, 1 - online only, 2 - online first, 3 - by name, + 4 - online and by name, 5 - online first and by name, 6 kind + :param filter_str: show contacts which name contains this substring + """ + filter_str = filter_str.lower() + current_contact = self.get_curr_contact() + + for index, contact in enumerate(self._contacts): + if not contact._kind: + set_contact_kind(contact) + + if sorting > 6 or sorting < 0: + sorting = 0 + + if sorting in (1, 2, 4, 5): # online first + self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True) + sort_by_name = sorting in (4, 5) + # save results of previous sorting + online_friends = filter(lambda x: x.status is not None, self._contacts) + online_friends_count = len(list(online_friends)) + part1 = self._contacts[:online_friends_count] + part2 = self._contacts[online_friends_count:] + key_lambda = lambda x: x.name.lower() if sort_by_name else x.number + part1 = sorted(part1, key=key_lambda) + part2 = sorted(part2, key=key_lambda) + self._contacts = part1 + part2 + elif sorting == 0: + # AttributeError: 'NoneType' object has no attribute 'number' + for (i, contact) in enumerate(self._contacts): + if contact is None or not hasattr(contact, 'number'): + LOG.error(f"Contact {i} is None or not hasattr 'number'") + del self._contacts[i] + continue + contacts = sorted(self._contacts, key=lambda c: c.number) + friends = filter(lambda c: type(c) is Friend, contacts) + groups = filter(lambda c: type(c) is GroupChat, contacts) + group_peers = filter(lambda c: type(c) is GroupPeerContact, contacts) + self._contacts = list(friends) + list(groups) + list(group_peers) + elif sorting == 6: + self._contacts = sorted(self._contacts, key=lambda x: x._kind) + else: + self._contacts = sorted(self._contacts, key=lambda x: x.name.lower()) + + + # change item widgets + for index, contact in enumerate(self._contacts): + list_item = self._screen.friends_list.item(index) + item_widget = self._screen.friends_list.itemWidget(list_item) + if not item_widget: + LOG_WARN("CM.filtration_and_sorting( item_widget is NULL") + continue + contact.set_widget(item_widget) + + for index, friend in enumerate(self._contacts): + filtered_by_name = filter_str in friend.name.lower() + friend.visibility = (friend.status is not None or sorting not in (1, 4)) and filtered_by_name + # show friend even if it's hidden when there any unread messages/actions + friend.visibility = friend.visibility or friend.messages or friend.actions + item = self._screen.friends_list.item(index) + item_widget = self._screen.friends_list.itemWidget(item) + item.setSizeHint(QtCore.QSize(250, item_widget.height() if friend.visibility else 0)) + + # save soring results + self._sorting, self._filter_string = sorting, filter_str + self._settings['sorting'] = self._sorting + self._settings.save() + + # update active contact + if current_contact is not None: + index = self._contacts.index(current_contact) + self.set_active(index) + + def update_filtration(self): + """ + Update list of contacts when 1 of friends change connection status + """ + self.filtration_and_sorting(self._sorting, self._filter_string) + + # Contact getters + + def get_friend_by_number(self, number): + return list(filter(lambda c: c.number == number and type(c) is Friend, self._contacts))[0] + + def get_group_by_number(self, number): + return list(filter(lambda c: c.number == number and type(c) is GroupChat, self._contacts))[0] + + def get_or_create_group_peer_contact(self, group_number, peer_id): + group = self.get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + if peer is None: + LOG.warn(f'get_or_create_group_peer_contact group_number={group_number} peer_id={peer_id} peer={peer}') + return None + LOG.debug(f'get_or_create_group_peer_contact group_number={group_number} peer_id={peer_id} peer={peer}') + if not self.check_if_contact_exists(peer.public_key): + contact = self.add_group_peer(group, peer) + # dunno + return contact + # me - later wrong kind of object? + return self.get_contact_by_tox_id(peer.public_key) + + def check_if_contact_exists(self, tox_id): + return any(filter(lambda c: c.tox_id == tox_id, self._contacts)) + + def get_contact_by_tox_id(self, tox_id): + return list(filter(lambda c: c.tox_id == tox_id, self._contacts))[0] + + def get_active_number(self): + return self.get_curr_contact().number if self._active_contact + 1 else -1 + + def get_active_name(self): + return self.get_curr_contact().name if self._active_contact + 1 else '' + + def is_active_online(self): + return self._active_contact + 1 and self.get_curr_contact().status is not None + + # Work with friends (remove, block, set alias, get public key) + + def set_alias(self, num): + """ + Set new alias for friend + """ + friend = self._contacts[num] + name = friend.name + text = util_ui.tr("Enter new alias for friend {} or leave empty to use friend's name:").format(name) + title = util_ui.tr('Set alias') + text, ok = util_ui.text_dialog(text, title, name) + if not ok: + return + aliases = self._settings['friends_aliases'] + if text: + friend.name = text + try: + index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) + aliases[index] = (friend.tox_id, text) + except: + aliases.append((friend.tox_id, text)) + friend.set_alias(text) + else: # use default name + friend.name = self._tox.friend_get_name(friend.number) + friend.set_alias('') + try: + index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) + del aliases[index] + except: + pass + self._settings.save() + + def friend_public_key(self, num): + return self._contacts[num].tox_id + + def delete_friend(self, num): + """ + Removes friend from contact list + :param num: number of friend in list + """ + friend = self._contacts[num] + self._cleanup_contact_data(friend) + try: + self._tox.friend_delete(friend.number) + except Exception as e: + LOG.warn(f"'There was no friend with the given friend number {e}") + self._delete_contact(num) + + def add_friend(self, tox_id): + """ + Adds friend to list + """ + self._tox.friend_add_norequest(tox_id) + self._add_friend(tox_id) + self.update_filtration() + + def block_user(self, tox_id): + """ + Block user with specified tox id (or public key) - delete from friends list and ignore friend requests + """ + tox_id = tox_id[:enums.TOX_PUBLIC_KEY_SIZE * 2] + if tox_id == self._tox.self_get_address()[:enums.TOX_PUBLIC_KEY_SIZE * 2]: + return + if tox_id not in self._settings['blocked']: + self._settings['blocked'].append(tox_id) + self._settings.save() + try: + num = self._tox.friend_by_public_key(tox_id) + self.delete_friend(num) + self.save_profile() + except: # not in friend list + pass + + def unblock_user(self, tox_id, add_to_friend_list): + """ + Unblock user + :param tox_id: tox id of contact + :param add_to_friend_list: add this contact to friend list or not + """ + self._settings['blocked'].remove(tox_id) + self._settings.save() + if add_to_friend_list: + self.add_friend(tox_id) + self.save_profile() + + # Groups support + + def get_group_chats(self): + return list(filter(lambda c: type(c) is GroupChat, self._contacts)) + + def add_group(self, group_number): + index = len(self._contacts) + group = self._contact_provider.get_group_by_number(group_number) + if group is None: + LOG.warn(f"CM.add_group: NULL group from group_number={group_number}") + elif type(group) == int and group < 0: + LOG.warn(f"CM.add_group: NO group from group={group} group_number={group_number}") + else: + LOG.info(f"CM.add_group: Adding group {group._name}") + self._contacts.append(group) + LOG.info(f"contacts_manager.add_group: saving profile") + self._save_profile() + group.reset_avatar(self._settings['identicons']) + LOG.info(f"contacts_manager.add_group: setting active") + self.set_active(index) + self.update_filtration() + + def delete_group(self, group_number): + group = self.get_group_by_number(group_number) + self._cleanup_contact_data(group) + num = self._contacts.index(group) + self._delete_contact(num) + + # Groups private messaging + + def add_group_peer(self, group, peer): + contact = self._contact_provider.get_group_peer_by_id(group, peer.id) + if self.check_if_contact_exists(contact.tox_id): + return contact + contact._kind = 'grouppeer' + self._contacts.append(contact) + contact.reset_avatar(self._settings['identicons']) + self._save_profile() + return contact + + def remove_group_peer_by_id(self, group, peer_id): + peer = group.get_peer_by_id(peer_id) + if peer: # broken + if not self.check_if_contact_exists(peer.public_key): + return + contact = self.get_contact_by_tox_id(peer.public_key) + self.remove_group_peer(contact) + + def remove_group_peer(self, group_peer_contact): + contact = self.get_contact_by_tox_id(group_peer_contact.tox_id) + if contact: + self._cleanup_contact_data(contact) + num = self._contacts.index(contact) + self._delete_contact(num) + + def get_gc_peer_name(self, name): + group = self.get_curr_contact() + + names = sorted(group.get_peers_names()) + if name in names: # return next nick + index = names.index(name) + index = (index + 1) % len(names) + + return names[index] + + suggested_names = list(filter(lambda x: x.startswith(name), names)) + if not len(suggested_names): + return '\t' + + return suggested_names[0] + + # Friend requests + + def send_friend_request(self, sToxPkOrId, message): + """ + Function tries to send request to contact with specified id + :param sToxPkOrId: id of new contact or tox dns 4 value + :param message: additional message + :return: True on success else error string + """ + retval = '' + try: + message = message or 'Hello! Add me to your contact list please' + if len(sToxPkOrId) == enums.TOX_PUBLIC_KEY_SIZE * 2: # public key + self.add_friend(sToxPkOrId) + title = 'Friend added' + text = 'Friend added without sending friend request' + else: + num = self._tox.friend_add(sToxPkOrId, message.encode('utf-8')) + if num < UINT32_MAX: + tox_pk = sToxPkOrId[:enums.TOX_PUBLIC_KEY_SIZE * 2] + self._add_friend(tox_pk) + self.update_filtration() + title = 'Friend added' + text = 'Friend added by sending friend request' + self.save_profile() + retval = True + else: + title = 'Friend failed' + text = 'Friend failed sending friend request' + retval = text + + except Exception as ex: # wrong data + title = 'Friend add exception' + text = 'Friend request exception with ' + str(ex) + self._log(text) + LOG.exception(text) + LOG.warn(f"DELETE {sToxPkOrId} ?") + retval = str(ex) + title = util_ui.tr(title) + text = util_ui.tr(text) + util_ui.message_box(text, title) + return retval + + def process_friend_request(self, tox_id, message): + """ + Accept or ignore friend request + :param tox_id: tox id of contact + :param message: message + """ + if tox_id in self._settings['blocked']: + return + try: + text = util_ui.tr('User {} wants to add you to contact list. Message:\n{}') + reply = util_ui.question(text.format(tox_id, message), util_ui.tr('Friend request')) + if reply: # accepted + self.add_friend(tox_id) + data = self._tox.get_savedata() + self._profile_manager.save_profile(data) + except Exception as ex: # something is wrong + LOG.error('Accept friend request failed! ' + str(ex)) + + def can_send_typing_notification(self): + return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() + + # Contacts numbers update + + def update_friends_numbers(self): + for friend in self._contact_provider.get_all_friends(): + friend.number = self._tox.friend_by_public_key(friend.tox_id) + self.update_filtration() + + def update_groups_numbers(self): + groups = self._contact_provider.get_all_groups() + LOG.info(f"update_groups_numbers len(groups)={len(groups)}") + # Thread 76 "ToxIterateThrea" received signal SIGSEGV, Segmentation fault. + for i in range(len(groups)): + chat_id = self._tox.group_get_chat_id(i) + if not chat_id: + LOG.warn(f"update_groups_numbers {i} chat_id") + continue + group = self.get_contact_by_tox_id(chat_id) + if not group: + LOG.warn(f"update_groups_numbers {i} group") + continue + group.number = i + self.update_filtration() + + def update_groups_lists(self): + groups = self._contact_provider.get_all_groups() + for group in groups: + group.remove_all_peers_except_self() + + # Private methods + + def _load_contacts(self): + self._load_friends() + self._load_groups() + if len(self._contacts): + self.set_active(0) + # filter(lambda c: not c.has_avatar(), self._contacts) + for (i, contact) in enumerate(self._contacts): + if contact is None: + LOG.warn(f"_load_contacts NULL contact {i}") + LOG.info(f"_load_contacts deleting NULL {self._contacts[i]}") + del self._contacts[i] + #? self.save_profile() + continue + if contact.has_avatar(): continue + contact.reset_avatar(self._settings['identicons']) + self.update_filtration() + + def _load_friends(self): + self._contacts.extend(self._contact_provider.get_all_friends()) + + def _load_groups(self): + self._contacts.extend(self._contact_provider.get_all_groups()) + + # Current contact subscriptions + + def _subscribe_to_events(self, contact): + contact.name_changed_event.add_callback(self._current_contact_name_changed) + contact.status_changed_event.add_callback(self._current_contact_status_changed) + contact.status_message_changed_event.add_callback(self._current_contact_status_message_changed) + contact.avatar_changed_event.add_callback(self._current_contact_avatar_changed) + + def _unsubscribe_from_events(self, contact): + contact.name_changed_event.remove_callback(self._current_contact_name_changed) + contact.status_changed_event.remove_callback(self._current_contact_status_changed) + contact.status_message_changed_event.remove_callback(self._current_contact_status_message_changed) + contact.avatar_changed_event.remove_callback(self._current_contact_avatar_changed) + + def _current_contact_name_changed(self, name): + self._screen.account_name.setText(name) + + def _current_contact_status_changed(self, status): + pass + + def _current_contact_status_message_changed(self, status_message): + self._screen.account_status.setText(status_message) + + def _current_contact_avatar_changed(self, avatar_path): + self._set_current_contact_avatar(avatar_path) + + def _set_current_contact_data(self, contact): + self._screen.account_name.setText(contact.name) + self._screen.account_status.setText(contact.status_message) + self._set_current_contact_avatar(contact.get_avatar_path()) + + def _set_current_contact_avatar(self, avatar_path): + width = self._screen.account_avatar.width() + pixmap = QtGui.QPixmap(avatar_path) + self._screen.account_avatar.setPixmap(pixmap.scaled(width, width, + QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) + + def _add_friend(self, tox_id): + self._history.add_friend_to_db(tox_id) + friend = self._contact_provider.get_friend_by_public_key(tox_id) + index = len(self._contacts) + self._contacts.append(friend) + if not friend.has_avatar(): + friend.reset_avatar(self._settings['identicons']) + self._save_profile() + self.set_active(index) + + def _save_profile(self): + data = self._tox.get_savedata() + self._profile_manager.save_profile(data) + + def _cleanup_contact_data(self, contact): + try: + index = list(map(lambda x: x[0], self._settings['friends_aliases'])).index(contact.tox_id) + del self._settings['friends_aliases'][index] + except Exception as e: + pass + if contact.tox_id in self._settings['notes']: + del self._settings['notes'][contact.tox_id] + self._settings.save() + self._history.delete_history(contact) + if contact.has_avatar(): + avatar_path = contact.get_contact_avatar_path() + remove(avatar_path) + + def _delete_contact(self, num): + self.set_active(-1 if len(self._contacts) == 1 else 0) + + self._contact_provider.remove_contact_from_cache(self._contacts[num].tox_id) + del self._contacts[num] + self._screen.friends_list.takeItem(num) + self._save_profile() + + self.update_filtration() diff --git a/toxygen/contacts/friend.py b/toxygen/contacts/friend.py new file mode 100644 index 0000000..24b04ad --- /dev/null +++ b/toxygen/contacts/friend.py @@ -0,0 +1,68 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os + +from contacts import contact, common +from messenger.messages import * +from contacts.contact_menu import * + +class Friend(contact.Contact): + """ + Friend in list of friends. + """ + + def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id): + super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id) + self._receipts = 0 + self._typing_notification_handler = common.FriendTypingNotificationHandler(number) + + # File transfers support + + def insert_inline(self, before_message_id, inline): + """ + Update status of active transfer and load inline if needed + """ + try: + tr = list(filter(lambda m: m.message_id == before_message_id, self._corr))[0] + i = self._corr.index(tr) + if inline: # inline was loaded + self._corr.insert(i, inline) + return i - len(self._corr) + except: + return -1 + + def get_unsent_files(self): + messages = filter(lambda m: type(m) is UnsentFileMessage, self._corr) + return list(messages) + + def clear_unsent_files(self): + self._corr = list(filter(lambda m: type(m) is not UnsentFileMessage, self._corr)) + + def remove_invalid_unsent_files(self): + def is_valid(message): + if type(message) is not UnsentFileMessage: + return True + if message.data is not None: + return True + return os.path.exists(message.path) + + self._corr = list(filter(is_valid, self._corr)) + + def delete_one_unsent_file(self, message_id): + self._corr = list(filter(lambda m: not (type(m) is UnsentFileMessage and m.message_id == message_id), + self._corr)) + + # Full status + + def get_full_status(self): + return self._status_message + + # Typing notifications + + def get_typing_notification_handler(self): + return self._typing_notification_handler + + # Context menu support + + def get_context_menu_generator(self): + return FriendMenuGenerator(self) diff --git a/toxygen/contacts/friend_factory.py b/toxygen/contacts/friend_factory.py new file mode 100644 index 0000000..31d5eec --- /dev/null +++ b/toxygen/contacts/friend_factory.py @@ -0,0 +1,43 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from contacts.friend import Friend +from common.tox_save import ToxSave + +class FriendFactory(ToxSave): + + def __init__(self, profile_manager, settings, tox, db, items_factory): + super().__init__(tox) + self._profile_manager = profile_manager + self._settings = settings + self._db = db + self._items_factory = items_factory + + def create_friend_by_public_key(self, public_key): + friend_number = self._tox.friend_by_public_key(public_key) + return self.create_friend_by_number(friend_number) + + def create_friend_by_number(self, friend_number:int): + aliases = self._settings['friends_aliases'] + sToxPk = self._tox.friend_get_public_key(friend_number) + assert sToxPk, sToxPk + try: + alias = list(filter(lambda x: x[0] == sToxPk, aliases))[0][1] + except: + alias = '' + item = self._create_friend_item() + name = alias or self._tox.friend_get_name(friend_number) or sToxPk + status_message = self._tox.friend_get_status_message(friend_number) + message_getter = self._db.messages_getter(sToxPk) + friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, sToxPk) + friend.set_alias(alias) + + return friend + + # Private methods + + def _create_friend_item(self): + """ + Method-factory + :return: new widget for friend instance + """ + return self._items_factory.create_contact_item() diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py new file mode 100644 index 0000000..c060e65 --- /dev/null +++ b/toxygen/contacts/group_chat.py @@ -0,0 +1,161 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from contacts import contact +from contacts.contact_menu import GroupMenuGenerator +import utils.util as util +from groups.group_peer import GroupChatPeer +from toxygen_wrapper import toxcore_enums_and_consts as constants +from common.tox_save import ToxSave +from groups.group_ban import GroupBan + +global LOG +import logging +LOG = logging.getLogger(__name__) +from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +class GroupChat(contact.Contact, ToxSave): + + def __init__(self, tox, profile_manager, message_getter, number, name, status_message, widget, tox_id, is_private): + super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id) + ToxSave.__init__(self, tox) + + self._is_private = is_private + self._password = str() + self._peers_limit = 512 + self._peers = [] + self._add_self_to_gc() + + def remove_invalid_unsent_files(self): + pass + + def get_context_menu_generator(self): + return GroupMenuGenerator(self) + + # Properties + + def get_is_private(self): + return self._is_private + + def set_is_private(self, is_private): + self._is_private = is_private + + is_private = property(get_is_private, set_is_private) + + def get_password(self): + return self._password + + def set_password(self, password): + self._password = password + + password = property(get_password, set_password) + + def get_peers_limit(self): + return self._peers_limit + + def set_peers_limit(self, peers_limit): + self._peers_limit = peers_limit + + peers_limit = property(get_peers_limit, set_peers_limit) + + # Peers methods + + def get_self_peer(self): + return self._peers[0] + + def get_self_name(self): + return self._peers[0].name + + def get_self_role(self): + return self._peers[0].role + + def is_self_moderator_or_founder(self): + return self.get_self_role() <= constants.TOX_GROUP_ROLE['MODERATOR'] + + def is_self_founder(self): + return self.get_self_role() == constants.TOX_GROUP_ROLE['FOUNDER'] + + def add_peer(self, peer_id, is_current_user=False): + "called from callbacks" + if peer_id > self._peers_limit: + LOG_WARN(f"add_peer id={peer_id} > {self._peers_limit}") + return + + status_message = f"Private in {self.name}" + LOG_TRACE(f"GC.add_peer id={peer_id} status_message={status_message}") + peer = GroupChatPeer(peer_id, + self._tox.group_peer_get_name(self._number, peer_id), + self._tox.group_peer_get_status(self._number, peer_id), + self._tox.group_peer_get_role(self._number, peer_id), + self._tox.group_peer_get_public_key(self._number, peer_id), + is_current_user, + status_message=status_message) + self._peers.append(peer) + + def remove_peer(self, peer_id): + if peer_id == self.get_self_peer().id: # we were kicked or banned + self.remove_all_peers_except_self() + else: + peer = self.get_peer_by_id(peer_id) + if peer: # broken + self._peers.remove(peer) + else: + LOG_WARN(f"remove_peer empty peers for {peer_id}") + + def get_peer_by_id(self, peer_id): + peers = list(filter(lambda p: p.id == peer_id, self._peers)) + if peers: + return peers[0] + else: + LOG_WARN(f"get_peer_by_id empty peers for {peer_id}") + return None + + def get_peer_by_public_key(self, public_key): + peers = list(filter(lambda p: p.public_key == public_key, self._peers)) + # DEBUGc: group_moderation #0 mod_id=4294967295 event_type=3 + # WARN_: get_peer_by_id empty peers for 4294967295 + if peers: + return peers[0] + else: + LOG_WARN(f"get_peer_by_public_key empty peers for {public_key}") + return None + + def remove_all_peers_except_self(self): + self._peers = self._peers[:1] + + def get_peers_names(self): + peers_names = map(lambda p: p.name, self._peers) + if peers_names: # broken + return list(peers_names) + else: + LOG_WARN(f"get_peers_names empty peers") + #? broken + return [] + + def get_peers(self): + return self._peers[:] + + peers = property(get_peers) + + def get_bans(self): + return [] +# ban_ids = self._tox.group_ban_get_list(self._number) +# bans = [] +# for ban_id in ban_ids: +# ban = GroupBan(ban_id, +# self._tox.group_ban_get_target(self._number, ban_id), +# self._tox.group_ban_get_time_set(self._number, ban_id)) +# bans.append(ban) +# +# return bans +# + bans = property(get_bans) + + # Private methods + + @staticmethod + def _get_default_avatar_path(): + return util.join_path(util.get_images_directory(), 'group.png') + + def _add_self_to_gc(self): + peer_id = self._tox.group_self_get_peer_id(self._number) + self.add_peer(peer_id, True) diff --git a/toxygen/contacts/group_factory.py b/toxygen/contacts/group_factory.py new file mode 100644 index 0000000..4345c4b --- /dev/null +++ b/toxygen/contacts/group_factory.py @@ -0,0 +1,59 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from contacts.group_chat import GroupChat +from common.tox_save import ToxSave +import toxygen_wrapper.toxcore_enums_and_consts as constants + +global LOG +import logging +LOG = logging.getLogger(__name__) + +class GroupFactory(ToxSave): + + def __init__(self, profile_manager, settings, tox, db, items_factory): + super().__init__(tox) + self._profile_manager = profile_manager + self._settings = settings + self._db = db + self._items_factory = items_factory + + def create_group_by_chat_id(self, chat_id): + return self.create_group_by_public_key(chat_id) + + def create_group_by_public_key(self, public_key): + group_number = self._get_group_number_by_chat_id(public_key) + return self.create_group_by_number(group_number) + + def create_group_by_number(self, group_number): + LOG.info(f"create_group_by_number {group_number}") + aliases = self._settings['friends_aliases'] + tox_id = self._tox.group_get_chat_id(group_number) + try: + alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] + except: + alias = '' + item = self._create_group_item() + name = alias or self._tox.group_get_name(group_number) or tox_id + status_message = self._tox.group_get_topic(group_number) + message_getter = self._db.messages_getter(tox_id) + is_private = self._tox.group_get_privacy_state(group_number) == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE'] + group = GroupChat(self._tox, self._profile_manager, message_getter, group_number, name, status_message, + item, tox_id, is_private) + group.set_alias(alias) + + return group + + # Private methods + + def _create_group_item(self): + """ + Method-factory + :return: new widget for group instance + """ + return self._items_factory.create_contact_item() + + def _get_group_number_by_chat_id(self, chat_id): + for i in range(self._tox.group_get_number_groups()+100): + if self._tox.group_get_chat_id(i) == chat_id: + return i + return -1 diff --git a/toxygen/contacts/group_peer_contact.py b/toxygen/contacts/group_peer_contact.py new file mode 100644 index 0000000..3e6131c --- /dev/null +++ b/toxygen/contacts/group_peer_contact.py @@ -0,0 +1,22 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import contacts.contact +from contacts.contact_menu import GroupPeerMenuGenerator + +class GroupPeerContact(contacts.contact.Contact): + + def __init__(self, profile_manager, message_getter, peer_number, name, widget, tox_id, group_pk, status_message=None): + if status_message is None: status_message=str() + super().__init__(profile_manager, message_getter, peer_number, name, status_message, widget, tox_id) + self._group_pk = group_pk + + def get_group_pk(self): + return self._group_pk + + group_pk = property(get_group_pk) + + def remove_invalid_unsent_files(self): + pass + + def get_context_menu_generator(self): + return GroupPeerMenuGenerator(self) diff --git a/toxygen/contacts/group_peer_factory.py b/toxygen/contacts/group_peer_factory.py new file mode 100644 index 0000000..1804b50 --- /dev/null +++ b/toxygen/contacts/group_peer_factory.py @@ -0,0 +1,26 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from common.tox_save import ToxSave +from contacts.group_peer_contact import GroupPeerContact + +class GroupPeerFactory(ToxSave): + + def __init__(self, tox, profile_manager, db, items_factory): + super().__init__(tox) + self._profile_manager = profile_manager + self._db = db + self._items_factory = items_factory + + def create_group_peer(self, group, peer): + item = self._create_group_peer_item() + message_getter = self._db.messages_getter(peer.public_key) + group_peer_contact = GroupPeerContact(self._profile_manager, message_getter, peer.id, peer.name, + item, + peer.public_key, + group.tox_id, + status_message=peer.status_message) + group_peer_contact.status = peer.status + + return group_peer_contact + + def _create_group_peer_item(self): + return self._items_factory.create_contact_item() diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py new file mode 100644 index 0000000..3afcf2b --- /dev/null +++ b/toxygen/contacts/profile.py @@ -0,0 +1,107 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from contacts import basecontact +import random +import threading +import common.tox_save as tox_save +from middleware.threads import invoke_in_main_thread + +iUMAXINT = 4294967295 +iRECONNECT = 50 + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +class Profile(basecontact.BaseContact, tox_save.ToxSave): + """ + Profile of current toxygen user. + """ + def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action, app=None): + """ + :param tox: tox instance + :param screen: ref to main screen + """ + assert tox + basecontact.BaseContact.__init__(self, + profile_manager, + tox.self_get_name(), + tox.self_get_status_message(), + screen, + tox.self_get_address()) + tox_save.ToxSave.__init__(self, tox) + self._screen = screen + self._messages = screen.messages + self._contacts_provider = contacts_provider + self._reset_action = reset_action + self._waiting_for_reconnection = False + self._timer = None + self._app = app + + # Edit current user's data + + def change_status(self) -> None: + """ + Changes status of user (online, away, busy) + """ + if self._status is not None: + self.set_status((self._status + 1) % 3) + + def set_status(self, status) -> None: + super().set_status(status) + if status is not None: + self._tox.self_set_status(status) + elif not self._waiting_for_reconnection: + self._waiting_for_reconnection = True + self._timer = threading.Timer(iRECONNECT, self._reconnect) + self._timer.start() + + def set_name(self, value) -> None: + if self.name == value: + return + super().set_name(value) + self._tox.self_set_name(self._name) + + def set_status_message(self, value) -> None: + super().set_status_message(value) + self._tox.self_set_status_message(self._status_message) + + def set_new_nospam(self): + """Sets new nospam part of tox id""" + self._tox.self_set_nospam(random.randint(0, iUMAXINT)) # no spam - uint32 + self._tox_id = self._tox.self_get_address() + self._sToxId = self._tox.self_get_address() + return self._sToxId + + # Reset + + def restart(self) -> None: + """ + Recreate tox instance + """ + self.status = None + invoke_in_main_thread(self._reset_action) + + def _reconnect(self) -> None: + self._waiting_for_reconnection = False + if self._app and self._app.bAppExiting: + # dont do anything after the app has been shipped + # there's a segv that results + return + contacts = self._contacts_provider.get_all_friends() + all_friends_offline = all(list(map(lambda x: x.status is None, contacts))) + if self.status is None or (all_friends_offline and len(contacts)): + self._waiting_for_reconnection = True + self.restart() + self._timer = threading.Timer(iRECONNECT, self._reconnect) + self._timer.start() + +# Current thread 0x00007901a13ccb80 (most recent call first): +# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 826 in self_get_friend_list_size +# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 838 in self_get_friend_list +# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact_provider.py", line 45 in get_all_friends +# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/profile.py", line 90 in _reconnect +# File "/usr/lib/python3.11/threading.py", line 1401 in run +# File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner +# File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap +# + diff --git a/toxygen/file_transfers/__init__.py b/toxygen/file_transfers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/file_transfers.py b/toxygen/file_transfers/file_transfers.py similarity index 58% rename from toxygen/file_transfers.py rename to toxygen/file_transfers/file_transfers.py index 2c1f73e..5fa87f9 100644 --- a/toxygen/file_transfers.py +++ b/toxygen/file_transfers/file_transfers.py @@ -1,25 +1,25 @@ -from 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, sleep -from tox import Tox -import settings -try: - from PySide import QtCore -except ImportError: - from PyQt4 import QtCore +from time import time -# TODO: threads! +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 - -TOX_FILE_TRANSFER_STATE = { +FILE_TRANSFER_STATE = { 'RUNNING': 0, 'PAUSED_BY_USER': 1, 'CANCELLED': 2, 'FINISHED': 3, 'PAUSED_BY_FRIEND': 4, 'INCOMING_NOT_STARTED': 5, - 'OUTGOING_NOT_STARTED': 6 + 'OUTGOING_NOT_STARTED': 6, + 'UNSENT': 7 } ACTIVE_FILE_TRANSFERS = (0, 1, 4, 5, 6) @@ -30,98 +30,122 @@ DO_NOT_SHOW_ACCEPT_BUTTON = (2, 3, 4, 6) SHOW_PROGRESS_BAR = (0, 1, 4) -ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png') + +def is_inline(file_name): + allowed_inlines = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png') + + return file_name in allowed_inlines or file_name.startswith('qTox_Image_') -class StateSignal(QtCore.QObject): - try: - signal = QtCore.Signal(int, float, int) # state and progress - except: - signal = QtCore.pyqtSignal(int, float, int) # state and progress - pyqt4 - - -class FileTransfer(QtCore.QObject): +class FileTransfer: """ Superclass for file transfers """ def __init__(self, path, tox, friend_number, size, file_number=None): - QtCore.QObject.__init__(self) self._path = path self._tox = tox self._friend_number = friend_number - self.state = TOX_FILE_TRANSFER_STATE['RUNNING'] + self._state = FILE_TRANSFER_STATE['RUNNING'] self._file_number = file_number self._creation_time = None self._size = float(size) self._done = 0 - self._state_changed = StateSignal() - - def set_tox(self, tox): - self._tox = tox + self._state_changed_event = Event() + self._finished_event = Event() + self._file_id = self._file = None def set_state_changed_handler(self, handler): - self._state_changed.signal.connect(handler) + self._state_changed_event += lambda *args: invoke_in_main_thread(handler, *args) - def signal(self): - percentage = self._done / self._size if self._size else 0 - if self._creation_time is None or not percentage: - t = -1 - else: - t = ((time() - self._creation_time) / percentage) * (1 - percentage) - self._state_changed.signal.emit(self.state, percentage, int(t)) + def set_transfer_finished_handler(self, handler): + self._finished_event += lambda *args: invoke_in_main_thread(handler, *args) def get_file_number(self): return self._file_number + file_number = property(get_file_number) + + def get_state(self): + return self._state + + def set_state(self, value): + self._state = value + self._signal() + + state = property(get_state, set_state) + def get_friend_number(self): return self._friend_number + friend_number = property(get_friend_number) + + def get_file_id(self): + return self._file_id +#? return self._tox.file_get_file_id(self._friend_number, self._file_number) + + file_id = property(get_file_id) + + def get_path(self): + return self._path + + path = property(get_path) + + def get_size(self): + return self._size + + size = property(get_size) + def cancel(self): self.send_control(TOX_FILE_CONTROL['CANCEL']) - if hasattr(self, '_file'): + if self._file is not None: self._file.close() - self.signal() + self._signal() def cancelled(self): - if hasattr(self, '_file'): - sleep(0.1) + if self._file is not None: self._file.close() - self.state = TOX_FILE_TRANSFER_STATE['CANCELLED'] - self.signal() + self.set_state(FILE_TRANSFER_STATE['CANCELLED']) def pause(self, by_friend): if not by_friend: self.send_control(TOX_FILE_CONTROL['PAUSE']) else: - self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'] - self.signal() + self.set_state(FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']) def send_control(self, control): if self._tox.file_control(self._friend_number, self._file_number, control): - self.state = control - self.signal() + self.set_state(control) - def get_file_id(self): - return self._tox.file_get_file_id(self._friend_number, self._file_number) + def _signal(self): + percentage = self._done / self._size if self._size else 0 + if self._creation_time is None or not percentage: + t = -1 + else: + t = ((time() - self._creation_time) / percentage) * (1 - percentage) + self._state_changed_event(self.state, percentage, int(t)) + + def _finished(self): + self._finished_event(self._friend_number, self._file_number) -# ----------------------------------------------------------------------------------------------------------------- # Send file -# ----------------------------------------------------------------------------------------------------------------- class SendTransfer(FileTransfer): def __init__(self, path, tox, friend_number, kind=TOX_FILE_KIND['DATA'], file_id=None): if path is not None: - self._file = open(path, 'rb') + fl = open(path, 'rb') size = getsize(path) else: + fl = None size = 0 - super(SendTransfer, self).__init__(path, tox, friend_number, size) - self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + super().__init__(path, tox, friend_number, size) + self._file = fl + self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] self._file_number = tox.file_send(friend_number, kind, size, file_id, bytes(basename(path), 'utf-8') if path else b'') + self._file_id = self.get_file_id() def send_chunk(self, position, size): """ @@ -136,12 +160,12 @@ class SendTransfer(FileTransfer): data = self._file.read(size) self._tox.file_send_chunk(self._friend_number, self._file_number, position, data) self._done += size - self.signal() + self._signal() else: - if hasattr(self, '_file'): + if self._file is not None: self._file.close() - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.signal() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() class SendAvatar(SendTransfer): @@ -150,12 +174,15 @@ class SendAvatar(SendTransfer): """ def __init__(self, path, tox, friend_number): - if path is None: - hash = 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: - hash = Tox.hash(fl.read()) - super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], hash) + 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) class SendFromBuffer(FileTransfer): @@ -164,8 +191,8 @@ class SendFromBuffer(FileTransfer): """ def __init__(self, tox, friend_number, data, file_name): - super(SendFromBuffer, self).__init__(None, tox, friend_number, len(data)) - self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + super().__init__(None, tox, friend_number, len(data)) + self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] self._data = data self._file_number = tox.file_send(friend_number, TOX_FILE_KIND['DATA'], len(data), None, bytes(file_name, 'utf-8')) @@ -173,6 +200,8 @@ class SendFromBuffer(FileTransfer): def get_data(self): return self._data + data = property(get_data) + def send_chunk(self, position, size): if self._creation_time is None: self._creation_time = time() @@ -180,40 +209,46 @@ class SendFromBuffer(FileTransfer): data = self._data[position:position + size] self._tox.file_send_chunk(self._friend_number, self._file_number, position, data) self._done += size - self.signal() else: - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.signal() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() + self._signal() class SendFromFileBuffer(SendTransfer): def __init__(self, *args): - super(SendFromFileBuffer, self).__init__(*args) + super().__init__(*args) def send_chunk(self, position, size): - super(SendFromFileBuffer, self).send_chunk(position, size) + super().send_chunk(position, size) if not size: - chdir(dirname(self._path)) - remove(self._path) + os.chdir(dirname(self._path)) + os.remove(self._path) -# ----------------------------------------------------------------------------------------------------------------- # Receive file -# ----------------------------------------------------------------------------------------------------------------- class ReceiveTransfer(FileTransfer): - def __init__(self, path, tox, friend_number, size, file_number): - super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number) + def __init__(self, path, tox, friend_number, size, file_number, position=0): + super().__init__(path, tox, friend_number, size, file_number) self._file = open(self._path, 'wb') - self._file.truncate(0) - self._file_size = 0 + self._file_size = position + self._file.truncate(position) + self._missed = set() + self._file_id = self.get_file_id() + self._done = position def cancel(self): - super(ReceiveTransfer, self).cancel() + super().cancel() remove(self._path) + def total_size(self): + self._missed.add(self._file_size) + + return min(self._missed) + def write_chunk(self, position, data): """ Incoming chunk @@ -224,20 +259,23 @@ class ReceiveTransfer(FileTransfer): self._creation_time = time() if data is None: self._file.close() - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] - self.signal() + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() else: data = bytearray(data) if self._file_size < position: self._file.seek(0, 2) self._file.write(b'\0' * (position - self._file_size)) + self._missed.add(self._file_size) + else: + self._missed.discard(position) self._file.seek(position) self._file.write(data) l = len(data) if position + l > self._file_size: self._file_size = position + l self._done += l - self.signal() + self._signal() class ReceiveToBuffer(FileTransfer): @@ -246,18 +284,21 @@ class ReceiveToBuffer(FileTransfer): """ def __init__(self, tox, friend_number, size, file_number): - super(ReceiveToBuffer, self).__init__(None, tox, friend_number, size, file_number) + super().__init__(None, tox, friend_number, size, file_number) self._data = bytes() self._data_size = 0 def get_data(self): return self._data + data = property(get_data) + def write_chunk(self, position, data): if self._creation_time is None: self._creation_time = time() if data is None: - self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self.state = FILE_TRANSFER_STATE['FINISHED'] + self._finished() else: data = bytes(data) l = len(data) @@ -267,7 +308,7 @@ class ReceiveToBuffer(FileTransfer): if position + l > self._data_size: self._data_size = position + l self._done += l - self.signal() + self._signal() class ReceiveAvatar(ReceiveTransfer): @@ -275,40 +316,36 @@ class ReceiveAvatar(ReceiveTransfer): Get friend's avatar. Doesn't need file transfer item """ MAX_AVATAR_SIZE = 512 * 1024 - - def __init__(self, tox, friend_number, size, file_number): - path = settings.ProfileHelper.get_path() + 'avatars/{}.png'.format(tox.friend_get_public_key(friend_number)) - super(ReceiveAvatar, self).__init__(path + '.tmp', tox, friend_number, size, file_number) + def __init__(self, path, tox, friend_number, size, file_number): + full_path = path + '.tmp' + super().__init__(full_path, tox, friend_number, size, file_number) if size > self.MAX_AVATAR_SIZE: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - remove(path + '.tmp') + remove(full_path) elif not size: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() - if exists(path): - remove(path) - self._file.close() - remove(path + '.tmp') + remove(full_path) elif exists(path): - hash = self.get_file_id() + 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(path + '.tmp') + remove(full_path) else: self.send_control(TOX_FILE_CONTROL['RESUME']) else: self.send_control(TOX_FILE_CONTROL['RESUME']) def write_chunk(self, position, data): - super(ReceiveAvatar, self).write_chunk(position, data) - if self.state: + if data is None: avatar_path = self._path[:-4] if exists(avatar_path): chdir(dirname(avatar_path)) remove(avatar_path) rename(self._path, avatar_path) + super().write_chunk(position, data) diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py new file mode 100644 index 0000000..a9085c2 --- /dev/null +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -0,0 +1,371 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import logging + +from messenger.messages import * +from file_transfers.file_transfers import SendAvatar, is_inline +from ui.contact_items import * +import utils.util as util +from common.tox_save import ToxSave + +from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +# LOG=util.log +global LOG +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) + +class FileTransfersHandler(ToxSave): + lBlockAvatars = [] + def __init__(self, tox, settings, contact_provider, file_transfers_message_service, profile): + super().__init__(tox) + self._settings = settings + self._contact_provider = contact_provider + self._file_transfers_message_service = file_transfers_message_service + self._file_transfers = {} + # key = (friend number, file number), value - transfer instance + self._paused_file_transfers = dict(settings['paused_file_transfers']) + # key - file id, value: [path, friend number, is incoming, start position] + self._insert_inline_before = {} + # key = (friend number, file number), value - message id + + profile.avatar_changed_event.add_callback(self._send_avatar_to_contacts) + self. lBlockAvatars = [] + + def stop(self) -> None: + self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {} + self._settings.save() + + # File transfers support + + def incoming_file_transfer(self, friend_number, file_number, size, file_name) -> None: + # main thread + """ + New transfer + :param friend_number: number of friend who sent file + :param file_number: file number + :param size: file size in bytes + :param file_name: file name without path + """ + friend = self._get_friend_by_number(friend_number) + if friend is None: + LOG.info(f'incoming_file_handler Friend NULL friend_number={friend_number}') + return + auto = self._settings['allow_auto_accept'] and friend.tox_id in self._settings['auto_accept_from_friends'] + inline = False # ?is_inline(file_name) and self._settings['allow_inline'] + file_id = self._tox.file_get_file_id(friend_number, file_number) + accepted = True + if file_id in self._paused_file_transfers: + LOG_INFO(f'incoming_file_handler paused friend_number={friend_number}') + (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[file_id] + pos = start_position if os.path.exists(path) else 0 + if pos >= size: + self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) + return + self._tox.file_seek(friend_number, file_number, pos) + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + self.accept_transfer(path, friend_number, file_number, size, False, pos) + elif inline and size < 1024 * 1024: + LOG_INFO(f'incoming_file_handler small friend_number={friend_number}') + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + self.accept_transfer('', friend_number, file_number, size, True) + elif auto: + # accepted is really started + LOG_INFO(f'incoming_file_handler auto friend_number={friend_number}') + path = self._settings['auto_accept_path'] or util.curr_directory() + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + self.accept_transfer(path + '/' + file_name, friend_number, file_number, size) + else: + LOG_INFO(f'incoming_file_handler reject friend_number={friend_number}') + accepted = False + # FixME: need GUI ask + # accepted is really started + self._file_transfers_message_service.add_incoming_transfer_message( + friend, accepted, size, file_name, file_number) + + def cancel_transfer(self, friend_number, file_number, already_cancelled=False) -> None: + """ + Stop transfer + :param friend_number: number of friend + :param file_number: file number + :param already_cancelled: was cancelled by friend + """ + # callback + if (friend_number, file_number) in self._file_transfers: + tr = self._file_transfers[(friend_number, file_number)] + if not already_cancelled: + tr.cancel() + else: + tr.cancelled() + if (friend_number, file_number) in self._file_transfers: + del tr + del self._file_transfers[(friend_number, file_number)] + elif not already_cancelled: + self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) + + def cancel_not_started_transfer(self, friend_number, message_id) -> None: + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + friend.delete_one_unsent_file(message_id) + + def pause_transfer(self, friend_number, file_number, by_friend=False) -> None: + """ + Pause transfer with specified data + """ + tr = self._file_transfers[(friend_number, file_number)] + tr.pause(by_friend) + + def resume_transfer(self, friend_number, file_number, by_friend=False) -> None: + """ + Resume transfer with specified data + """ + tr = self._file_transfers[(friend_number, file_number)] + if by_friend: + tr.state = FILE_TRANSFER_STATE['RUNNING'] + else: + tr.send_control(TOX_FILE_CONTROL['RESUME']) + + def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0) -> None: + """ + :param path: path for saving + :param friend_number: friend number + :param file_number: file number + :param size: file size + :param inline: is inline image + :param from_position: position for start + """ + path = self._generate_valid_path(path, from_position) + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + if not inline: + rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number, from_position) + else: + rt = ReceiveToBuffer(self._tox, friend_number, size, file_number) + rt.set_transfer_finished_handler(self.transfer_finished) + message = friend.get_message(lambda m: m.type == MESSAGE_TYPE['FILE_TRANSFER'] + and m.state in (FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'], + FILE_TRANSFER_STATE['RUNNING']) + and m.file_number == file_number) + rt.set_state_changed_handler(message.transfer_updated) + self._file_transfers[(friend_number, file_number)] = rt + rt.send_control(TOX_FILE_CONTROL['RESUME']) + if inline: + self._insert_inline_before[(friend_number, file_number)] = message.message_id + + def send_screenshot(self, data, friend_number) -> None: + """ + Send screenshot + :param data: raw data - png format + :param friend_number: friend number + """ + self.send_inline(data, 'toxygen_inline.png', friend_number) + + def send_sticker(self, path, friend_number) -> None: + with open(path, 'rb') as fl: + data = fl.read() + self.send_inline(data, 'sticker.png', friend_number) + + def send_inline(self, data, file_name, friend_number, is_resend=False) -> None: + friend = self._get_friend_by_number(friend_number) + if friend is None: + LOG_WARN("fsend_inline Error friend is None file_name: {file_name}") + return + if friend.status is None and not is_resend: + self._file_transfers_message_service.add_unsent_file_message(friend, file_name, data) + return + elif friend.status is None and is_resend: + LOG_WARN("fsend_inline Error friend.status is None file_name: {file_name}") + return + st = SendFromBuffer(self._tox, friend.number, data, file_name) + self._send_file_add_set_handlers(st, friend, file_name, True) + + def send_file(self, path, friend_number, is_resend=False, file_id=None) -> None: + """ + Send file to current active friend + :param path: file path + :param friend_number: friend_number + :param is_resend: is 'offline' message + :param file_id: file id of transfer + """ + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + if friend.status is None and not is_resend: + self._file_transfers_message_service.add_unsent_file_message(friend, path, None) + return + elif friend.status is None and is_resend: + LOG_WARN('Error in sending') + return + st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id) + file_name = os.path.basename(path) + self._send_file_add_set_handlers(st, friend, file_name) + + def incoming_chunk(self, friend_number, file_number, position, data) -> None: + """ + Incoming chunk + """ + self._file_transfers[(friend_number, file_number)].write_chunk(position, data) + + def outgoing_chunk(self, friend_number, file_number, position, size) -> None: + """ + Outgoing chunk + """ + self._file_transfers[(friend_number, file_number)].send_chunk(position, size) + + def transfer_finished(self, friend_number, file_number) -> None: + transfer = self._file_transfers[(friend_number, file_number)] + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + t = type(transfer) + if t is ReceiveAvatar: + friend.load_avatar() + elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image + LOG.debug('inline') + inline = InlineImageMessage(transfer.data) + message_id = self._insert_inline_before[(friend_number, file_number)] + del self._insert_inline_before[(friend_number, file_number)] + if friend is None: return None + index = friend.insert_inline(message_id, inline) + self._file_transfers_message_service.add_inline_message(transfer, index) + del self._file_transfers[(friend_number, file_number)] + + def send_files(self, friend_number:int) -> None: + try: + friend = self._get_friend_by_number(friend_number) + if friend is None: return + friend.remove_invalid_unsent_files() + files = friend.get_unsent_files() + for fl in files: + data, path = fl.data, fl.path + if data is not None: + self.send_inline(data, path, friend_number, True) + else: + self.send_file(path, friend_number, True) + friend.clear_unsent_files() + for key in self._paused_file_transfers.keys(): + # RuntimeError: dictionary changed size during iteration + (path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[key] + if not os.path.exists(path): + del self._paused_file_transfers[key] + elif ft_friend_number == friend_number and not is_incoming: + self.send_file(path, friend_number, True, key) + del self._paused_file_transfers[key] + except Exception as ex: + LOG_ERROR('send_files EXCEPTION in file sending: ' + str(ex)) + + def friend_exit(self, friend_number:int) -> None: + # RuntimeError: dictionary changed size during iteration + lMayChangeDynamically = self._file_transfers.copy() + for friend_num, file_num in lMayChangeDynamically: + if friend_num != friend_number: + continue + if (friend_num, file_num) not in self._file_transfers: + continue + ft = self._file_transfers[(friend_num, file_num)] + if type(ft) is SendTransfer: + try: + file_id = ft.file_id + except Exception as e: + LOG_WARN("friend_exit SendTransfer Error getting file_id: {e}") + # drop through + else: + self._paused_file_transfers[file_id] = [ft.path, friend_num, False, -1] + elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + try: + file_id = ft.file_id + except Exception as e: + LOG_WARN("friend_exit ReceiveTransfer Error getting file_id: {e}") + # drop through + else: + self._paused_file_transfers[file_id] = [ft.path, friend_num, True, ft.total_size()] + self.cancel_transfer(friend_num, file_num, True) + + # Avatars support + + def send_avatar(self, friend_number, avatar_path=None) -> None: + """ + :param friend_number: number of friend who should get new avatar + :param avatar_path: path to avatar or None if reset + """ + return + if (avatar_path, friend_number,) in self.lBlockAvatars: + return + if friend_number is None: + LOG_WARN(f"send_avatar friend_number NULL {friend_number}") + return + if avatar_path and type(avatar_path) != str: + LOG_WARN(f"send_avatar avatar_path type {type(avatar_path)}") + return + LOG_INFO(f"send_avatar avatar_path={avatar_path} friend_number={friend_number}") + try: + # self NOT missing - who's self? + sa = SendAvatar(avatar_path, self._tox, friend_number) + LOG_INFO(f"send_avatar avatar_path={avatar_path} sa={sa}") + self._file_transfers[(friend_number, sa.file_number)] = sa + except Exception as e: + # ArgumentError('This client is currently not connected to the friend.') + LOG_WARN(f"send_avatar EXCEPTION {e}") + self.lBlockAvatars.append( (avatar_path, friend_number,) ) + + def incoming_avatar(self, friend_number, file_number, size) -> None: + """ + Friend changed avatar + :param friend_number: friend number + :param file_number: file number + :param size: size of avatar or 0 (default avatar) + """ + friend = self._get_friend_by_number(friend_number) + if friend is None: return + ra = ReceiveAvatar(friend.get_contact_avatar_path(), self._tox, friend_number, size, file_number) + if ra.state != FILE_TRANSFER_STATE['CANCELLED']: + self._file_transfers[(friend_number, file_number)] = ra + ra.set_transfer_finished_handler(self.transfer_finished) + elif not size: + friend.reset_avatar(self._settings['identicons']) + + def _send_avatar_to_contacts(self, _) -> None: + # from a callback + friends = self._get_all_friends() + for friend in filter(self._is_friend_online, friends): + self.send_avatar(friend.number) + + # Private methods + + def _is_friend_online(self, friend_number:int) -> bool: + friend = self._get_friend_by_number(friend_number) + if friend is None: return None + + return friend.status is not None + + def _get_friend_by_number(self, friend_number:int): + return self._contact_provider.get_friend_by_number(friend_number) + + def _get_all_friends(self): + return self._contact_provider.get_all_friends() + + def _send_file_add_set_handlers(self, st, friend, file_name, inline=False): + st.set_transfer_finished_handler(self.transfer_finished) + file_number = st.get_file_number() + self._file_transfers[(friend.number, file_number)] = st + tm = self._file_transfers_message_service.add_outgoing_transfer_message(friend, st.size, file_name, file_number) + st.set_state_changed_handler(tm.transfer_updated) + if inline: + self._insert_inline_before[(friend.number, file_number)] = tm.message_id + + @staticmethod + def _generate_valid_path(path, from_position): + path, file_name = os.path.split(path) + new_file_name, i = file_name, 1 + if not from_position: + while os.path.isfile(join_path(path, new_file_name)): # file with same name already exists + if '.' in file_name: # has extension + d = file_name.rindex('.') + else: # no extension + d = len(file_name) + new_file_name = file_name[:d] + ' ({})'.format(i) + file_name[d:] + i += 1 + path = join_path(path, new_file_name) + + return path diff --git a/toxygen/file_transfers/file_transfers_messages_service.py b/toxygen/file_transfers/file_transfers_messages_service.py new file mode 100644 index 0000000..1b292ee --- /dev/null +++ b/toxygen/file_transfers/file_transfers_messages_service.py @@ -0,0 +1,94 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import logging + +from messenger.messenger import * +import utils.util as util +from file_transfers.file_transfers import * + +global LOG +LOG = logging.getLogger('app.'+__name__) + +from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +class FileTransfersMessagesService: + + def __init__(self, contacts_manager, messages_items_factory, profile, main_screen): + self._contacts_manager = contacts_manager + self._messages_items_factory = messages_items_factory + self._profile = profile + self._messages = main_screen.messages + + def add_incoming_transfer_message(self, friend, accepted, size, file_name, file_number): + assert friend + author = MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND']) + # accepted is really started + status = FILE_TRANSFER_STATE['RUNNING'] if accepted else FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'] + tm = TransferMessage(author, util.get_unix_time(), status, size, file_name, friend.number, file_number) + + if self._is_friend_active(friend.number): + self._create_file_transfer_item(tm) + self._messages.scrollToBottom() + else: + friend.actions = True + + friend.append_message(tm) + + return tm + + def add_outgoing_transfer_message(self, friend, size, file_name, file_number): + assert friend + author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME']) + status = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] + tm = TransferMessage(author, util.get_unix_time(), status, size, file_name, friend.number, file_number) + + if self._is_friend_active(friend.number): + self._create_file_transfer_item(tm) + self._messages.scrollToBottom() + + friend.append_message(tm) + + return tm + + def add_inline_message(self, transfer, index) -> None: + """callback""" + if not self._is_friend_active(transfer.friend_number): + return + if transfer is None or not hasattr(transfer, 'data') or \ + not transfer.data: + LOG_ERROR(f"add_inline_message empty data") + return + count = self._messages.count() + if count + index + 1 >= 0: + # assumes .data + self._create_inline_item(transfer, count + index + 1) + + def add_unsent_file_message(self, friend, file_path, data): + assert friend + author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME']) + size = os.path.getsize(file_path) if data is None else len(data) + tm = UnsentFileMessage(file_path, data, util.get_unix_time(), author, size, friend.number) + friend.append_message(tm) + + if self._is_friend_active(friend.number): + self._create_unsent_file_item(tm) + self._messages.scrollToBottom() + + return tm + + # Private methods + + def _is_friend_active(self, friend_number:int) -> bool: + if not self._contacts_manager.is_active_a_friend(): + return False + + return friend_number == self._contacts_manager.get_active_number() + + def _create_file_transfer_item(self, tm): + return self._messages_items_factory.create_file_transfer_item(tm) + + def _create_inline_item(self, data, position): + return self._messages_items_factory.create_inline_item(data, False, position) + + def _create_unsent_file_item(self, tm): + return self._messages_items_factory.create_unsent_file_item(tm) diff --git a/toxygen/friend.py b/toxygen/friend.py deleted file mode 100644 index 3dd5b0f..0000000 --- a/toxygen/friend.py +++ /dev/null @@ -1,243 +0,0 @@ -import contact -from messages import * -from history import * -import util -import file_transfers as ft - - -class Friend(contact.Contact): - """ - Friend in list of friends. Can be hidden, properties 'has unread messages' and 'has alias' added - """ - - def __init__(self, message_getter, number, *args): - """ - :param message_getter: gets messages from db - :param number: number of friend. - """ - super(Friend, self).__init__(*args) - self._number = number - self._new_messages = False - self._visible = True - self._alias = False - self._message_getter = message_getter - self._corr = [] - self._unsaved_messages = 0 - self._history_loaded = self._new_actions = False - self._receipts = 0 - self._curr_text = '' - - def __del__(self): - self.set_visibility(False) - del self._widget - if hasattr(self, '_message_getter'): - del self._message_getter - - # ----------------------------------------------------------------------------------------------------------------- - # History support - # ----------------------------------------------------------------------------------------------------------------- - - def get_receipts(self): - return self._receipts - - receipts = property(get_receipts) # read receipts - - def inc_receipts(self): - self._receipts += 1 - - def dec_receipt(self): - if self._receipts: - self._receipts -= 1 - self.mark_as_sent() - - def load_corr(self, first_time=True): - """ - :param first_time: friend became active, load first part of messages - """ - if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')): - return - data = list(self._message_getter.get(PAGE_SIZE)) - if data is not None and len(data): - data.reverse() - else: - return - data = list(map(lambda tupl: TextMessage(*tupl), data)) - self._corr = data + self._corr - self._history_loaded = True - - def get_corr_for_saving(self): - """ - Get data to save in db - :return: list of unsaved messages or [] - """ - messages = list(filter(lambda x: x.get_type() <= 1, self._corr)) - return list(map(lambda x: x.get_data(), messages[-self._unsaved_messages:])) if self._unsaved_messages else [] - - def get_corr(self): - return self._corr[:] - - def append_message(self, message): - """ - :param message: text or file transfer message - """ - self._corr.append(message) - if message.get_type() <= 1: - self._unsaved_messages += 1 - - def get_last_message_text(self): - messages = list(filter(lambda x: x.get_type() <= 1 and x.get_owner() != MESSAGE_OWNER['FRIEND'], self._corr)) - if messages: - return messages[-1].get_data()[0] - else: - return '' - - def get_unsent_messages(self): - """ - :return list of unsent messages - """ - messages = filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) - return list(messages) - - def get_unsent_messages_for_saving(self): - """ - :return list of unsent messages for saving - """ - messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) - return list(map(lambda x: x.get_data(), messages)) - - def delete_message(self, time): - elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0] - tmp = list(filter(lambda x: x.get_type() <= 1, self._corr)) - if elem in tmp[-self._unsaved_messages:]: - self._unsaved_messages -= 1 - self._corr.remove(elem) - - def mark_as_sent(self): - try: - message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] - message.mark_as_sent() - except Exception as ex: - util.log('Mark as sent ex: ' + str(ex)) - - def clear_corr(self, save_unsent=False): - """ - Clear messages list - """ - if hasattr(self, '_message_getter'): - del self._message_getter - # don't delete data about active file transfer - if not save_unsent: - self._corr = list(filter(lambda x: x.get_type() in (2, 3) and - x.get_status() in ft.ACTIVE_FILE_TRANSFERS, self._corr)) - self._unsaved_messages = 0 - else: - self._corr = list(filter(lambda x: (x.get_type() in (2, 3) and x.get_status() in ft.ACTIVE_FILE_TRANSFERS) - or (x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT']), - self._corr)) - self._unsaved_messages = len(self.get_unsent_messages()) - - def get_curr_text(self): - return self._curr_text - - def set_curr_text(self, value): - self._curr_text = value - - curr_text = property(get_curr_text, set_curr_text) - - # ----------------------------------------------------------------------------------------------------------------- - # File transfers support - # ----------------------------------------------------------------------------------------------------------------- - - def update_transfer_data(self, file_number, status, inline=None): - """ - Update status of active transfer and load inline if needed - """ - try: - tr = list(filter(lambda x: x.get_type() == MESSAGE_TYPE['FILE_TRANSFER'] and x.is_active(file_number), - self._corr))[0] - tr.set_status(status) - i = self._corr.index(tr) - if inline: # inline was loaded - self._corr.insert(i, inline) - return i - len(self._corr) - except: - pass - - def get_unsent_files(self): - messages = filter(lambda x: type(x) is UnsentFile, self._corr) - return messages - - def clear_unsent_files(self): - self._corr = list(filter(lambda x: type(x) is not UnsentFile, self._corr)) - - def delete_one_unsent_file(self, time): - self._corr = list(filter(lambda x: not (type(x) is UnsentFile and x.get_data()[2] == time), self._corr)) - - # ----------------------------------------------------------------------------------------------------------------- - # Alias support - # ----------------------------------------------------------------------------------------------------------------- - - def set_name(self, value): - """ - Set new name or ignore if alias exists - :param value: new name - """ - if not self._alias: - super(Friend, self).set_name(value) - - def set_alias(self, alias): - self._alias = bool(alias) - - # ----------------------------------------------------------------------------------------------------------------- - # Visibility in friends' list - # ----------------------------------------------------------------------------------------------------------------- - - def get_visibility(self): - return self._visible - - def set_visibility(self, value): - self._visible = value - - visibility = property(get_visibility, set_visibility) - - # ----------------------------------------------------------------------------------------------------------------- - # Unread messages from friend - # ----------------------------------------------------------------------------------------------------------------- - - def get_actions(self): - return self._new_actions - - def set_actions(self, value): - self._new_actions = value - self._widget.connection_status.update(self.status, value) - - actions = property(get_actions, set_actions) # unread messages, incoming files, av calls - - def get_messages(self): - return self._new_messages - - def inc_messages(self): - self._new_messages += 1 - self._new_actions = True - self._widget.connection_status.update(self.status, True) - self._widget.messages.update(self._new_messages) - - def reset_messages(self): - self._new_actions = False - self._new_messages = 0 - self._widget.messages.update(self._new_messages) - self._widget.connection_status.update(self.status, False) - - messages = property(get_messages) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend's number (can be used in toxcore) - # ----------------------------------------------------------------------------------------------------------------- - - def get_number(self): - return self._number - - def set_number(self, value): - self._number = value - - number = property(get_number, set_number) diff --git a/toxygen/groups/__init__.py b/toxygen/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/groups/group_ban.py b/toxygen/groups/group_ban.py new file mode 100644 index 0000000..2b17a25 --- /dev/null +++ b/toxygen/groups/group_ban.py @@ -0,0 +1,23 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class GroupBan: + + def __init__(self, ban_id, ban_target, ban_time): + self._ban_id = ban_id + self._ban_target = ban_target + self._ban_time = ban_time + + def get_ban_id(self): + return self._ban_id + + ban_id = property(get_ban_id) + + def get_ban_target(self): + return self._ban_target + + ban_target = property(get_ban_target) + + def get_ban_time(self): + return self._ban_time + + ban_time = property(get_ban_time) diff --git a/toxygen/groups/group_invite.py b/toxygen/groups/group_invite.py new file mode 100644 index 0000000..2332933 --- /dev/null +++ b/toxygen/groups/group_invite.py @@ -0,0 +1,23 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class GroupInvite: + + def __init__(self, friend_public_key, chat_name, invite_data): + self._friend_public_key = friend_public_key + self._chat_name = chat_name + self._invite_data = invite_data[:] + + def get_friend_public_key(self): + return self._friend_public_key + + friend_public_key = property(get_friend_public_key) + + def get_chat_name(self): + return self._chat_name + + chat_name = property(get_chat_name) + + def get_invite_data(self): + return self._invite_data[:] + + invite_data = property(get_invite_data) diff --git a/toxygen/groups/group_peer.py b/toxygen/groups/group_peer.py new file mode 100644 index 0000000..a96c751 --- /dev/null +++ b/toxygen/groups/group_peer.py @@ -0,0 +1,73 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class GroupChatPeer: + """ + Represents peer in group chat. + """ + + def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False, status_message=None): + self._peer_id = peer_id + self._name = name + self._status = status + self._status_message = status_message + self._role = role + self._public_key = public_key + self._is_current_user = is_current_user + self._is_muted = is_muted + self._kind = 'grouppeer' + + # Readonly properties + + def get_id(self): + return self._peer_id + + id = property(get_id) + + def get_public_key(self): + return self._public_key + + public_key = property(get_public_key) + + def get_is_current_user(self): + return self._is_current_user + + is_current_user = property(get_is_current_user) + + def get_status_message(self): + return self._status_message + + status_message = property(get_status_message) + + # Read-write properties + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = property(get_name, set_name) + + def get_status(self): + return self._status + + def set_status(self, status): + self._status = status + + status = property(get_status, set_status) + + def get_role(self): + return self._role + + def set_role(self, role): + self._role = role + + role = property(get_role, set_role) + + def get_is_muted(self): + return self._is_muted + + def set_is_muted(self, is_muted): + self._is_muted = is_muted + + is_muted = property(get_is_muted, set_is_muted) diff --git a/toxygen/groups/groups_service.py b/toxygen/groups/groups_service.py new file mode 100644 index 0000000..0e52d2a --- /dev/null +++ b/toxygen/groups/groups_service.py @@ -0,0 +1,291 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import logging + +import common.tox_save as tox_save +import utils.ui as util_ui +from groups.peers_list import PeersListGenerator +from groups.group_invite import GroupInvite +import toxygen_wrapper.toxcore_enums_and_consts as constants +from toxygen_wrapper.toxcore_enums_and_consts import * +from toxygen_wrapper.tox import UINT32_MAX + +global LOG +LOG = logging.getLogger('app.'+'gs') + +class GroupsService(tox_save.ToxSave): + + def __init__(self, tox, contacts_manager, contacts_provider, main_screen, widgets_factory_provider): + super().__init__(tox) + self._contacts_manager = contacts_manager + self._contacts_provider = contacts_provider + self._main_screen = main_screen + self._peers_list_widget = main_screen.peers_list + self._widgets_factory_provider = widgets_factory_provider + self._group_invites = [] + self._screen = None + # maybe just use self + self._tox = tox + + def set_tox(self, tox) -> None: + super().set_tox(tox) + for group in self._get_all_groups(): + group.set_tox(tox) + + # Groups creation + + def create_new_gc(self, name, privacy_state, nick, status) -> None: + try: + group_number = self._tox.group_new(privacy_state, name, nick, status) + except Exception as e: + LOG.error(f"create_new_gc {e}") + return + if group_number == -1: + return + + self._add_new_group_by_number(group_number) + group = self._get_group_by_number(group_number) + group.status = constants.TOX_USER_STATUS['NONE'] + self._contacts_manager.update_filtration() + + def join_gc_by_id(self, chat_id, password, nick, status) -> None: + try: + group_number = self._tox.group_join(chat_id, password, nick, status) + assert type(group_number) == int, group_number + assert group_number < UINT32_MAX, group_number + except Exception as e: + # gui + title = f"join_gc_by_id {chat_id}" + util_ui.message_box(title +'\n' +str(e), title) + LOG.error(f"_join_gc_via_id {e}") + return + LOG.debug(f"_join_gc_via_id {group_number}") + self._add_new_group_by_number(group_number) + group = self._get_group_by_number(group_number) + try: + assert group and hasattr(group, 'status') + except Exception as e: + # gui + title = f"join_gc_by_id {chat_id}" + util_ui.message_box(title +'\n' +str(e), title) + LOG.error(f"_join_gc_via_id {e}") + return + group.status = constants.TOX_USER_STATUS['NONE'] + self._contacts_manager.update_filtration() + + # Groups reconnect and leaving + + def leave_group(self, group_number) -> None: + if type(group_number) == int: + self._tox.group_leave(group_number) + self._contacts_manager.delete_group(group_number) + + def disconnect_from_group(self, group_number) -> None: + self._tox.group_disconnect(group_number) + group = self._get_group_by_number(group_number) + group.status = None + self._clear_peers_list(group) + + def reconnect_to_group(self, group_number) -> None: + self._tox.group_reconnect(group_number) + group = self._get_group_by_number(group_number) + group.status = constants.TOX_USER_STATUS['NONE'] + self._clear_peers_list(group) + + # Group invites + + def invite_friend(self, friend_number, group_number) -> None: + if self._tox.friend_get_connection_status(friend_number) == TOX_CONNECTION['NONE']: + title = f"Error in group_invite_friend {friend_number}" + e = f"Friend not connected friend_number={friend_number}" + util_ui.message_box(title +'\n' +str(e), title) + return + + try: + self._tox.group_invite_friend(group_number, friend_number) + except Exception as e: + title = f"Error in group_invite_friend {group_number} {friend_number}" + util_ui.message_box(title +'\n' +str(e), title) + + def process_group_invite(self, friend_number, group_name, invite_data) -> None: + friend = self._get_friend_by_number(friend_number) + # binary {invite_data} + LOG.debug(f"process_group_invite {friend_number} {group_name}") + invite = GroupInvite(friend.tox_id, group_name, invite_data) + self._group_invites.append(invite) + self._update_invites_button_state() + + def accept_group_invite(self, invite, name, status, password) -> None: + pk = invite.friend_public_key + friend = self._get_friend_by_public_key(pk) + LOG.debug(f"accept_group_invite {name}") + self._join_gc_via_invite(invite.invite_data, friend.number, name, status, password) + self._delete_group_invite(invite) + self._update_invites_button_state() + + def decline_group_invite(self, invite) -> None: + self._delete_group_invite(invite) + self._main_screen.update_gc_invites_button_state() + + def get_group_invites(self): + return self._group_invites[:] + + group_invites = property(get_group_invites) + + def get_group_invites_count(self): + return len(self._group_invites) + + group_invites_count = property(get_group_invites_count) + + # Group info methods + + def update_group_info(self, group): + group.name = self._tox.group_get_name(group.number) + group.status_message = self._tox.group_get_topic(group.number) + + def set_group_topic(self, group) -> None: + if not group.is_self_moderator_or_founder(): + return + text = util_ui.tr('New topic for group "{}":'.format(group.name)) + title = util_ui.tr('Set group topic') + topic, ok = util_ui.text_dialog(text, title, group.status_message) + if not ok or not topic: + return + self._tox.group_set_topic(group.number, topic) + group.status_message = topic + + def show_group_management_screen(self, group) -> None: + widgets_factory = self._get_widgets_factory() + self._screen = widgets_factory.create_group_management_screen(group) + self._screen.show() + + def show_group_settings_screen(self, group) -> None: + widgets_factory = self._get_widgets_factory() + self._screen = widgets_factory.create_group_settings_screen(group) + self._screen.show() + + def set_group_password(self, group, password) -> None: + if group.password == password: + return + self._tox.group_founder_set_password(group.number, password) + group.password = password + + def set_group_peers_limit(self, group, peers_limit) -> None: + if group.peers_limit == peers_limit: + return + self._tox.group_founder_set_peer_limit(group.number, peers_limit) + group.peers_limit = peers_limit + + def set_group_privacy_state(self, group, privacy_state) -> None: + is_private = privacy_state == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE'] + if group.is_private == is_private: + return + self._tox.group_founder_set_privacy_state(group.number, privacy_state) + group.is_private = is_private + + # Peers list + + def generate_peers_list(self) -> None: + if not self._contacts_manager.is_active_a_group(): + return + group = self._contacts_manager.get_curr_contact() + PeersListGenerator().generate(group.peers, self, self._peers_list_widget, group.tox_id) + + def peer_selected(self, chat_id, peer_id) -> None: + widgets_factory = self._get_widgets_factory() + group = self._get_group_by_public_key(chat_id) + self_peer = group.get_self_peer() + if self_peer.id != peer_id: + self._screen = widgets_factory.create_peer_screen_window(group, peer_id) + else: + self._screen = widgets_factory.create_self_peer_screen_window(group) + self._screen.show() + + # Peers actions + + def set_new_peer_role(self, group, peer, role) -> None: + self._tox.group_mod_set_role(group.number, peer.id, role) + peer.role = role + self.generate_peers_list() + + def toggle_ignore_peer(self, group, peer, ignore) -> None: + self._tox.group_toggle_ignore(group.number, peer.id, ignore) + peer.is_muted = ignore + + def set_self_info(self, group, name, status) -> None: + self._tox.group_self_set_name(group.number, name) + self._tox.group_self_set_status(group.number, status) + self_peer = group.get_self_peer() + self_peer.name = name + self_peer.status = status + self.generate_peers_list() + + # Bans support + + def show_bans_list(self, group) -> None: + return + widgets_factory = self._get_widgets_factory() + self._screen = widgets_factory.create_groups_bans_screen(group) + self._screen.show() + + def ban_peer(self, group, peer_id, ban_type) -> None: + self._tox.group_mod_ban_peer(group.number, peer_id, ban_type) + + def kick_peer(self, group, peer_id) -> None: + self._tox.group_mod_remove_peer(group.number, peer_id) + + def cancel_ban(self, group_number, ban_id) -> None: + self._tox.group_mod_remove_ban(group_number, ban_id) + + # Private methods + + def _add_new_group_by_number(self, group_number) -> None: + LOG.debug(f"_add_new_group_by_number group_number={group_number}") + self._contacts_manager.add_group(group_number) + + def _get_group_by_number(self, group_number): + return self._contacts_provider.get_group_by_number(group_number) + + def _get_group_by_public_key(self, public_key): + return self._contacts_provider.get_group_by_public_key(public_key) + + def _get_all_groups(self): + return self._contacts_provider.get_all_groups() + + def _get_friend_by_number(self, friend_number:int): + return self._contacts_provider.get_friend_by_number(friend_number) + + def _get_friend_by_public_key(self, public_key): + return self._contacts_provider.get_friend_by_public_key(public_key) + + def _clear_peers_list(self, group) -> None: + group.remove_all_peers_except_self() + self.generate_peers_list() + + def _delete_group_invite(self, invite) -> None: + if invite in self._group_invites: + self._group_invites.remove(invite) + + # status should be dropped + def _join_gc_via_invite(self, invite_data, friend_number, nick, status='', password='') -> None: + LOG.debug(f"_join_gc_via_invite friend_number={friend_number} nick={nick} datalen={len(invite_data)}") + if nick is None: + nick = '' + if invite_data is None: + invite_data = b'' + try: + # status should be dropped + group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, password=password) + except Exception as e: + LOG.error(f"_join_gc_via_invite ERROR {e}") + return + try: + self._add_new_group_by_number(group_number) + except Exception as e: + LOG.error(f"_join_gc_via_invite group_number={group_number} {e}") + return + + def _update_invites_button_state(self) -> None: + self._main_screen.update_gc_invites_button_state() + + def _get_widgets_factory(self) -> None: + return self._widgets_factory_provider.get_item() diff --git a/toxygen/groups/peers_list.py b/toxygen/groups/peers_list.py new file mode 100644 index 0000000..97641d9 --- /dev/null +++ b/toxygen/groups/peers_list.py @@ -0,0 +1,101 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from ui.group_peers_list import PeerItem, PeerTypeItem +from toxygen_wrapper.toxcore_enums_and_consts import * +from ui.widgets import * + +# Builder + + +class PeerListBuilder: + + def __init__(self): + self._peers = {} + self._titles = {} + self._index = 0 + self._handler = None + + def with_click_handler(self, handler): + self._handler = handler + + return self + + def with_title(self, title): + self._titles[self._index] = title + self._index += 1 + + return self + + def with_peers(self, peers): + for peer in peers: + self._add_peer(peer) + + return self + + def build(self, list_widget): + list_widget.clear() + + for i in range(self._index): + if i in self._peers: + peer = self._peers[i] + self._add_peer_item(peer, list_widget) + else: + title = self._titles[i] + self._add_peer_type_item(title, list_widget) + + def _add_peer_item(self, peer, parent): + item = PeerItem(peer, self._handler, parent.width(), parent) + self._add_item(parent, item) + + def _add_peer_type_item(self, text, parent): + item = PeerTypeItem(text, parent.width(), parent) + self._add_item(parent, item) + + @staticmethod + def _add_item(parent, item): + elem = QtWidgets.QListWidgetItem(parent) + elem.setSizeHint(QtCore.QSize(parent.width(), item.height())) + parent.addItem(elem) + parent.setItemWidget(elem, item) + + def _add_peer(self, peer): + self._peers[self._index] = peer + self._index += 1 + +# Generators + + +class PeersListGenerator: + + @staticmethod + def generate(peers_list, groups_service, list_widget, chat_id): + admin_title = util_ui.tr('Administrator') + moderators_title = util_ui.tr('Moderators') + users_title = util_ui.tr('Users') + observers_title = util_ui.tr('Observers') + + admins = list(filter(lambda p: p.role == TOX_GROUP_ROLE['FOUNDER'], peers_list)) + moderators = list(filter(lambda p: p.role == TOX_GROUP_ROLE['MODERATOR'], peers_list)) + users = list(filter(lambda p: p.role == TOX_GROUP_ROLE['USER'], peers_list)) + observers = list(filter(lambda p: p.role == TOX_GROUP_ROLE['OBSERVER'], peers_list)) + + builder = (PeerListBuilder() + .with_click_handler(lambda peer_id: groups_service.peer_selected(chat_id, peer_id))) + if len(admins): + (builder + .with_title(admin_title) + .with_peers(admins)) + if len(moderators): + (builder + .with_title(moderators_title) + .with_peers(moderators)) + if len(users): + (builder + .with_title(users_title) + .with_peers(users)) + if len(observers): + (builder + .with_title(observers_title) + .with_peers(observers)) + + builder.build(list_widget) diff --git a/toxygen/history.py b/toxygen/history.py deleted file mode 100644 index ad18ee5..0000000 --- a/toxygen/history.py +++ /dev/null @@ -1,182 +0,0 @@ -# coding=utf-8 -from sqlite3 import connect -import settings -from os import chdir -import os.path -from toxencryptsave import ToxEncryptSave - - -PAGE_SIZE = 42 - -MESSAGE_OWNER = { - 'ME': 0, - 'FRIEND': 1, - 'NOT_SENT': 2 -} - - -class History: - - def __init__(self, name): - self._name = name - chdir(settings.ProfileHelper.get_path()) - path = settings.ProfileHelper.get_path() + self._name + '.hstr' - if os.path.exists(path): - decr = ToxEncryptSave.get_instance() - try: - with open(path, 'rb') as fin: - data = fin.read() - if decr.is_data_encrypted(data): - data = decr.pass_decrypt(data) - with open(path, 'wb') as fout: - fout.write(data) - except: - os.remove(path) - db = connect(name + '.hstr') - cursor = db.cursor() - cursor.execute('CREATE TABLE IF NOT EXISTS friends(' - ' tox_id TEXT PRIMARY KEY' - ')') - db.close() - - def save(self): - encr = ToxEncryptSave.get_instance() - if encr.has_password(): - path = settings.ProfileHelper.get_path() + self._name + '.hstr' - with open(path, 'rb') as fin: - data = fin.read() - data = encr.pass_encrypt(bytes(data)) - with open(path, 'wb') as fout: - fout.write(data) - - def export(self, directory): - path = settings.ProfileHelper.get_path() + self._name + '.hstr' - new_path = directory + self._name + '.hstr' - with open(path, 'rb') as fin: - data = fin.read() - encr = ToxEncryptSave.get_instance() - if encr.has_password(): - data = encr.pass_encrypt(data) - with open(new_path, 'wb') as fout: - fout.write(data) - - def add_friend_to_db(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - try: - cursor = db.cursor() - cursor.execute('INSERT INTO friends VALUES (?);', (tox_id, )) - cursor.execute('CREATE TABLE id' + tox_id + '(' - ' id INTEGER PRIMARY KEY,' - ' message TEXT,' - ' owner INTEGER,' - ' unix_time REAL,' - ' message_type INTEGER' - ')') - db.commit() - except: - db.rollback() - raise - finally: - db.close() - - def delete_friend_from_db(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - try: - cursor = db.cursor() - cursor.execute('DELETE FROM friends WHERE tox_id=?;', (tox_id, )) - cursor.execute('DROP TABLE id' + tox_id + ';') - db.commit() - except: - db.rollback() - raise - finally: - db.close() - - def friend_exists_in_db(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - cursor = db.cursor() - cursor.execute('SELECT 0 FROM friends WHERE tox_id=?', (tox_id, )) - result = cursor.fetchone() - db.close() - return result is not None - - def save_messages_to_db(self, tox_id, messages_iter): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - try: - cursor = db.cursor() - cursor.executemany('INSERT INTO id' + tox_id + '(message, owner, unix_time, message_type) ' - 'VALUES (?, ?, ?, ?);', messages_iter) - db.commit() - except: - db.rollback() - raise - finally: - db.close() - - def update_messages(self, tox_id, unsent_time): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - try: - cursor = db.cursor() - cursor.execute('UPDATE id' + tox_id + ' SET owner = 0 ' - 'WHERE unix_time < ' + str(unsent_time) + ' AND owner = 2;') - db.commit() - except: - db.rollback() - raise - finally: - db.close() - pass - - def delete_message(self, tox_id, time): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - try: - cursor = db.cursor() - cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time = ' + str(time) + ';') - db.commit() - except: - db.rollback() - raise - finally: - db.close() - - def delete_messages(self, tox_id): - chdir(settings.ProfileHelper.get_path()) - db = connect(self._name + '.hstr') - try: - cursor = db.cursor() - cursor.execute('DELETE FROM id' + tox_id + ';') - db.commit() - except: - db.rollback() - raise - finally: - db.close() - - def messages_getter(self, tox_id): - return History.MessageGetter(self._name, tox_id) - - class MessageGetter: - def __init__(self, name, tox_id): - chdir(settings.ProfileHelper.get_path()) - self._db = connect(name + '.hstr') - self._cursor = self._db.cursor() - self._cursor.execute('SELECT message, owner, unix_time, message_type FROM id' + tox_id + - ' ORDER BY unix_time DESC;') - - def get_one(self): - return self._cursor.fetchone() - - def get_all(self): - return self._cursor.fetchall() - - def get(self, count): - return self._cursor.fetchmany(count) - - def __del__(self): - self._db.close() diff --git a/toxygen/history/__init__.py b/toxygen/history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/history/database.py b/toxygen/history/database.py new file mode 100644 index 0000000..7d8dd35 --- /dev/null +++ b/toxygen/history/database.py @@ -0,0 +1,227 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from sqlite3 import connect +import os.path +import utils.util as util + +global LOG +import logging +LOG = logging.getLogger('h.database') + +TIMEOUT = 11 +SAVE_MESSAGES = 500 +MESSAGE_AUTHOR = { + 'ME': 0, + 'FRIEND': 1, + 'NOT_SENT': 2, + 'GC_PEER': 3 +} +CONTACT_TYPE = { + 'FRIEND': 0, + 'GC_PEER': 1, + 'GC_PEER_PRIVATE': 2 +} + + +class Database: + + def __init__(self, path, toxes): + self._path = path + self._toxes = toxes + self._name = os.path.basename(path) + + def open(self): + path = self._path + toxes = self._toxes + if not os.path.exists(path): + LOG.warn('Db not found: ' +path) + return + try: + with open(path, 'rb') as fin: + data = fin.read() + except Exception as ex: + LOG.error('Db reading error: ' +path +' ' +str(ex)) + raise + try: + if toxes.is_data_encrypted(data): + data = toxes.pass_decrypt(data) + with open(path, 'wb') as fout: + fout.write(data) + except Exception as ex: + LOG.error('Db writing error: ' +path +' ' + str(ex)) + os.remove(path) + LOG.info('Db opened: ' +path) + + # Public methods + + def save(self): + if self._toxes.has_password(): + with open(self._path, 'rb') as fin: + data = fin.read() + data = self._toxes.pass_encrypt(bytes(data)) + with open(self._path, 'wb') as fout: + fout.write(data) + + def export(self, directory): + new_path = util.join_path(directory, self._name) + with open(self._path, 'rb') as fin: + data = fin.read() + if self._toxes.has_password(): + data = self._toxes.pass_encrypt(data) + with open(new_path, 'wb') as fout: + fout.write(data) + LOG.info('Db exported: ' +new_path) + + def add_friend_to_db(self, tox_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('CREATE TABLE IF NOT EXISTS id' + tox_id + '(' + ' id INTEGER PRIMARY KEY,' + ' author_name TEXT,' + ' message TEXT,' + ' author_type INTEGER,' + ' unix_time REAL,' + ' message_type INTEGER' + ')') + db.commit() + return True + except Exception as e: + LOG.error("dd_friend_to_db " +self._name +f" Database exception! {e}") + db.rollback() + return False + finally: + db.close() + LOG.debug(f"add_friend_to_db {tox_id}") + + def delete_friend_from_db(self, tox_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('DROP TABLE id' + tox_id + ';') + db.commit() + return True + except Exception as e: + LOG.error("delete_friend_from_db " +self._name +f" Database exception! {e}") + db.rollback() + return False + finally: + db.close() + LOG.debug(f"delete_friend_from_db {tox_id}") + + def save_messages_to_db(self, tox_id, messages_iter): + db = self._connect() + try: + cursor = db.cursor() + cursor.executemany('INSERT INTO id' + tox_id + + '(message, author_name, author_type, unix_time, message_type) ' + + 'VALUES (?, ?, ?, ?, ?);', messages_iter) + db.commit() + return True + except Exception as e: + LOG.error("save_messages_to_db" +self._name +f" Database exception! {e}") + db.rollback() + return False + finally: + db.close() + LOG.debug(f"save_messages_to_db {tox_id}") + + def update_messages(self, tox_id, message_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('UPDATE id' + tox_id + ' SET author = 0 ' + 'WHERE id = ' + str(message_id) + ' AND author = 2;') + db.commit() + return True + except Exception as e: + LOG.error("update_messages" +self._name +f" Database exception! {e}") + db.rollback() + return False + finally: + db.close() + LOG.debug(f"update_messages {tox_id}") + + def delete_message(self, tox_id, unique_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('DELETE FROM id' + tox_id + ' WHERE id = ' + str(unique_id) + ';') + db.commit() + return True + except Exception as e: + LOG.error("delete_message" +self._name +f" Database exception! {e}") + db.rollback() + return False + finally: + db.close() + LOG.debug(f"delete_message {tox_id}") + + def delete_messages(self, tox_id): + db = self._connect() + try: + cursor = db.cursor() + cursor.execute('DELETE FROM id' + tox_id + ';') + db.commit() + return True + except Exception as e: + LOG.error("delete_messages" +self._name +f" Database exception! {e}") + db.rollback() + return False + finally: + db.close() + LOG.debug(f"delete_messages {tox_id}") + + def messages_getter(self, tox_id): + self.add_friend_to_db(tox_id) + + return Database.MessageGetter(self._path, tox_id) + + # Messages loading + + class MessageGetter: + + def __init__(self, path, tox_id): + self._count = 0 + self._path = path + self._tox_id = tox_id + self._db = self._cursor = None + + def get_one(self): + return self.get(1) + + def get_all(self): + self._connect() + data = self._cursor.fetchall() + self._disconnect() + self._count = len(data) + return data + + def get(self, count): + self._connect() + self.skip() + data = self._cursor.fetchmany(count) + self._disconnect() + self._count += len(data) + return data + + def skip(self): + if self._count: + self._cursor.fetchmany(self._count) + + def delete_one(self): + if self._count: + self._count -= 1 + + def _connect(self): + self._db = connect(self._path, timeout=TIMEOUT) + self._cursor = self._db.cursor() + self._cursor.execute('SELECT message, author_type, author_name, unix_time, message_type, id FROM id' + + self._tox_id + ' ORDER BY unix_time DESC;') + + def _disconnect(self): + self._db.close() + + # Private methods + + def _connect(self): + return connect(self._path, timeout=TIMEOUT) diff --git a/toxygen/history/history.py b/toxygen/history/history.py new file mode 100644 index 0000000..971fa29 --- /dev/null +++ b/toxygen/history/history.py @@ -0,0 +1,141 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from history.history_logs_generators import * + +global LOG +import logging +LOG = logging.getLogger('app.db') + +class History: + + def __init__(self, contact_provider, db, settings, main_screen, messages_items_factory): + self._contact_provider = contact_provider + self._db = db + self._settings = settings + self._messages = main_screen.messages + self._messages_items_factory = messages_items_factory + self._is_loading = False + self._contacts_manager = None + + def __del__(self): + del self._db + + def set_contacts_manager(self, contacts_manager): + self._contacts_manager = contacts_manager + + # History support + + def save_history(self): + """ + Save history to db + """ + # me a mistake? was _db not _history + if self._settings['save_history']: + for friend in self._contact_provider.get_all_friends(): + self._db.add_friend_to_db(friend.tox_id) + if not self._settings['save_unsent_only']: + messages = friend.get_corr_for_saving() + else: + messages = friend.get_unsent_messages_for_saving() + self._db.delete_messages(friend.tox_id) + messages = map(lambda m: (m.text, m.author.name, m.author.type, m.time, m.type), messages) + self._db.save_messages_to_db(friend.tox_id, messages) + + self._db.save() + + def clear_history(self, friend, save_unsent=False): + """ + Clear chat history + """ + friend.clear_corr(save_unsent) + self._db.delete_friend_from_db(friend.tox_id) + + def export_history(self, contact, as_text=True): + extension = 'txt' if as_text else 'html' + file_name, _ = util_ui.save_file_dialog(util_ui.tr('Choose file name'), extension) + + if not file_name: + return + + if not file_name.endswith('.' + extension): + file_name += '.' + extension + + history = self.generate_history(contact, as_text) + assert history + with open(file_name, 'wt') as fl: + fl.write(history) + LOG.info(f"wrote history to {file_name}") + + def delete_message(self, message): + contact = self._contacts_manager.get_curr_contact() + if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): + if message.is_saved(): + self._db.delete_message(contact.tox_id, message.id) + contact.delete_message(message.message_id) + + def load_history(self, friend): + """ + Tries to load next part of messages + """ + if self._is_loading: + return + self._is_loading = True + friend.load_corr(False) + messages = friend.get_corr() + if not messages: + self._is_loading = False + return + messages.reverse() + messages = messages[self._messages.count():self._messages.count() + PAGE_SIZE] + for message in messages: + message_type = message.get_type() + if message_type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): # text message + self._create_message_item(message) + elif message_type == MESSAGE_TYPE['FILE_TRANSFER']: # file transfer + if message.state == FILE_TRANSFER_STATE['UNSENT']: + self._create_unsent_file_item(message) + else: + self._create_file_transfer_item(message) + elif message_type == MESSAGE_TYPE['INLINE']: # inline image + self._create_inline_item(message) + else: # info message + self._create_message_item(message) + self._is_loading = False + + def get_message_getter(self, friend_public_key): + self._db.add_friend_to_db(friend_public_key) + + return self._db.messages_getter(friend_public_key) + + def delete_history(self, friend): + self._db.delete_friend_from_db(friend.tox_id) + + def add_friend_to_db(self, tox_id): + self._db.add_friend_to_db(tox_id) + + @staticmethod + def generate_history(contact, as_text=True, _range=None): + if _range is None: + contact.load_all_corr() + corr = contact.get_corr() + elif _range[1] + 1: + corr = contact.get_corr()[_range[0]:_range[1] + 1] + else: + corr = contact.get_corr()[_range[0]:] + + generator = TextHistoryGenerator(corr, contact.name) if as_text else HtmlHistoryGenerator(corr, contact.name) + + return generator.generate() + + # Items creation + + def _create_message_item(self, message): + return self._messages_items_factory.create_message_item(message, False) + + def _create_unsent_file_item(self, message): + return self._messages_items_factory.create_unsent_file_item(message, False) + + def _create_file_transfer_item(self, message): + return self._messages_items_factory.create_file_transfer_item(message, False) + + def _create_inline_item(self, message): + return self._messages_items_factory.create_inline_item(message, False) diff --git a/toxygen/history/history_logs_generators.py b/toxygen/history/history_logs_generators.py new file mode 100644 index 0000000..91c0a28 --- /dev/null +++ b/toxygen/history/history_logs_generators.py @@ -0,0 +1,50 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import utils.util as util +from messenger.messages import * + + +class HistoryLogsGenerator: + + def __init__(self, history, contact_name): + self._history = history + self._contact_name = contact_name + + def generate(self): + return str() + + @staticmethod + def _get_message_time(message): + return util.convert_time(message.time) if message.author.type != MESSAGE_AUTHOR['NOT_SENT'] else 'Unsent' + + +class HtmlHistoryGenerator(HistoryLogsGenerator): + + def __init__(self, history, contact_name): + super().__init__(history, contact_name) + + def generate(self): + arr = [] + for message in self._history: + if type(message) is TextMessage: + x = '[{}] {}: {}
' + arr.append(x.format(self._get_message_time(message), message.author.name, message.text)) + s = '
'.join(arr) + html = '{}{}' + + return html.format(self._contact_name, s) + + +class TextHistoryGenerator(HistoryLogsGenerator): + + def __init__(self, history, contact_name): + super().__init__(history, contact_name) + + def generate(self): + arr = [self._contact_name] + for message in self._history: + if type(message) is TextMessage: + x = '[{}] {}: {}\n' + arr.append(x.format(self._get_message_time(message), message.author.name, message.text)) + + return '\n'.join(arr) diff --git a/toxygen/images/accept.png b/toxygen/images/accept.png old mode 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/audio_message.png b/toxygen/images/audio_message.png deleted file mode 100755 index 22ba2a0..0000000 Binary files a/toxygen/images/audio_message.png and /dev/null differ diff --git a/toxygen/images/avatar.png b/toxygen/images/avatar.png old mode 100755 new mode 100644 index 91d1200..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 new file mode 100644 index 0000000..ba153e9 Binary files /dev/null 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 new file mode 100644 index 0000000..9e4f830 Binary files /dev/null and b/toxygen/images/finish_call_video.png differ diff --git a/toxygen/images/group.png b/toxygen/images/group.png new file mode 100644 index 0000000..3ea6469 Binary files /dev/null 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 new file mode 100644 index 0000000..2301877 Binary files /dev/null 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/images/video_message.png b/toxygen/images/video_message.png deleted file mode 100755 index 37603ce..0000000 Binary files a/toxygen/images/video_message.png and /dev/null differ diff --git a/toxygen/images/videocall.png b/toxygen/images/videocall.png deleted file mode 100755 index ef9fa86..0000000 Binary files a/toxygen/images/videocall.png and /dev/null differ diff --git a/toxygen/libtox.py b/toxygen/libtox.py deleted file mode 100644 index edf2a12..0000000 --- a/toxygen/libtox.py +++ /dev/null @@ -1,50 +0,0 @@ -from platform import system -from ctypes import CDLL -import util - - -class LibToxCore: - - def __init__(self): - if system() == 'Linux': - # libtoxcore and libsodium must be installed in your os - self._libtoxcore = CDLL('libtoxcore.so') - elif system() == 'Windows': - self._libtoxcore = CDLL(util.curr_directory() + '/libs/libtox.dll') - else: - raise OSError('Unknown system.') - - def __getattr__(self, item): - return self._libtoxcore.__getattr__(item) - - -class LibToxAV: - - def __init__(self): - if system() == 'Linux': - # that /usr/lib/libtoxav.so must exists - self._libtoxav = CDLL('libtoxav.so') - elif system() == 'Windows': - # on Windows av api is in libtox.dll - self._libtoxav = CDLL(util.curr_directory() + '/libs/libtox.dll') - else: - raise OSError('Unknown system.') - - def __getattr__(self, item): - return self._libtoxav.__getattr__(item) - - -class LibToxEncryptSave: - - def __init__(self): - if system() == 'Linux': - # /usr/lib/libtoxencryptsave.so must exists - self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.so') - elif system() == 'Windows': - # on Windows profile encryption api is in libtox.dll - self._lib_tox_encrypt_save = CDLL(util.curr_directory() + '/libs/libtox.dll') - else: - raise OSError('Unknown system.') - - def __getattr__(self, item): - return self._lib_tox_encrypt_save.__getattr__(item) diff --git a/toxygen/list_items.py b/toxygen/list_items.py deleted file mode 100644 index 78e12c3..0000000 --- a/toxygen/list_items.py +++ /dev/null @@ -1,500 +0,0 @@ -from toxcore_enums_and_consts import * -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -import profile -from file_transfers import TOX_FILE_TRANSFER_STATE, PAUSED_FILE_TRANSFERS, DO_NOT_SHOW_ACCEPT_BUTTON, ACTIVE_FILE_TRANSFERS, SHOW_PROGRESS_BAR -from util import curr_directory, convert_time, curr_time -from widgets import DataLabel, create_menu -import html as h -import smileys -import settings - - -class MessageEdit(QtGui.QTextBrowser): - - def __init__(self, text, width, message_type, parent=None): - super(MessageEdit, self).__init__(parent) - self.urls = {} - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere) - self.document().setTextWidth(width) - self.setOpenExternalLinks(True) - self.setAcceptRichText(True) - self.setOpenLinks(False) - self.setSearchPaths([smileys.SmileyLoader.get_instance().get_smileys_path()]) - self.document().setDefaultStyleSheet('a { color: #306EFF; }') - text = self.decoratedText(text) - if message_type != TOX_MESSAGE_TYPE['NORMAL']: - self.setHtml('

' + text + '

') - else: - self.setHtml(text) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPixelSize(settings.Settings.get_instance()['message_font_size']) - font.setBold(False) - self.setFont(font) - self.resize(width, self.document().size().height()) - self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) - self.anchorClicked.connect(self.on_anchor_clicked) - - def contextMenuEvent(self, event): - menu = create_menu(self.createStandardContextMenu(event.pos())) - menu.popup(event.globalPos()) - menu.exec_(event.globalPos()) - del menu - - def on_anchor_clicked(self, url): - text = str(url.toString()) - if text.startswith('tox:'): - import menu - self.add_contact = menu.AddContact(text[4:]) - self.add_contact.show() - else: - QtGui.QDesktopServices.openUrl(url) - self.clearFocus() - - def addAnimation(self, url, fileName): - movie = QtGui.QMovie(self) - movie.setFileName(fileName) - self.urls[movie] = url - movie.frameChanged[int].connect(lambda x: self.animate(movie)) - movie.start() - - def animate(self, movie): - self.document().addResource(QtGui.QTextDocument.ImageResource, - self.urls[movie], - movie.currentPixmap()) - self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth()) - - def decoratedText(self, text): - text = h.escape(text) # replace < and > - exp = QtCore.QRegExp( - '(' - '(?:\\b)((www\\.)|(http[s]?|ftp)://)' - '\\w+\\S+)' - '|(?:\\b)(file:///)([\\S| ]*)' - '|(?:\\b)(tox:[a-zA-Z\\d]{76}$)' - '|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)' - '|(?:\\b)(tox:\\S+@\\S+)') - offset = exp.indexIn(text, 0) - while offset != -1: # add links - url = exp.cap() - if exp.cap(2) == 'www.': - html = '{0}'.format(url) - else: - html = '{0}'.format(url) - text = text[:offset] + html + text[offset + len(exp.cap()):] - offset += len(html) - offset = exp.indexIn(text, offset) - arr = text.split('\n') - for i in range(len(arr)): # quotes - if arr[i].startswith('>'): - arr[i] = '' + arr[i][4:] + '' - text = '
'.join(arr) - text = smileys.SmileyLoader.get_instance().add_smileys_to_text(text, self) # smileys - return text - - -class MessageItem(QtGui.QWidget): - """ - Message in messages list - """ - def __init__(self, text, time, user='', sent=True, message_type=TOX_MESSAGE_TYPE['NORMAL'], parent=None): - QtGui.QWidget.__init__(self, parent) - self.name = DataLabel(self) - self.name.setGeometry(QtCore.QRect(2, 2, 95, 20)) - self.name.setTextFormat(QtCore.Qt.PlainText) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(11) - font.setBold(True) - self.name.setFont(font) - self.name.setText(user) - - self.time = QtGui.QLabel(self) - self.time.setGeometry(QtCore.QRect(parent.width() - 50, 0, 50, 20)) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(10) - font.setBold(False) - self.time.setFont(font) - self._time = time - if not sent: - movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif') - self.time.setMovie(movie) - movie.start() - self.t = True - else: - self.time.setText(convert_time(time)) - self.t = False - - self.message = MessageEdit(text, parent.width() - 150, message_type, self) - if message_type != TOX_MESSAGE_TYPE['NORMAL']: - self.name.setStyleSheet("QLabel { color: #5CB3FF; }") - self.message.setAlignment(QtCore.Qt.AlignCenter) - self.time.setStyleSheet("QLabel { color: #5CB3FF; }") - self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 150, self.message.height())) - self.setFixedHeight(self.message.height()) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x(): - self.listMenu = QtGui.QMenu() - delete_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Delete message', None, QtGui.QApplication.UnicodeUTF8)) - self.connect(delete_item, QtCore.SIGNAL("triggered()"), self.delete) - parent_position = self.time.mapToGlobal(QtCore.QPoint(0, 0)) - self.listMenu.move(parent_position) - self.listMenu.show() - - def delete(self): - pr = profile.Profile.get_instance() - pr.delete_message(self._time) - - def mark_as_sent(self): - if self.t: - self.time.setText(convert_time(self._time)) - self.t = False - return True - return False - - -class ContactItem(QtGui.QWidget): - """ - Contact in friends list - """ - - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) - mode = settings.Settings.get_instance()['compact_mode'] - self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) - self.avatar_label = QtGui.QLabel(self) - size = 32 if mode else 64 - self.avatar_label.setGeometry(QtCore.QRect(3, 4, size, size)) - self.avatar_label.setScaledContents(True) - self.name = DataLabel(self) - self.name.setGeometry(QtCore.QRect(50 if mode else 75, 3 if mode else 10, 150, 15 if mode else 25)) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(10 if mode else 12) - font.setBold(True) - self.name.setFont(font) - self.status_message = DataLabel(self) - self.status_message.setGeometry(QtCore.QRect(50 if mode else 75, 20 if mode else 30, 170, 15 if mode else 20)) - font.setPointSize(10) - font.setBold(False) - self.status_message.setFont(font) - self.connection_status = StatusCircle(self) - self.connection_status.setGeometry(QtCore.QRect(230, -2 if mode else 5, 32, 32)) - self.messages = UnreadMessagesCount(self) - self.messages.setGeometry(QtCore.QRect(20 if mode else 52, 20 if mode else 50, 30, 20)) - - -class StatusCircle(QtGui.QWidget): - """ - Connection status - """ - def __init__(self, parent): - QtGui.QWidget.__init__(self, parent) - self.setGeometry(0, 0, 32, 32) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) - self.unread = False - - def update(self, status, unread_messages=None): - if unread_messages is None: - unread_messages = self.unread - else: - self.unread = unread_messages - if status == TOX_USER_STATUS['NONE']: - name = 'online' - elif status == TOX_USER_STATUS['AWAY']: - name = 'idle' - elif status == TOX_USER_STATUS['BUSY']: - name = 'busy' - else: - name = 'offline' - if unread_messages: - name += '_notification' - self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) - else: - self.label.setGeometry(QtCore.QRect(2, 0, 32, 32)) - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(name)) - self.label.setPixmap(pixmap) - - -class UnreadMessagesCount(QtGui.QWidget): - - def __init__(self, parent=None): - super(UnreadMessagesCount, self).__init__(parent) - self.resize(30, 20) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(0, 0, 30, 20)) - self.label.setVisible(False) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(12) - font.setBold(True) - self.label.setFont(font) - self.label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter) - color = settings.Settings.get_instance()['unread_color'] - self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') - - def update(self, messages_count): - color = settings.Settings.get_instance()['unread_color'] - self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') - if messages_count: - self.label.setVisible(True) - self.label.setText(str(messages_count)) - else: - self.label.setVisible(False) - - -class FileTransferItem(QtGui.QListWidget): - - def __init__(self, file_name, size, time, user, friend_number, file_number, state, width, parent=None): - - QtGui.QListWidget.__init__(self, parent) - self.resize(QtCore.QSize(width, 34)) - if state == TOX_FILE_TRANSFER_STATE['CANCELLED']: - self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') - elif state in PAUSED_FILE_TRANSFERS: - self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') - else: - self.setStyleSheet('QListWidget { border: 1px solid green; }') - self.state = state - - self.name = DataLabel(self) - self.name.setGeometry(QtCore.QRect(3, 7, 95, 20)) - self.name.setTextFormat(QtCore.Qt.PlainText) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(11) - font.setBold(True) - self.name.setFont(font) - self.name.setText(user) - - self.time = QtGui.QLabel(self) - self.time.setGeometry(QtCore.QRect(width - 53, 7, 50, 20)) - font.setPointSize(10) - font.setBold(False) - self.time.setFont(font) - self.time.setText(convert_time(time)) - - self.cancel = QtGui.QPushButton(self) - self.cancel.setGeometry(QtCore.QRect(width - 120, 2, 30, 30)) - pixmap = QtGui.QPixmap(curr_directory() + '/images/decline.png') - icon = QtGui.QIcon(pixmap) - self.cancel.setIcon(icon) - self.cancel.setIconSize(QtCore.QSize(30, 30)) - self.cancel.setVisible(state in ACTIVE_FILE_TRANSFERS) - self.cancel.clicked.connect(lambda: self.cancel_transfer(friend_number, file_number)) - self.cancel.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none;}') - - self.accept_or_pause = QtGui.QPushButton(self) - self.accept_or_pause.setGeometry(QtCore.QRect(width - 170, 2, 30, 30)) - if state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - self.accept_or_pause.setVisible(True) - self.button_update('accept') - elif state in DO_NOT_SHOW_ACCEPT_BUTTON: - self.accept_or_pause.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # setup for continue - self.accept_or_pause.setVisible(True) - self.button_update('resume') - else: # pause - self.accept_or_pause.setVisible(True) - self.button_update('pause') - self.accept_or_pause.clicked.connect(lambda: self.accept_or_pause_transfer(friend_number, file_number, size)) - - self.accept_or_pause.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none}') - - self.pb = QtGui.QProgressBar(self) - self.pb.setGeometry(QtCore.QRect(100, 7, 100, 20)) - self.pb.setValue(0) - self.pb.setStyleSheet('QProgressBar { background-color: #302F2F; }') - self.pb.setVisible(state in SHOW_PROGRESS_BAR) - - self.file_name = DataLabel(self) - self.file_name.setGeometry(QtCore.QRect(210, 7, width - 420, 20)) - font.setPointSize(12) - self.file_name.setFont(font) - file_size = size // 1024 - if not file_size: - file_size = '{}B'.format(size) - elif file_size >= 1024: - file_size = '{}MB'.format(file_size // 1024) - else: - file_size = '{}KB'.format(file_size) - file_data = '{} {}'.format(file_size, file_name) - self.file_name.setText(file_data) - self.file_name.setToolTip(file_name) - self.saved_name = file_name - self.time_left = QtGui.QLabel(self) - self.time_left.setGeometry(QtCore.QRect(width - 87, 7, 30, 20)) - font.setPointSize(10) - self.time_left.setFont(font) - self.time_left.setVisible(state == TOX_FILE_TRANSFER_STATE['RUNNING']) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.paused = False - - def cancel_transfer(self, friend_number, file_number): - pr = profile.Profile.get_instance() - pr.cancel_transfer(friend_number, file_number) - self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') - self.cancel.setVisible(False) - self.accept_or_pause.setVisible(False) - self.pb.setVisible(False) - - def accept_or_pause_transfer(self, friend_number, file_number, size): - if self.state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: - directory = QtGui.QFileDialog.getExistingDirectory(self, - QtGui.QApplication.translate("MainWindow", 'Choose folder', None, QtGui.QApplication.UnicodeUTF8), - curr_directory(), - QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) - self.pb.setVisible(True) - if directory: - pr = profile.Profile.get_instance() - pr.accept_transfer(self, directory + '/' + self.saved_name, friend_number, file_number, size) - self.button_update('pause') - elif self.state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume - self.paused = False - profile.Profile.get_instance().resume_transfer(friend_number, file_number) - self.button_update('pause') - self.state = TOX_FILE_TRANSFER_STATE['RUNNING'] - else: # pause - self.paused = True - self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER'] - profile.Profile.get_instance().pause_transfer(friend_number, file_number) - self.button_update('resume') - self.accept_or_pause.clearFocus() - - def button_update(self, path): - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(path)) - icon = QtGui.QIcon(pixmap) - self.accept_or_pause.setIcon(icon) - self.accept_or_pause.setIconSize(QtCore.QSize(30, 30)) - - @QtCore.Slot(int, float, int) - def update(self, state, progress, time): - self.pb.setValue(int(progress * 100)) - if time + 1: - m, s = divmod(time, 60) - self.time_left.setText('{0:02d}:{1:02d}'.format(m, s)) - if self.state != state: - if state == TOX_FILE_TRANSFER_STATE['CANCELLED']: - self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') - self.cancel.setVisible(False) - self.accept_or_pause.setVisible(False) - self.pb.setVisible(False) - self.state = state - self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['FINISHED']: - self.accept_or_pause.setVisible(False) - self.pb.setVisible(False) - self.cancel.setVisible(False) - self.setStyleSheet('QListWidget { border: 1px solid green; }') - self.state = state - self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']: - self.accept_or_pause.setVisible(False) - self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') - self.state = state - self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: - self.button_update('resume') # setup button continue - self.setStyleSheet('QListWidget { border: 1px solid green; }') - self.state = state - self.time_left.setVisible(False) - elif state == TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']: - self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') - self.accept_or_pause.setVisible(False) - self.time_left.setVisible(False) - self.pb.setVisible(False) - elif not self.paused: # active - self.pb.setVisible(True) - self.accept_or_pause.setVisible(True) # setup to pause - self.button_update('pause') - self.setStyleSheet('QListWidget { border: 1px solid green; }') - self.state = state - self.time_left.setVisible(True) - - def mark_as_sent(self): - return False - - -class UnsentFileItem(FileTransferItem): - - def __init__(self, file_name, size, user, time, width, parent=None): - super(UnsentFileItem, self).__init__(file_name, size, time, user, -1, -1, - TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'], width, parent) - self._time = time - self.pb.setVisible(False) - movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif') - self.time.setMovie(movie) - movie.start() - - def cancel_transfer(self, *args): - pr = profile.Profile.get_instance() - pr.cancel_not_started_transfer(self._time) - - -class InlineImageItem(QtGui.QScrollArea): - - def __init__(self, data, width, elem): - - QtGui.QScrollArea.__init__(self) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self._elem = elem - self._image_label = QtGui.QLabel(self) - self._image_label.raise_() - self.setWidget(self._image_label) - self._image_label.setScaledContents(False) - self._pixmap = QtGui.QPixmap() - self._pixmap.loadFromData(data, 'PNG') - self._max_size = width - 30 - self._resize_needed = not (self._pixmap.width() <= self._max_size) - self._full_size = not self._resize_needed - if not self._resize_needed: - self._image_label.setPixmap(self._pixmap) - self.resize(QtCore.QSize(self._max_size + 5, self._pixmap.height() + 5)) - self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) - else: - pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) - self._image_label.setPixmap(pixmap) - self.resize(QtCore.QSize(self._max_size + 5, pixmap.height())) - self._image_label.setGeometry(5, 0, self._max_size + 5, pixmap.height()) - self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton and self._resize_needed: # scale inline - if self._full_size: - pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) - self._image_label.setPixmap(pixmap) - self.resize(QtCore.QSize(self._max_size, pixmap.height())) - self._image_label.setGeometry(5, 0, pixmap.width(), pixmap.height()) - else: - self._image_label.setPixmap(self._pixmap) - self.resize(QtCore.QSize(self._max_size, self._pixmap.height() + 17)) - self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) - self._full_size = not self._full_size - self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) - elif event.button() == QtCore.Qt.RightButton: # save inline - directory = QtGui.QFileDialog.getExistingDirectory(self, - QtGui.QApplication.translate("MainWindow", - 'Choose folder', None, - QtGui.QApplication.UnicodeUTF8), - curr_directory(), - QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) - if directory: - fl = QtCore.QFile(directory + '/toxygen_inline_' + curr_time().replace(':', '_') + '.png') - self._pixmap.save(fl, 'PNG') - - return False - - def mark_as_sent(self): - return False - - - - diff --git a/toxygen/loginscreen.py b/toxygen/loginscreen.py deleted file mode 100644 index df51c5b..0000000 --- a/toxygen/loginscreen.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- - -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from widgets import * - - -class NickEdit(LineEdit): - - def __init__(self, parent): - super(NickEdit, self).__init__(parent) - self.parent = parent - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Return: - self.parent.create_profile() - else: - super(NickEdit, self).keyPressEvent(event) - - -class LoginScreen(CenteredWidget): - - def __init__(self): - super(LoginScreen, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.resize(400, 200) - self.setMinimumSize(QtCore.QSize(400, 200)) - self.setMaximumSize(QtCore.QSize(400, 200)) - self.new_profile = QtGui.QPushButton(self) - self.new_profile.setGeometry(QtCore.QRect(20, 150, 171, 27)) - self.new_profile.clicked.connect(self.create_profile) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(20, 70, 101, 17)) - self.new_name = NickEdit(self) - self.new_name.setGeometry(QtCore.QRect(20, 100, 171, 31)) - self.load_profile = QtGui.QPushButton(self) - self.load_profile.setGeometry(QtCore.QRect(220, 150, 161, 27)) - self.load_profile.clicked.connect(self.load_ex_profile) - self.default = QtGui.QCheckBox(self) - self.default.setGeometry(QtCore.QRect(220, 110, 131, 22)) - self.groupBox = QtGui.QGroupBox(self) - self.groupBox.setGeometry(QtCore.QRect(210, 40, 181, 151)) - self.comboBox = QtGui.QComboBox(self.groupBox) - self.comboBox.setGeometry(QtCore.QRect(10, 30, 161, 27)) - self.groupBox_2 = QtGui.QGroupBox(self) - self.groupBox_2.setGeometry(QtCore.QRect(10, 40, 191, 151)) - self.toxygen = QtGui.QLabel(self) - self.groupBox.raise_() - self.groupBox_2.raise_() - self.comboBox.raise_() - self.default.raise_() - self.load_profile.raise_() - self.new_name.raise_() - self.new_profile.raise_() - self.toxygen.setGeometry(QtCore.QRect(160, 10, 90, 21)) - font = QtGui.QFont() - font.setFamily("Impact") - font.setPointSize(16) - self.toxygen.setFont(font) - self.toxygen.setObjectName("toxygen") - self.type = 0 - self.number = -1 - self.load_as_default = False - self.name = None - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.new_name.setPlaceholderText(QtGui.QApplication.translate("login", "Profile name", None, QtGui.QApplication.UnicodeUTF8)) - self.setWindowTitle(QtGui.QApplication.translate("login", "Log in", None, QtGui.QApplication.UnicodeUTF8)) - self.new_profile.setText(QtGui.QApplication.translate("login", "Create", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("login", "Profile name:", None, QtGui.QApplication.UnicodeUTF8)) - self.load_profile.setText(QtGui.QApplication.translate("login", "Load profile", None, QtGui.QApplication.UnicodeUTF8)) - self.default.setText(QtGui.QApplication.translate("login", "Use as default", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox.setTitle(QtGui.QApplication.translate("login", "Load existing profile", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_2.setTitle(QtGui.QApplication.translate("login", "Create new profile", None, QtGui.QApplication.UnicodeUTF8)) - self.toxygen.setText(QtGui.QApplication.translate("login", "toxygen", None, QtGui.QApplication.UnicodeUTF8)) - - def create_profile(self): - self.type = 1 - self.name = self.new_name.text() - self.close() - - def load_ex_profile(self): - if not self.create_only: - self.type = 2 - self.number = self.comboBox.currentIndex() - self.load_as_default = self.default.isChecked() - self.close() - - def update_select(self, data): - list_of_profiles = [] - for elem in data: - list_of_profiles.append(elem) - self.comboBox.addItems(list_of_profiles) - self.create_only = not list_of_profiles - - def update_on_close(self, func): - self.onclose = func - - def closeEvent(self, event): - self.onclose(self.type, self.number, self.load_as_default, self.name) - event.accept() diff --git a/toxygen/main.py b/toxygen/main.py deleted file mode 100644 index b7b7120..0000000 --- a/toxygen/main.py +++ /dev/null @@ -1,435 +0,0 @@ -import sys -from loginscreen import LoginScreen -import profile -from settings import * -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from bootstrap import node_generator -from mainscreen import MainWindow -from callbacks import init_callbacks -from util import curr_directory, program_version -import styles.style -import toxencryptsave -from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen -from plugin_support import PluginLoader - - -class Toxygen: - - def __init__(self, path_or_uri=None): - super(Toxygen, self).__init__() - self.tox = self.ms = self.init = self.mainloop = self.avloop = None - if path_or_uri is None: - self.uri = self.path = None - elif path_or_uri.startswith('tox:'): - self.path = None - self.uri = path_or_uri[4:] - else: - self.path = path_or_uri - self.uri = None - - def enter_pass(self, data): - """ - Show password screen - """ - tmp = [data] - p = PasswordScreen(toxencryptsave.ToxEncryptSave.get_instance(), tmp) - p.show() - self.app.connect(self.app, QtCore.SIGNAL("lastWindowClosed()"), self.app, QtCore.SLOT("quit()")) - self.app.exec_() - if tmp[0] == data: - raise SystemExit() - else: - return tmp[0] - - def main(self): - """ - Main function of app. loads login screen if needed and starts main screen - """ - app = QtGui.QApplication(sys.argv) - app.setWindowIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) - self.app = app - - # application color scheme - with open(curr_directory() + '/styles/style.qss') as fl: - dark_style = fl.read() - app.setStyleSheet(dark_style) - - encrypt_save = toxencryptsave.ToxEncryptSave() - - if self.path is not None: - path = os.path.dirname(self.path) + '/' - name = os.path.basename(self.path)[:-4] - data = ProfileHelper(path, name).open_profile() - if encrypt_save.is_data_encrypted(data): - data = self.enter_pass(data) - settings = Settings(name) - self.tox = profile.tox_factory(data, settings) - else: - auto_profile = Settings.get_auto_profile() - if not auto_profile[0]: - # show login screen if default profile not found - current_locale = QtCore.QLocale() - curr_lang = current_locale.languageToString(current_locale.language()) - langs = Settings.supported_languages() - if curr_lang in langs: - lang_path = langs[curr_lang] - translator = QtCore.QTranslator() - translator.load(curr_directory() + '/translations/' + lang_path) - app.installTranslator(translator) - app.translator = translator - ls = LoginScreen() - ls.setWindowIconText("Toxygen") - profiles = ProfileHelper.find_profiles() - ls.update_select(map(lambda x: x[1], profiles)) - _login = self.Login(profiles) - ls.update_on_close(_login.login_screen_close) - ls.show() - app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()")) - app.exec_() - if not _login.t: - return - elif _login.t == 1: # create new profile - _login.name = _login.name.strip() - name = _login.name if _login.name else 'toxygen_user' - pr = map(lambda x: x[1], ProfileHelper.find_profiles()) - if name in list(pr): - msgBox = QtGui.QMessageBox() - msgBox.setWindowTitle( - QtGui.QApplication.translate("MainWindow", "Error", None, QtGui.QApplication.UnicodeUTF8)) - text = (QtGui.QApplication.translate("MainWindow", - 'Profile with this name already exists', - None, QtGui.QApplication.UnicodeUTF8)) - msgBox.setText(text) - msgBox.exec_() - return - self.tox = profile.tox_factory() - self.tox.self_set_name(bytes(_login.name, 'utf-8') if _login.name else b'Toxygen User') - self.tox.self_set_status_message(b'Toxing on Toxygen') - reply = QtGui.QMessageBox.question(None, - 'Profile {}'.format(name), - QtGui.QApplication.translate("login", - 'Do you want to set profile password?', - None, - QtGui.QApplication.UnicodeUTF8), - QtGui.QMessageBox.Yes, - QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: - set_pass = SetProfilePasswordScreen(encrypt_save) - set_pass.show() - self.app.connect(self.app, QtCore.SIGNAL("lastWindowClosed()"), self.app, QtCore.SLOT("quit()")) - self.app.exec_() - ProfileHelper(Settings.get_default_path(), name).save_profile(self.tox.get_savedata()) - path = Settings.get_default_path() - settings = Settings(name) - if curr_lang in langs: - settings['language'] = curr_lang - settings.save() - else: # load existing profile - path, name = _login.get_data() - if _login.default: - Settings.set_auto_profile(path, name) - data = ProfileHelper(path, name).open_profile() - if encrypt_save.is_data_encrypted(data): - data = self.enter_pass(data) - settings = Settings(name) - self.tox = profile.tox_factory(data, settings) - else: - path, name = auto_profile - data = ProfileHelper(path, name).open_profile() - if encrypt_save.is_data_encrypted(data): - data = self.enter_pass(data) - settings = Settings(name) - self.tox = profile.tox_factory(data, settings) - - if Settings.is_active_profile(path, name): # profile is in use - reply = QtGui.QMessageBox.question(None, - 'Profile {}'.format(name), - QtGui.QApplication.translate("login", 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?', None, QtGui.QApplication.UnicodeUTF8), - QtGui.QMessageBox.Yes, - QtGui.QMessageBox.No) - if reply != QtGui.QMessageBox.Yes: - return - else: - settings.set_active_profile() - - lang = Settings.supported_languages()[settings['language']] - translator = QtCore.QTranslator() - translator.load(curr_directory() + '/translations/' + lang) - app.installTranslator(translator) - app.translator = translator - - # tray icon - self.tray = QtGui.QSystemTrayIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) - self.tray.setObjectName('tray') - - self.ms = MainWindow(self.tox, self.reset, self.tray) - - class Menu(QtGui.QMenu): - - def newStatus(self, status): - profile.Profile.get_instance().set_status(status) - self.aboutToShow() - self.hide() - - def aboutToShow(self): - status = profile.Profile.get_instance().status - act = self.act - if status is None or Settings.get_instance().locked: - self.actions()[1].setVisible(False) - else: - self.actions()[1].setVisible(True) - act.actions()[0].setChecked(False) - act.actions()[1].setChecked(False) - act.actions()[2].setChecked(False) - act.actions()[status].setChecked(True) - self.actions()[2].setVisible(not Settings.get_instance().locked) - - def languageChange(self, *args, **kwargs): - self.actions()[0].setText(QtGui.QApplication.translate('tray', 'Open Toxygen', None, QtGui.QApplication.UnicodeUTF8)) - self.actions()[1].setText(QtGui.QApplication.translate('tray', 'Set status', None, QtGui.QApplication.UnicodeUTF8)) - self.actions()[2].setText(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8)) - self.act.actions()[0].setText(QtGui.QApplication.translate('tray', 'Online', None, QtGui.QApplication.UnicodeUTF8)) - self.act.actions()[1].setText(QtGui.QApplication.translate('tray', 'Away', None, QtGui.QApplication.UnicodeUTF8)) - self.act.actions()[2].setText(QtGui.QApplication.translate('tray', 'Busy', None, QtGui.QApplication.UnicodeUTF8)) - - m = Menu() - show = m.addAction(QtGui.QApplication.translate('tray', 'Open Toxygen', None, QtGui.QApplication.UnicodeUTF8)) - sub = m.addMenu(QtGui.QApplication.translate('tray', 'Set status', None, QtGui.QApplication.UnicodeUTF8)) - onl = sub.addAction(QtGui.QApplication.translate('tray', 'Online', None, QtGui.QApplication.UnicodeUTF8)) - away = sub.addAction(QtGui.QApplication.translate('tray', 'Away', None, QtGui.QApplication.UnicodeUTF8)) - busy = sub.addAction(QtGui.QApplication.translate('tray', 'Busy', None, QtGui.QApplication.UnicodeUTF8)) - onl.setCheckable(True) - away.setCheckable(True) - busy.setCheckable(True) - m.act = sub - exit = m.addAction(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8)) - - def show_window(): - def show(): - if not self.ms.isActiveWindow(): - self.ms.setWindowState(self.ms.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) - self.ms.activateWindow() - self.ms.show() - if not Settings.get_instance().locked: - show() - else: - def correct_pass(): - show() - Settings.get_instance().locked = False - self.p = UnlockAppScreen(toxencryptsave.ToxEncryptSave.get_instance(), correct_pass) - self.p.show() - - m.connect(show, QtCore.SIGNAL("triggered()"), show_window) - m.connect(exit, QtCore.SIGNAL("triggered()"), lambda: app.exit()) - m.connect(m, QtCore.SIGNAL("aboutToShow()"), lambda: m.aboutToShow()) - sub.connect(onl, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(0)) - sub.connect(away, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(1)) - sub.connect(busy, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(2)) - - self.tray.setContextMenu(m) - self.tray.show() - - self.ms.show() - - plugin_helper = PluginLoader(self.tox, settings) # plugin support - plugin_helper.load() - - # init thread - self.init = self.InitThread(self.tox, self.ms, self.tray) - self.init.start() - - # starting threads for tox iterate and toxav iterate - self.mainloop = self.ToxIterateThread(self.tox) - self.mainloop.start() - self.avloop = self.ToxAVIterateThread(self.tox.AV) - self.avloop.start() - - if self.uri is not None: - self.ms.add_contact(self.uri) - - app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()")) - app.exec_() - self.init.stop = True - self.mainloop.stop = True - self.avloop.stop = True - plugin_helper.stop() - self.mainloop.wait() - self.init.wait() - self.avloop.wait() - data = self.tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - settings.close() - del self.tox - - def reset(self): - """ - Create new tox instance (new network settings) - :return: tox instance - """ - self.mainloop.stop = True - self.init.stop = True - self.avloop.stop = True - self.mainloop.wait() - self.init.wait() - self.avloop.wait() - data = self.tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - del self.tox - # create new tox instance - self.tox = profile.tox_factory(data, Settings.get_instance()) - # init thread - self.init = self.InitThread(self.tox, self.ms, self.tray) - self.init.start() - - # starting threads for tox iterate and toxav iterate - self.mainloop = self.ToxIterateThread(self.tox) - self.mainloop.start() - - self.avloop = self.ToxAVIterateThread(self.tox.AV) - self.avloop.start() - - plugin_helper = PluginLoader.get_instance() - plugin_helper.set_tox(self.tox) - - return self.tox - - # ----------------------------------------------------------------------------------------------------------------- - # Inner classes - # ----------------------------------------------------------------------------------------------------------------- - - class InitThread(QtCore.QThread): - - def __init__(self, tox, ms, tray): - QtCore.QThread.__init__(self) - self.tox, self.ms, self.tray = tox, ms, tray - self.stop = False - - def run(self): - # initializing callbacks - init_callbacks(self.tox, self.ms, self.tray) - # bootstrap - try: - for data in node_generator(): - if self.stop: - return - self.tox.bootstrap(*data) - self.tox.add_tcp_relay(*data) - except: - pass - for _ in range(10): - if self.stop: - return - self.msleep(1000) - while not self.tox.self_get_connection_status(): - try: - for data in node_generator(): - if self.stop: - return - self.tox.bootstrap(*data) - self.tox.add_tcp_relay(*data) - except: - pass - finally: - self.msleep(5000) - - class ToxIterateThread(QtCore.QThread): - - def __init__(self, tox): - QtCore.QThread.__init__(self) - self.tox = tox - self.stop = False - - def run(self): - while not self.stop: - self.tox.iterate() - self.msleep(self.tox.iteration_interval()) - - class ToxAVIterateThread(QtCore.QThread): - - def __init__(self, toxav): - QtCore.QThread.__init__(self) - self.toxav = toxav - self.stop = False - - def run(self): - while not self.stop: - self.toxav.iterate() - self.msleep(self.toxav.iteration_interval()) - - class Login: - - def __init__(self, arr): - self.arr = arr - - def login_screen_close(self, t, number=-1, default=False, name=None): - """ Function which processes data from login screen - :param t: 0 - window was closed, 1 - new profile was created, 2 - profile loaded - :param number: num of chosen profile in list (-1 by default) - :param default: was or not chosen profile marked as default - :param name: name of new profile - """ - self.t = t - self.num = number - self.default = default - self.name = name - - def get_data(self): - return self.arr[self.num] - - -def clean(): - """Removes all windows libs from libs folder""" - d = curr_directory() + '/libs/' - for fl in ('libtox64.dll', 'libtox.dll', 'libsodium64.a', 'libsodium.a'): - if os.path.exists(d + fl): - os.remove(d + fl) - - -def configure(): - """Removes unused libs""" - d = curr_directory() + '/libs/' - is_64bits = sys.maxsize > 2 ** 32 - if not is_64bits: - if os.path.exists(d + 'libtox64.dll'): - os.remove(d + 'libtox64.dll') - if os.path.exists(d + 'libsodium64.a'): - os.remove(d + 'libsodium64.a') - else: - if os.path.exists(d + 'libtox.dll'): - os.remove(d + 'libtox.dll') - if os.path.exists(d + 'libsodium.a'): - os.remove(d + 'libsodium.a') - try: - os.rename(d + 'libtox64.dll', d + 'libtox.dll') - os.rename(d + 'libsodium64.a', d + 'libsodium.a') - except: - pass - - -def main(): - if len(sys.argv) == 1: - toxygen = Toxygen() - else: # started with argument(s) - arg = sys.argv[1] - if arg == '--version': - print('Toxygen ' + program_version) - return - elif arg == '--help': - print('Usage:\ntoxygen path_to_profile\ntoxygen tox_id\ntoxygen --version') - return - elif arg == '--configure': - configure() - return - elif arg == '--clean': - clean() - return - else: - toxygen = Toxygen(arg) - toxygen.main() - - -if __name__ == '__main__': - main() diff --git a/toxygen/mainscreen.py b/toxygen/mainscreen.py deleted file mode 100644 index 3260614..0000000 --- a/toxygen/mainscreen.py +++ /dev/null @@ -1,611 +0,0 @@ -# -*- coding: utf-8 -*- - -from menu import * -from profile import * -from list_items import * -from widgets import MultilineEdit, LineEdit -import plugin_support -from mainscreen_widgets import * - - -class MainWindow(QtGui.QMainWindow): - - def __init__(self, tox, reset, tray): - super(MainWindow, self).__init__() - self.reset = reset - self.tray = tray - self.setAcceptDrops(True) - self.initUI(tox) - if settings.Settings.get_instance()['show_welcome_screen']: - self.ws = WelcomeScreen() - - def setup_menu(self, MainWindow): - self.menubar = QtGui.QMenuBar(MainWindow) - self.menubar.setObjectName("menubar") - self.menubar.setNativeMenuBar(False) - self.menubar.setMinimumSize(self.width(), 25) - self.menubar.setMaximumSize(self.width(), 25) - self.menubar.setBaseSize(self.width(), 25) - - self.menuProfile = QtGui.QMenu(self.menubar) - self.menuProfile.setObjectName("menuProfile") - self.menuSettings = QtGui.QMenu(self.menubar) - self.menuSettings.setObjectName("menuSettings") - self.menuPlugins = QtGui.QMenu(self.menubar) - self.menuPlugins.setObjectName("menuPlugins") - self.menuAbout = QtGui.QMenu(self.menubar) - self.menuAbout.setObjectName("menuAbout") - - self.actionAdd_friend = QtGui.QAction(MainWindow) - self.actionAdd_friend.setObjectName("actionAdd_friend") - self.actionprofilesettings = QtGui.QAction(MainWindow) - self.actionprofilesettings.setObjectName("actionprofilesettings") - self.actionPrivacy_settings = QtGui.QAction(MainWindow) - self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") - self.actionInterface_settings = QtGui.QAction(MainWindow) - self.actionInterface_settings.setObjectName("actionInterface_settings") - self.actionNotifications = QtGui.QAction(MainWindow) - self.actionNotifications.setObjectName("actionNotifications") - self.actionNetwork = QtGui.QAction(MainWindow) - self.actionNetwork.setObjectName("actionNetwork") - self.actionAbout_program = QtGui.QAction(MainWindow) - self.actionAbout_program.setObjectName("actionAbout_program") - self.actionSettings = QtGui.QAction(MainWindow) - self.actionSettings.setObjectName("actionSettings") - self.audioSettings = QtGui.QAction(MainWindow) - self.pluginData = QtGui.QAction(MainWindow) - self.importPlugin = QtGui.QAction(MainWindow) - self.lockApp = QtGui.QAction(MainWindow) - self.menuProfile.addAction(self.actionAdd_friend) - self.menuProfile.addAction(self.actionSettings) - self.menuProfile.addAction(self.lockApp) - self.menuSettings.addAction(self.actionPrivacy_settings) - self.menuSettings.addAction(self.actionInterface_settings) - self.menuSettings.addAction(self.actionNotifications) - self.menuSettings.addAction(self.actionNetwork) - self.menuSettings.addAction(self.audioSettings) - self.menuPlugins.addAction(self.pluginData) - self.menuPlugins.addAction(self.importPlugin) - self.menuAbout.addAction(self.actionAbout_program) - self.menubar.addAction(self.menuProfile.menuAction()) - self.menubar.addAction(self.menuSettings.menuAction()) - self.menubar.addAction(self.menuPlugins.menuAction()) - self.menubar.addAction(self.menuAbout.menuAction()) - - self.actionAbout_program.triggered.connect(self.about_program) - self.actionNetwork.triggered.connect(self.network_settings) - self.actionAdd_friend.triggered.connect(self.add_contact) - self.actionSettings.triggered.connect(self.profilesettings) - self.actionPrivacy_settings.triggered.connect(self.privacy_settings) - self.actionInterface_settings.triggered.connect(self.interface_settings) - self.actionNotifications.triggered.connect(self.notification_settings) - self.audioSettings.triggered.connect(self.audio_settings) - self.pluginData.triggered.connect(self.plugins_menu) - self.lockApp.triggered.connect(self.lock_app) - self.importPlugin.triggered.connect(self.import_plugin) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def languageChange(self, *args, **kwargs): - self.retranslateUi() - - def event(self, event): - if event.type() == QtCore.QEvent.WindowActivate: - self.tray.setIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) - return super(MainWindow, self).event(event) - - def retranslateUi(self): - self.lockApp.setText(QtGui.QApplication.translate("MainWindow", "Lock", None, QtGui.QApplication.UnicodeUTF8)) - self.menuPlugins.setTitle(QtGui.QApplication.translate("MainWindow", "Plugins", None, QtGui.QApplication.UnicodeUTF8)) - self.pluginData.setText(QtGui.QApplication.translate("MainWindow", "List of plugins", None, QtGui.QApplication.UnicodeUTF8)) - self.menuProfile.setTitle(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8)) - self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) - self.menuAbout.setTitle(QtGui.QApplication.translate("MainWindow", "About", None, QtGui.QApplication.UnicodeUTF8)) - self.actionAdd_friend.setText(QtGui.QApplication.translate("MainWindow", "Add contact", None, QtGui.QApplication.UnicodeUTF8)) - self.actionprofilesettings.setText(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8)) - self.actionPrivacy_settings.setText(QtGui.QApplication.translate("MainWindow", "Privacy", None, QtGui.QApplication.UnicodeUTF8)) - self.actionInterface_settings.setText(QtGui.QApplication.translate("MainWindow", "Interface", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNotifications.setText(QtGui.QApplication.translate("MainWindow", "Notifications", None, QtGui.QApplication.UnicodeUTF8)) - self.actionNetwork.setText(QtGui.QApplication.translate("MainWindow", "Network", None, QtGui.QApplication.UnicodeUTF8)) - self.actionAbout_program.setText(QtGui.QApplication.translate("MainWindow", "About program", None, QtGui.QApplication.UnicodeUTF8)) - self.actionSettings.setText(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) - self.audioSettings.setText(QtGui.QApplication.translate("MainWindow", "Audio", None, QtGui.QApplication.UnicodeUTF8)) - self.contact_name.setPlaceholderText(QtGui.QApplication.translate("MainWindow", "Search", None, QtGui.QApplication.UnicodeUTF8)) - self.sendMessageButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Send message", None, QtGui.QApplication.UnicodeUTF8)) - self.callButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Start audio call with friend", None, QtGui.QApplication.UnicodeUTF8)) - self.online_contacts.clear() - self.online_contacts.addItem(QtGui.QApplication.translate("MainWindow", "All", None, QtGui.QApplication.UnicodeUTF8)) - self.online_contacts.addItem(QtGui.QApplication.translate("MainWindow", "Online", None, QtGui.QApplication.UnicodeUTF8)) - self.online_contacts.setCurrentIndex(int(Settings.get_instance()['show_online_friends'])) - self.importPlugin.setText(QtGui.QApplication.translate("MainWindow", "Import plugin", None, QtGui.QApplication.UnicodeUTF8)) - - def setup_right_bottom(self, Form): - Form.resize(650, 60) - self.messageEdit = MessageArea(Form, self) - self.messageEdit.setGeometry(QtCore.QRect(0, 3, 450, 55)) - self.messageEdit.setObjectName("messageEdit") - font = QtGui.QFont() - font.setPointSize(10) - self.messageEdit.setFont(font) - - self.sendMessageButton = QtGui.QPushButton(Form) - self.sendMessageButton.setGeometry(QtCore.QRect(565, 3, 60, 55)) - self.sendMessageButton.setObjectName("sendMessageButton") - - self.menuButton = MenuButton(Form, self.show_menu) - self.menuButton.setGeometry(QtCore.QRect(QtCore.QRect(455, 3, 55, 55))) - - pixmap = QtGui.QPixmap('send.png') - icon = QtGui.QIcon(pixmap) - self.sendMessageButton.setIcon(icon) - self.sendMessageButton.setIconSize(QtCore.QSize(45, 60)) - - pixmap = QtGui.QPixmap('menu.png') - icon = QtGui.QIcon(pixmap) - self.menuButton.setIcon(icon) - self.menuButton.setIconSize(QtCore.QSize(40, 40)) - - self.sendMessageButton.clicked.connect(self.send_message) - - QtCore.QMetaObject.connectSlotsByName(Form) - - def setup_left_center_menu(self, Form): - Form.resize(270, 25) - self.search_label = QtGui.QLabel(Form) - self.search_label.setGeometry(QtCore.QRect(3, 2, 20, 20)) - pixmap = QtGui.QPixmap() - pixmap.load(curr_directory() + '/images/search.png') - self.search_label.setScaledContents(False) - self.search_label.setPixmap(pixmap) - - self.contact_name = LineEdit(Form) - self.contact_name.setGeometry(QtCore.QRect(0, 0, 150, 25)) - self.contact_name.setObjectName("contact_name") - self.contact_name.textChanged.connect(self.filtering) - - self.online_contacts = QtGui.QComboBox(Form) - self.online_contacts.setGeometry(QtCore.QRect(150, 0, 120, 25)) - self.online_contacts.activated[int].connect(lambda x: self.filtering()) - self.search_label.raise_() - - QtCore.QMetaObject.connectSlotsByName(Form) - - def setup_left_top(self, Form): - Form.setCursor(QtCore.Qt.PointingHandCursor) - Form.setMinimumSize(QtCore.QSize(270, 100)) - Form.setMaximumSize(QtCore.QSize(270, 100)) - Form.setBaseSize(QtCore.QSize(270, 100)) - self.avatar_label = Form.avatar_label = QtGui.QLabel(Form) - self.avatar_label.setGeometry(QtCore.QRect(5, 30, 64, 64)) - self.avatar_label.setScaledContents(True) - self.name = Form.name = DataLabel(Form) - Form.name.setGeometry(QtCore.QRect(75, 40, 150, 25)) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(14) - font.setBold(True) - Form.name.setFont(font) - Form.name.setObjectName("name") - self.status_message = Form.status_message = DataLabel(Form) - Form.status_message.setGeometry(QtCore.QRect(75, 60, 170, 25)) - font.setPointSize(12) - font.setBold(False) - Form.status_message.setFont(font) - Form.status_message.setObjectName("status_message") - self.connection_status = Form.connection_status = StatusCircle(Form) - Form.connection_status.setGeometry(QtCore.QRect(230, 35, 32, 32)) - self.avatar_label.mouseReleaseEvent = self.profilesettings - self.status_message.mouseReleaseEvent = self.profilesettings - self.name.mouseReleaseEvent = self.profilesettings - self.connection_status.raise_() - Form.connection_status.setObjectName("connection_status") - - def setup_right_top(self, Form): - Form.resize(650, 100) - self.account_avatar = QtGui.QLabel(Form) - self.account_avatar.setGeometry(QtCore.QRect(10, 30, 64, 64)) - self.account_avatar.setScaledContents(True) - self.account_name = DataLabel(Form) - self.account_name.setGeometry(QtCore.QRect(100, 25, 400, 25)) - self.account_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) - font = QtGui.QFont() - font.setFamily("Times New Roman") - font.setPointSize(14) - font.setBold(True) - self.account_name.setFont(font) - self.account_name.setObjectName("account_name") - self.account_status = DataLabel(Form) - self.account_status.setGeometry(QtCore.QRect(100, 45, 400, 25)) - self.account_status.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) - font.setPointSize(12) - font.setBold(False) - self.account_status.setFont(font) - self.account_status.setObjectName("account_status") - self.callButton = QtGui.QPushButton(Form) - self.callButton.setGeometry(QtCore.QRect(550, 30, 50, 50)) - self.callButton.setObjectName("callButton") - self.callButton.clicked.connect(lambda: self.profile.call_click(True)) - self.videocallButton = QtGui.QPushButton(Form) - self.videocallButton.setGeometry(QtCore.QRect(550, 30, 50, 50)) - self.videocallButton.setObjectName("videocallButton") - self.videocallButton.clicked.connect(lambda: self.profile.call_click(True, True)) - self.update_call_state('call') - self.typing = QtGui.QLabel(Form) - self.typing.setGeometry(QtCore.QRect(500, 50, 50, 30)) - pixmap = QtGui.QPixmap(QtCore.QSize(50, 30)) - pixmap.load(curr_directory() + '/images/typing.png') - self.typing.setScaledContents(False) - self.typing.setPixmap(pixmap.scaled(50, 30, QtCore.Qt.KeepAspectRatio)) - self.typing.setVisible(False) - QtCore.QMetaObject.connectSlotsByName(Form) - - def setup_left_center(self, widget): - self.friends_list = QtGui.QListWidget(widget) - self.friends_list.setObjectName("friends_list") - self.friends_list.setGeometry(0, 0, 270, 310) - self.friends_list.clicked.connect(self.friend_click) - self.friends_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.friends_list.connect(self.friends_list, QtCore.SIGNAL("customContextMenuRequested(QPoint)"), - self.friend_right_click) - self.friends_list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) - - def setup_right_center(self, widget): - self.messages = QtGui.QListWidget(widget) - self.messages.setGeometry(0, 0, 620, 310) - self.messages.setObjectName("messages") - self.messages.setSpacing(1) - self.messages.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.messages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.messages.setFocusPolicy(QtCore.Qt.NoFocus) - - def load(pos): - if not pos: - self.profile.load_history() - self.messages.verticalScrollBar().setValue(1) - self.messages.verticalScrollBar().valueChanged.connect(load) - self.messages.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) - - def initUI(self, tox): - self.setMinimumSize(920, 500) - s = Settings.get_instance() - self.setGeometry(s['x'], s['y'], s['width'], s['height']) - self.setWindowTitle('Toxygen') - os.chdir(curr_directory() + '/images/') - main = QtGui.QWidget() - grid = QtGui.QGridLayout() - search = QtGui.QWidget() - name = QtGui.QWidget() - info = QtGui.QWidget() - main_list = QtGui.QWidget() - messages = QtGui.QWidget() - message_buttons = QtGui.QWidget() - self.setup_left_center_menu(search) - self.setup_left_top(name) - self.setup_right_center(messages) - self.setup_right_top(info) - self.setup_right_bottom(message_buttons) - self.setup_left_center(main_list) - if not Settings.get_instance()['mirror_mode']: - grid.addWidget(search, 1, 0) - grid.addWidget(name, 0, 0) - grid.addWidget(messages, 1, 1, 2, 1) - grid.addWidget(info, 0, 1) - grid.addWidget(message_buttons, 3, 1) - grid.addWidget(main_list, 2, 0, 2, 1) - grid.setColumnMinimumWidth(1, 500) - grid.setColumnMinimumWidth(0, 270) - else: - grid.addWidget(search, 1, 1) - grid.addWidget(name, 0, 1) - grid.addWidget(messages, 1, 0, 2, 1) - grid.addWidget(info, 0, 0) - grid.addWidget(message_buttons, 3, 0) - grid.addWidget(main_list, 2, 1, 2, 1) - grid.setColumnMinimumWidth(0, 500) - grid.setColumnMinimumWidth(1, 270) - grid.setSpacing(0) - grid.setContentsMargins(0, 0, 0, 0) - grid.setRowMinimumHeight(0, 100) - grid.setRowMinimumHeight(1, 25) - grid.setRowMinimumHeight(2, 320) - grid.setRowMinimumHeight(3, 55) - grid.setColumnStretch(1, 1) - grid.setRowStretch(2, 1) - main.setLayout(grid) - self.setCentralWidget(main) - self.setup_menu(self) - self.messageEdit.setFocus() - self.user_info = name - self.friend_info = info - self.retranslateUi() - self.profile = Profile(tox, self) - - def closeEvent(self, *args, **kwargs): - self.profile.save_history() - self.profile.close() - s = Settings.get_instance() - s['x'] = self.pos().x() - s['y'] = self.pos().y() - s['width'] = self.width() - s['height'] = self.height() - s.save() - QtGui.QApplication.closeAllWindows() - - def resizeEvent(self, *args, **kwargs): - self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 155) - self.friends_list.setGeometry(0, 0, 270, self.height() - 125) - - self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 40, 50, 50)) - self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 40, 50, 50)) - self.typing.setGeometry(QtCore.QRect(self.width() - 450, 50, 50, 30)) - - self.messageEdit.setGeometry(QtCore.QRect(55, 0, self.width() - 395, 55)) - self.menuButton.setGeometry(QtCore.QRect(0, 0, 55, 55)) - self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 340, 0, 70, 55)) - - self.account_name.setGeometry(QtCore.QRect(100, 40, self.width() - 560, 25)) - self.account_status.setGeometry(QtCore.QRect(100, 60, self.width() - 560, 25)) - self.messageEdit.setFocus() - self.profile.update() - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Escape: - self.hide() - else: - super(MainWindow, self).keyPressEvent(event) - - # ----------------------------------------------------------------------------------------------------------------- - # Functions which called when user click in menu - # ----------------------------------------------------------------------------------------------------------------- - - def about_program(self): - import util - msgBox = QtGui.QMessageBox() - msgBox.setWindowTitle(QtGui.QApplication.translate("MainWindow", "About", None, QtGui.QApplication.UnicodeUTF8)) - text = (QtGui.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.\nVersion: ', None, QtGui.QApplication.UnicodeUTF8)) - msgBox.setText(text + util.program_version + '\nGitHub: github.com/xveduk/toxygen/') - msgBox.exec_() - - def network_settings(self): - self.n_s = NetworkSettings(self.reset) - self.n_s.show() - - def plugins_menu(self): - self.p_s = PluginsSettings() - self.p_s.show() - - def add_contact(self, link=''): - self.a_c = AddContact(link) - self.a_c.show() - - def profilesettings(self, *args): - self.p_s = ProfileSettings() - self.p_s.show() - - def privacy_settings(self): - self.priv_s = PrivacySettings() - self.priv_s.show() - - def notification_settings(self): - self.notif_s = NotificationsSettings() - self.notif_s.show() - - def interface_settings(self): - self.int_s = InterfaceSettings() - self.int_s.show() - - def audio_settings(self): - self.audio_s = AudioSettings() - self.audio_s.show() - - def import_plugin(self): - import util - directory = QtGui.QFileDialog.getExistingDirectory(self, - QtGui.QApplication.translate("MainWindow", 'Choose folder with plugin', - None, - QtGui.QApplication.UnicodeUTF8), - util.curr_directory(), - QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) - if directory: - src = directory + '/' - dest = curr_directory() + '/plugins/' - util.copy(src, dest) - msgBox = QtGui.QMessageBox() - msgBox.setWindowTitle( - QtGui.QApplication.translate("MainWindow", "Restart Toxygen", None, QtGui.QApplication.UnicodeUTF8)) - msgBox.setText( - QtGui.QApplication.translate("MainWindow", 'Plugin will be loaded after restart', None, - QtGui.QApplication.UnicodeUTF8)) - msgBox.exec_() - - def lock_app(self): - if toxencryptsave.ToxEncryptSave.get_instance().has_password(): - Settings.get_instance().locked = True - self.hide() - else: - msgBox = QtGui.QMessageBox() - msgBox.setWindowTitle( - QtGui.QApplication.translate("MainWindow", "Cannot lock app", None, QtGui.QApplication.UnicodeUTF8)) - msgBox.setText( - QtGui.QApplication.translate("MainWindow", 'Error. Profile password is not set.', None, - QtGui.QApplication.UnicodeUTF8)) - msgBox.exec_() - - def show_menu(self): - if not hasattr(self, 'menu'): - self.menu = DropdownMenu(self) - self.menu.setGeometry(QtCore.QRect(0 if Settings.get_instance()['mirror_mode'] else 270, - self.height() - 120, - 180, - 120)) - self.menu.show() - - # ----------------------------------------------------------------------------------------------------------------- - # Messages, calls and file transfers - # ----------------------------------------------------------------------------------------------------------------- - - def send_message(self): - text = self.messageEdit.toPlainText() - self.profile.send_message(text) - - def send_file(self): - self.menu.hide() - if self.profile.active_friend + 1: - choose = QtGui.QApplication.translate("MainWindow", 'Choose file', None, QtGui.QApplication.UnicodeUTF8) - name = QtGui.QFileDialog.getOpenFileName(self, choose, options=QtGui.QFileDialog.DontUseNativeDialog) - if name[0]: - self.profile.send_file(name[0]) - - def send_screenshot(self, hide=False): - self.menu.hide() - if self.profile.active_friend + 1: - self.sw = ScreenShotWindow(self) - self.sw.show() - if hide: - self.hide() - - def send_smiley(self): - self.menu.hide() - if self.profile.active_friend + 1: - self.smiley = SmileyWindow(self) - self.smiley.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(), - self.y() + self.height() - 200, - self.smiley.width(), - self.smiley.height())) - self.smiley.show() - - def send_sticker(self): - self.menu.hide() - if self.profile.active_friend + 1: - self.sticker = StickerWindow(self) - self.sticker.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(), - self.y() + self.height() - 200, - self.sticker.width(), - self.sticker.height())) - self.sticker.show() - - def active_call(self): - self.update_call_state('finish_call') - - def incoming_call(self): - self.update_call_state('incoming_call') - - def call_finished(self): - self.update_call_state('call') - - def update_call_state(self, fl): - # TODO: do smth with video call button - os.chdir(curr_directory() + '/images/') - pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(fl)) - icon = QtGui.QIcon(pixmap) - self.callButton.setIcon(icon) - self.callButton.setIconSize(QtCore.QSize(50, 50)) - pixmap = QtGui.QPixmap(curr_directory() + '/images/videocall.png') - icon = QtGui.QIcon(pixmap) - self.videocallButton.setIcon(icon) - self.videocallButton.setIconSize(QtCore.QSize(35, 35)) - - # ----------------------------------------------------------------------------------------------------------------- - # Functions which called when user open context menu in friends list - # ----------------------------------------------------------------------------------------------------------------- - - def friend_right_click(self, pos): - item = self.friends_list.itemAt(pos) - num = self.friends_list.indexFromItem(item).row() - friend = Profile.get_instance().get_friend(num) - settings = Settings.get_instance() - allowed = friend.tox_id in settings['auto_accept_from_friends'] - auto = QtGui.QApplication.translate("MainWindow", 'Disallow auto accept', None, QtGui.QApplication.UnicodeUTF8) if allowed else QtGui.QApplication.translate("MainWindow", 'Allow auto accept', None, QtGui.QApplication.UnicodeUTF8) - if item is not None: - self.listMenu = QtGui.QMenu() - set_alias_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Set alias', None, QtGui.QApplication.UnicodeUTF8)) - clear_history_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Clear history', None, QtGui.QApplication.UnicodeUTF8)) - copy_menu = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Copy', None, QtGui.QApplication.UnicodeUTF8)) - copy_name_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Name', None, QtGui.QApplication.UnicodeUTF8)) - copy_status_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Status message', None, QtGui.QApplication.UnicodeUTF8)) - copy_key_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Public key', None, QtGui.QApplication.UnicodeUTF8)) - - auto_accept_item = self.listMenu.addAction(auto) - remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8)) - notes_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Notes', None, QtGui.QApplication.UnicodeUTF8)) - - submenu = plugin_support.PluginLoader.get_instance().get_menu(self.listMenu, num) - if len(submenu): - plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8)) - plug.addActions(submenu) - self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) - self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) - self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) - self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num)) - self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed)) - self.connect(notes_item, QtCore.SIGNAL("triggered()"), lambda: self.show_note(friend)) - self.connect(copy_name_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_name(friend)) - self.connect(copy_status_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_status(friend)) - parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) - self.listMenu.move(parent_position + pos) - self.listMenu.show() - - def show_note(self, friend): - s = Settings.get_instance() - note = s['notes'][friend.tox_id] if friend.tox_id in s['notes'] else '' - user = QtGui.QApplication.translate("MainWindow", 'Notes about user', None, QtGui.QApplication.UnicodeUTF8) - user = '{} {}'.format(user, friend.name) - - def save_note(text): - if friend.tox_id in s['notes']: - del s['notes'][friend.tox_id] - if text: - s['notes'][friend.tox_id] = text - s.save() - self.note = MultilineEdit(user, note, save_note) - self.note.show() - - def set_alias(self, num): - self.profile.set_alias(num) - - def remove_friend(self, num): - self.profile.delete_friend(num) - - def copy_friend_key(self, num): - tox_id = self.profile.friend_public_key(num) - clipboard = QtGui.QApplication.clipboard() - clipboard.setText(tox_id) - - def copy_name(self, friend): - clipboard = QtGui.QApplication.clipboard() - clipboard.setText(friend.name) - - def copy_status(self, friend): - clipboard = QtGui.QApplication.clipboard() - clipboard.setText(friend.status_message) - - def clear_history(self, num): - self.profile.clear_history(num) - - def auto_accept(self, num, value): - settings = Settings.get_instance() - tox_id = self.profile.friend_public_key(num) - if value: - settings['auto_accept_from_friends'].append(tox_id) - else: - settings['auto_accept_from_friends'].remove(tox_id) - settings.save() - - # ----------------------------------------------------------------------------------------------------------------- - # Functions which called when user click somewhere else - # ----------------------------------------------------------------------------------------------------------------- - - def friend_click(self, index): - num = index.row() - self.profile.set_active(num) - - def mouseReleaseEvent(self, event): - pos = self.connection_status.pos() - x, y = pos.x() + self.user_info.pos().x(), pos.y() + self.user_info.pos().y() - if (x < event.x() < x + 32) and (y < event.y() < y + 32): - self.profile.change_status() - else: - super(MainWindow, self).mouseReleaseEvent(event) - - def filtering(self): - self.profile.filtration(self.online_contacts.currentIndex() == 1, self.contact_name.text()) - diff --git a/toxygen/mainscreen_widgets.py b/toxygen/mainscreen_widgets.py deleted file mode 100644 index f12f521..0000000 --- a/toxygen/mainscreen_widgets.py +++ /dev/null @@ -1,378 +0,0 @@ -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget -from profile import Profile -import smileys -import util - - -class MessageArea(QtGui.QPlainTextEdit): - """User types messages here""" - - def __init__(self, parent, form): - super(MessageArea, self).__init__(parent) - self.parent = form - self.setAcceptDrops(True) - self.timer = QtCore.QTimer(self) - self.timer.timeout.connect(lambda: self.parent.profile.send_typing(False)) - - def keyPressEvent(self, event): - if event.matches(QtGui.QKeySequence.Paste): - self.pasteEvent() - elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): - modifiers = event.modifiers() - if modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier: - self.insertPlainText('\n') - else: - if self.timer.isActive(): - self.timer.stop() - self.parent.profile.send_typing(False) - self.parent.send_message() - elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): - self.appendPlainText(Profile.get_instance().get_last_message()) - else: - self.parent.profile.send_typing(True) - if self.timer.isActive(): - self.timer.stop() - self.timer.start(5000) - super(MessageArea, self).keyPressEvent(event) - - def contextMenuEvent(self, event): - menu = create_menu(self.createStandardContextMenu()) - menu.exec_(event.globalPos()) - del menu - - def dragEnterEvent(self, e): - e.accept() - - def dragMoveEvent(self, e): - e.accept() - - def dropEvent(self, e): - if e.mimeData().hasFormat('text/plain'): - e.accept() - self.pasteEvent(e.mimeData().text()) - else: - e.ignore() - - def pasteEvent(self, text=None): - text = text or QtGui.QApplication.clipboard().text() - if text.startswith('file://'): - self.parent.profile.send_file(text[7:]) - else: - self.insertPlainText(text) - - -class ScreenShotWindow(QtGui.QWidget): - - def __init__(self, parent): - super(ScreenShotWindow, self).__init__() - self.parent = parent - self.setMouseTracking(True) - self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) - self.showFullScreen() - self.setWindowOpacity(0.5) - self.rubberband = RubberBand() - - def closeEvent(self, *args): - if self.parent.isHidden(): - self.parent.show() - - def mousePressEvent(self, event): - self.origin = event.pos() - self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize())) - self.rubberband.show() - QtGui.QWidget.mousePressEvent(self, event) - - def mouseMoveEvent(self, event): - if self.rubberband.isVisible(): - self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized()) - left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height())) - right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height())) - top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y()) - bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height()) - self.setMask(left + right + top + bottom) - - def mouseReleaseEvent(self, event): - if self.rubberband.isVisible(): - self.rubberband.hide() - rect = self.rubberband.geometry() - if rect.width() and rect.height(): - p = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop().winId(), - rect.x() + 4, - rect.y() + 4, - rect.width() - 8, - rect.height() - 8) - byte_array = QtCore.QByteArray() - buffer = QtCore.QBuffer(byte_array) - buffer.open(QtCore.QIODevice.WriteOnly) - p.save(buffer, 'PNG') - Profile.get_instance().send_screenshot(bytes(byte_array.data())) - self.close() - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Escape: - self.rubberband.setHidden(True) - self.close() - else: - super(ScreenShotWindow, self).keyPressEvent(event) - - -class SmileyWindow(QtGui.QWidget): - """ - Smiley selection window - """ - - def __init__(self, parent): - super(SmileyWindow, self).__init__() - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - inst = smileys.SmileyLoader.get_instance() - self.data = inst.get_smileys() - count = len(self.data) - if not count: - self.close() - self.page_size = int(pow(count / 8, 0.5) + 1) * 8 # smileys per page - if count % self.page_size == 0: - self.page_count = count // self.page_size - else: - self.page_count = round(count / self.page_size + 0.5) - self.page = -1 - self.radio = [] - self.parent = parent - for i in range(self.page_count): # buttons with smileys - elem = QtGui.QRadioButton(self) - elem.setGeometry(QtCore.QRect(i * 20 + 5, 180, 20, 20)) - elem.clicked.connect(lambda i=i: self.checked(i)) - self.radio.append(elem) - width = max(self.page_count * 20 + 30, (self.page_size + 5) * 8 // 10) - self.setMaximumSize(width, 200) - self.setMinimumSize(width, 200) - self.buttons = [] - for i in range(self.page_size): # pages - radio buttons - b = QtGui.QPushButton(self) - b.setGeometry(QtCore.QRect((i // 8) * 20 + 5, (i % 8) * 20, 20, 20)) - b.clicked.connect(lambda i=i: self.clicked(i)) - self.buttons.append(b) - self.checked(0) - - def checked(self, pos): # new page opened - self.radio[self.page].setChecked(False) - self.radio[pos].setChecked(True) - self.page = pos - start = self.page * self.page_size - for i in range(self.page_size): - try: - self.buttons[i].setVisible(True) - pixmap = QtGui.QPixmap(self.data[start + i][1]) - icon = QtGui.QIcon(pixmap) - self.buttons[i].setIcon(icon) - except: - self.buttons[i].setVisible(False) - - def clicked(self, pos): # smiley selected - pos += self.page * self.page_size - smiley = self.data[pos][0] - self.parent.messageEdit.insertPlainText(smiley) - self.close() - - def leaveEvent(self, event): - self.close() - - -class MenuButton(QtGui.QPushButton): - - def __init__(self, parent, enter): - super(MenuButton, self).__init__(parent) - self.enter = enter - - def enterEvent(self, event): - self.enter() - super(MenuButton, self).enterEvent(event) - - -class DropdownMenu(QtGui.QWidget): - - def __init__(self, parent): - super(DropdownMenu, self).__init__(parent) - self.installEventFilter(self) - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - self.setMaximumSize(180, 120) - self.setMinimumSize(180, 120) - self.screenshotButton = QRightClickButton(self) - self.screenshotButton.setGeometry(QtCore.QRect(0, 60, 60, 60)) - self.screenshotButton.setObjectName("screenshotButton") - - self.fileTransferButton = QtGui.QPushButton(self) - self.fileTransferButton.setGeometry(QtCore.QRect(60, 60, 60, 60)) - self.fileTransferButton.setObjectName("fileTransferButton") - - self.audioMessageButton = QtGui.QPushButton(self) - self.audioMessageButton.setGeometry(QtCore.QRect(120, 60, 60, 60)) - - self.smileyButton = QtGui.QPushButton(self) - self.smileyButton.setGeometry(QtCore.QRect(0, 0, 60, 60)) - - self.videoMessageButton = QtGui.QPushButton(self) - self.videoMessageButton.setGeometry(QtCore.QRect(120, 0, 60, 60)) - - self.stickerButton = QtGui.QPushButton(self) - self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60)) - - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/file.png') - icon = QtGui.QIcon(pixmap) - self.fileTransferButton.setIcon(icon) - self.fileTransferButton.setIconSize(QtCore.QSize(50, 50)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/screenshot.png') - icon = QtGui.QIcon(pixmap) - self.screenshotButton.setIcon(icon) - self.screenshotButton.setIconSize(QtCore.QSize(50, 60)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/audio_message.png') - icon = QtGui.QIcon(pixmap) - self.audioMessageButton.setIcon(icon) - self.audioMessageButton.setIconSize(QtCore.QSize(50, 50)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/smiley.png') - icon = QtGui.QIcon(pixmap) - self.smileyButton.setIcon(icon) - self.smileyButton.setIconSize(QtCore.QSize(50, 50)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/video_message.png') - icon = QtGui.QIcon(pixmap) - self.videoMessageButton.setIcon(icon) - self.videoMessageButton.setIconSize(QtCore.QSize(55, 55)) - pixmap = QtGui.QPixmap(util.curr_directory() + '/images/sticker.png') - icon = QtGui.QIcon(pixmap) - self.stickerButton.setIcon(icon) - self.stickerButton.setIconSize(QtCore.QSize(55, 55)) - - self.screenshotButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send screenshot", None, QtGui.QApplication.UnicodeUTF8)) - self.fileTransferButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send file", None, QtGui.QApplication.UnicodeUTF8)) - self.audioMessageButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send audio message", None, QtGui.QApplication.UnicodeUTF8)) - self.videoMessageButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send video message", None, QtGui.QApplication.UnicodeUTF8)) - self.smileyButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Add smiley", None, QtGui.QApplication.UnicodeUTF8)) - self.stickerButton.setToolTip(QtGui.QApplication.translate("MenuWindow", "Send sticker", None, QtGui.QApplication.UnicodeUTF8)) - - self.fileTransferButton.clicked.connect(parent.send_file) - self.screenshotButton.clicked.connect(parent.send_screenshot) - self.connect(self.screenshotButton, QtCore.SIGNAL("rightClicked()"), lambda: parent.send_screenshot(True)) - self.smileyButton.clicked.connect(parent.send_smiley) - self.stickerButton.clicked.connect(parent.send_sticker) - - def leaveEvent(self, event): - self.close() - - def eventFilter(self, object, event): - if event.type() == QtCore.QEvent.WindowDeactivate: - self.close() - return False - - -class StickerItem(QtGui.QWidget): - - def __init__(self, fl): - super(StickerItem, self).__init__() - self._image_label = QtGui.QLabel(self) - self.path = fl - self.pixmap = QtGui.QPixmap() - self.pixmap.load(fl) - if self.pixmap.width() > 150: - self.pixmap = self.pixmap.scaled(150, 200, QtCore.Qt.KeepAspectRatio) - self.setFixedSize(150, self.pixmap.height()) - self._image_label.setPixmap(self.pixmap) - - -class StickerWindow(QtGui.QWidget): - """Sticker selection window""" - - def __init__(self, parent): - super(StickerWindow, self).__init__() - self.setWindowFlags(QtCore.Qt.FramelessWindowHint) - self.setMaximumSize(250, 200) - self.setMinimumSize(250, 200) - self.list = QtGui.QListWidget(self) - self.list.setGeometry(QtCore.QRect(0, 0, 250, 200)) - self.arr = smileys.sticker_loader() - for sticker in self.arr: - item = StickerItem(sticker) - elem = QtGui.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(250, item.height())) - self.list.addItem(elem) - self.list.setItemWidget(elem, item) - self.list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) - self.list.setSpacing(3) - self.list.clicked.connect(self.click) - self.parent = parent - - def click(self, index): - num = index.row() - self.parent.profile.send_sticker(self.arr[num]) - self.close() - - def leaveEvent(self, event): - self.close() - - -class WelcomeScreen(CenteredWidget): - - def __init__(self): - super().__init__() - self.setMaximumSize(250, 200) - self.setMinimumSize(250, 200) - self.center() - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.text = QtGui.QTextBrowser(self) - self.text.setGeometry(QtCore.QRect(0, 0, 250, 170)) - self.text.setOpenExternalLinks(True) - self.checkbox = QtGui.QCheckBox(self) - self.checkbox.setGeometry(QtCore.QRect(5, 170, 240, 30)) - self.checkbox.setText(QtGui.QApplication.translate('WelcomeScreen', "Don't show again", - None, QtGui.QApplication.UnicodeUTF8)) - self.setWindowTitle(QtGui.QApplication.translate('WelcomeScreen', 'Tip of the day', - None, QtGui.QApplication.UnicodeUTF8)) - import random - num = random.randint(0, 8) - if num == 0: - text = QtGui.QApplication.translate('WelcomeScreen', 'Press Esc if you want hide app to tray.', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 1: - text = QtGui.QApplication.translate('WelcomeScreen', - 'Right click on screenshot button hides app to tray during screenshot.', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 2: - text = QtGui.QApplication.translate('WelcomeScreen', - 'You can use Tox over Tor. For more info read this post', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 3: - text = QtGui.QApplication.translate('WelcomeScreen', - 'Use Settings -> Interface to customize interface.', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 4: - text = QtGui.QApplication.translate('WelcomeScreen', - 'Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 5: - text = QtGui.QApplication.translate('WelcomeScreen', - 'Since v0.1.3 Toxygen supports plugins. Read more', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 6: - text = QtGui.QApplication.translate('WelcomeScreen', - 'New in Toxygen v0.2.2:
Users can lock application using profile password.
Compact contact list support
Bug fixes
Tox DNS improvements', - None, QtGui.QApplication.UnicodeUTF8) - elif num == 7: - text = QtGui.QApplication.translate('WelcomeScreen', - 'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.', - None, QtGui.QApplication.UnicodeUTF8) - else: - text = QtGui.QApplication.translate('WelcomeScreen', - 'Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.', - None, QtGui.QApplication.UnicodeUTF8) - self.text.setHtml(text) - self.checkbox.stateChanged.connect(self.not_show) - QtCore.QTimer.singleShot(1000, self.show) - - def not_show(self): - import settings - s = settings.Settings.get_instance() - s['show_welcome_screen'] = False - s.save() - diff --git a/toxygen/menu.py b/toxygen/menu.py deleted file mode 100644 index a1c39ed..0000000 --- a/toxygen/menu.py +++ /dev/null @@ -1,852 +0,0 @@ -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from settings import * -from profile import Profile -from util import curr_directory, copy -from widgets import CenteredWidget, DataLabel, LineEdit -import pyaudio -import toxencryptsave -import plugin_support - - -class AddContact(CenteredWidget): - """Add contact form""" - - def __init__(self, tox_id=''): - super(AddContact, self).__init__() - self.initUI(tox_id) - self._adding = False - - def initUI(self, tox_id): - self.setObjectName('AddContact') - self.resize(568, 306) - self.sendRequestButton = QtGui.QPushButton(self) - self.sendRequestButton.setGeometry(QtCore.QRect(50, 270, 471, 31)) - self.sendRequestButton.setMinimumSize(QtCore.QSize(0, 0)) - self.sendRequestButton.setBaseSize(QtCore.QSize(0, 0)) - self.sendRequestButton.setObjectName("sendRequestButton") - self.sendRequestButton.clicked.connect(self.add_friend) - self.tox_id = LineEdit(self) - self.tox_id.setGeometry(QtCore.QRect(50, 40, 471, 27)) - self.tox_id.setObjectName("lineEdit") - self.tox_id.setText(tox_id) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(50, 10, 80, 20)) - self.error_label = DataLabel(self) - self.error_label.setGeometry(QtCore.QRect(120, 10, 420, 20)) - font = QtGui.QFont() - font.setPointSize(10) - font.setWeight(30) - self.error_label.setFont(font) - self.error_label.setStyleSheet("QLabel { color: #BC1C1C; }") - self.label.setObjectName("label") - self.message_edit = QtGui.QTextEdit(self) - self.message_edit.setGeometry(QtCore.QRect(50, 110, 471, 151)) - self.message_edit.setObjectName("textEdit") - self.message = QtGui.QLabel(self) - self.message.setGeometry(QtCore.QRect(50, 70, 101, 31)) - self.message.setFont(font) - self.message.setObjectName("label_2") - self.retranslateUi() - self.message_edit.setText('Hello! Add me to your contact list please') - font = QtGui.QFont() - font.setPointSize(12) - font.setBold(True) - self.label.setFont(font) - self.message.setFont(font) - QtCore.QMetaObject.connectSlotsByName(self) - - def add_friend(self): - if self._adding: - return - self._adding = True - profile = Profile.get_instance() - send = profile.send_friend_request(self.tox_id.text().strip(), self.message_edit.toPlainText()) - self._adding = False - if send is True: - # request was successful - self.close() - else: # print error data - self.error_label.setText(send) - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate('AddContact', "Add contact", None, QtGui.QApplication.UnicodeUTF8)) - self.sendRequestButton.setText(QtGui.QApplication.translate("Form", "Send request", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate('AddContact', "TOX ID:", None, QtGui.QApplication.UnicodeUTF8)) - self.message.setText(QtGui.QApplication.translate('AddContact', "Message:", None, QtGui.QApplication.UnicodeUTF8)) - self.tox_id.setPlaceholderText(QtGui.QApplication.translate('AddContact', "TOX ID or public key of contact", None, QtGui.QApplication.UnicodeUTF8)) - - -class ProfileSettings(CenteredWidget): - """Form with profile settings such as name, status, TOX ID""" - def __init__(self): - super(ProfileSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("ProfileSettingsForm") - self.setMinimumSize(QtCore.QSize(700, 600)) - self.setMaximumSize(QtCore.QSize(700, 600)) - self.nick = LineEdit(self) - self.nick.setGeometry(QtCore.QRect(30, 60, 350, 27)) - profile = Profile.get_instance() - self.nick.setText(profile.name) - self.status = QtGui.QComboBox(self) - self.status.setGeometry(QtCore.QRect(400, 60, 200, 27)) - self.status_message = LineEdit(self) - self.status_message.setGeometry(QtCore.QRect(30, 130, 350, 27)) - self.status_message.setText(profile.status_message) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(40, 30, 91, 25)) - font = QtGui.QFont() - font.setPointSize(18) - font.setWeight(75) - font.setBold(True) - self.label.setFont(font) - self.label_2 = QtGui.QLabel(self) - self.label_2.setGeometry(QtCore.QRect(40, 100, 100, 25)) - self.label_2.setFont(font) - self.label_3 = QtGui.QLabel(self) - self.label_3.setGeometry(QtCore.QRect(40, 180, 100, 25)) - self.label_3.setFont(font) - self.tox_id = QtGui.QLabel(self) - self.tox_id.setGeometry(QtCore.QRect(15, 210, 685, 21)) - font.setPointSize(10) - self.tox_id.setFont(font) - s = profile.tox_id - self.tox_id.setText(s) - self.copyId = QtGui.QPushButton(self) - self.copyId.setGeometry(QtCore.QRect(40, 250, 180, 30)) - self.copyId.clicked.connect(self.copy) - self.export = QtGui.QPushButton(self) - self.export.setGeometry(QtCore.QRect(230, 250, 180, 30)) - self.export.clicked.connect(self.export_profile) - self.new_nospam = QtGui.QPushButton(self) - self.new_nospam.setGeometry(QtCore.QRect(420, 250, 180, 30)) - self.new_nospam.clicked.connect(self.new_no_spam) - self.copy_pk = QtGui.QPushButton(self) - self.copy_pk.setGeometry(QtCore.QRect(40, 300, 180, 30)) - self.copy_pk.clicked.connect(self.copy_public_key) - self.new_avatar = QtGui.QPushButton(self) - self.new_avatar.setGeometry(QtCore.QRect(230, 300, 180, 30)) - self.delete_avatar = QtGui.QPushButton(self) - self.delete_avatar.setGeometry(QtCore.QRect(420, 300, 180, 30)) - self.delete_avatar.clicked.connect(self.reset_avatar) - self.new_avatar.clicked.connect(self.set_avatar) - self.profilepass = QtGui.QLabel(self) - self.profilepass.setGeometry(QtCore.QRect(40, 340, 300, 30)) - font.setPointSize(18) - self.profilepass.setFont(font) - self.password = LineEdit(self) - self.password.setGeometry(QtCore.QRect(40, 380, 300, 30)) - self.password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) - self.leave_blank = QtGui.QLabel(self) - self.leave_blank.setGeometry(QtCore.QRect(350, 380, 300, 30)) - self.confirm_password = LineEdit(self) - self.confirm_password.setGeometry(QtCore.QRect(40, 420, 300, 30)) - self.confirm_password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) - self.set_password = QtGui.QPushButton(self) - self.set_password.setGeometry(QtCore.QRect(40, 470, 300, 30)) - self.set_password.clicked.connect(self.new_password) - self.not_match = QtGui.QLabel(self) - self.not_match.setGeometry(QtCore.QRect(350, 420, 300, 30)) - self.not_match.setVisible(False) - self.not_match.setStyleSheet('QLabel { color: #BC1C1C; }') - self.warning = QtGui.QLabel(self) - self.warning.setGeometry(QtCore.QRect(40, 510, 500, 30)) - self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') - self.default = QtGui.QPushButton(self) - self.default.setGeometry(QtCore.QRect(40, 550, 620, 30)) - path, name = Settings.get_auto_profile() - self.auto = path + name == ProfileHelper.get_path() + Settings.get_instance().name - self.default.clicked.connect(self.auto_profile) - self.retranslateUi() - if profile.status is not None: - self.status.setCurrentIndex(profile.status) - else: - self.status.setVisible(False) - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.export.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Export profile", None, QtGui.QApplication.UnicodeUTF8)) - self.setWindowTitle(QtGui.QApplication.translate("ProfileSettingsForm", "Profile settings", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Name:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Status:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_3.setText(QtGui.QApplication.translate("ProfileSettingsForm", "TOX ID:", None, QtGui.QApplication.UnicodeUTF8)) - self.copyId.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Copy TOX ID", None, QtGui.QApplication.UnicodeUTF8)) - self.new_avatar.setText(QtGui.QApplication.translate("ProfileSettingsForm", "New avatar", None, QtGui.QApplication.UnicodeUTF8)) - self.delete_avatar.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Reset avatar", None, QtGui.QApplication.UnicodeUTF8)) - self.new_nospam.setText(QtGui.QApplication.translate("ProfileSettingsForm", "New NoSpam", None, QtGui.QApplication.UnicodeUTF8)) - self.profilepass.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Profile password", None, QtGui.QApplication.UnicodeUTF8)) - self.password.setPlaceholderText(QtGui.QApplication.translate("ProfileSettingsForm", "Password (at least 8 symbols)", None, QtGui.QApplication.UnicodeUTF8)) - self.confirm_password.setPlaceholderText(QtGui.QApplication.translate("ProfileSettingsForm", "Confirm password", None, QtGui.QApplication.UnicodeUTF8)) - self.set_password.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Set password", None, QtGui.QApplication.UnicodeUTF8)) - self.not_match.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Passwords do not match", None, QtGui.QApplication.UnicodeUTF8)) - self.leave_blank.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Leaving blank will reset current password", None, QtGui.QApplication.UnicodeUTF8)) - self.warning.setText(QtGui.QApplication.translate("ProfileSettingsForm", "There is no way to recover lost passwords", None, QtGui.QApplication.UnicodeUTF8)) - self.status.addItem(QtGui.QApplication.translate("ProfileSettingsForm", "Online", None, QtGui.QApplication.UnicodeUTF8)) - self.status.addItem(QtGui.QApplication.translate("ProfileSettingsForm", "Away", None, QtGui.QApplication.UnicodeUTF8)) - self.status.addItem(QtGui.QApplication.translate("ProfileSettingsForm", "Busy", None, QtGui.QApplication.UnicodeUTF8)) - self.copy_pk.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Copy public key", None, QtGui.QApplication.UnicodeUTF8)) - if self.auto: - self.default.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Mark as not default profile", None, QtGui.QApplication.UnicodeUTF8)) - else: - self.default.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Mark as default profile", None, QtGui.QApplication.UnicodeUTF8)) - - def auto_profile(self): - if self.auto: - Settings.reset_auto_profile() - else: - Settings.set_auto_profile(ProfileHelper.get_path(), Settings.get_instance().name) - self.auto = not self.auto - if self.auto: - self.default.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Mark as not default profile", None, - QtGui.QApplication.UnicodeUTF8)) - else: - self.default.setText( - QtGui.QApplication.translate("ProfileSettingsForm", "Mark as default profile", None, - QtGui.QApplication.UnicodeUTF8)) - - def new_password(self): - if self.password.text() == self.confirm_password.text(): - if not len(self.password.text()) or len(self.password.text()) >= 8: - e = toxencryptsave.ToxEncryptSave.get_instance() - e.set_password(self.password.text()) - self.close() - else: - self.not_match.setText( - QtGui.QApplication.translate("ProfileSettingsForm", "Password must be at least 8 symbols", None, - QtGui.QApplication.UnicodeUTF8)) - self.not_match.setVisible(True) - else: - self.not_match.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Passwords do not match", None, - QtGui.QApplication.UnicodeUTF8)) - self.not_match.setVisible(True) - - def copy(self): - clipboard = QtGui.QApplication.clipboard() - profile = Profile.get_instance() - clipboard.setText(profile.tox_id) - pixmap = QtGui.QPixmap(curr_directory() + '/images/accept.png') - icon = QtGui.QIcon(pixmap) - self.copyId.setIcon(icon) - self.copyId.setIconSize(QtCore.QSize(10, 10)) - - def copy_public_key(self): - clipboard = QtGui.QApplication.clipboard() - profile = Profile.get_instance() - clipboard.setText(profile.tox_id[:64]) - pixmap = QtGui.QPixmap(curr_directory() + '/images/accept.png') - icon = QtGui.QIcon(pixmap) - self.copy_pk.setIcon(icon) - self.copy_pk.setIconSize(QtCore.QSize(10, 10)) - - def new_no_spam(self): - self.tox_id.setText(Profile.get_instance().new_nospam()) - - def reset_avatar(self): - Profile.get_instance().reset_avatar() - - def set_avatar(self): - choose = QtGui.QApplication.translate("ProfileSettingsForm", "Choose avatar", None, QtGui.QApplication.UnicodeUTF8) - name = QtGui.QFileDialog.getOpenFileName(self, choose, None, 'Images (*.png)', - options=QtGui.QFileDialog.DontUseNativeDialog) - if name[0]: - bitmap = QtGui.QPixmap(name[0]) - bitmap.scaled(QtCore.QSize(128, 128), aspectMode=QtCore.Qt.KeepAspectRatio, - mode=QtCore.Qt.SmoothTransformation) - - byte_array = QtCore.QByteArray() - buffer = QtCore.QBuffer(byte_array) - buffer.open(QtCore.QIODevice.WriteOnly) - bitmap.save(buffer, 'PNG') - Profile.get_instance().set_avatar(str(byte_array.data())) - - def export_profile(self): - directory = QtGui.QFileDialog.getExistingDirectory(options=QtGui.QFileDialog.DontUseNativeDialog) + '/' - if directory != '/': - ProfileHelper.get_instance().export_profile(directory) - settings = Settings.get_instance() - settings.export(directory) - profile = Profile.get_instance() - profile.export_history(directory) - - def closeEvent(self, event): - profile = Profile.get_instance() - profile.set_name(self.nick.text()) - profile.set_status_message(self.status_message.text().encode('utf-8')) - profile.set_status(self.status.currentIndex()) - - -class NetworkSettings(CenteredWidget): - """Network settings form: UDP, Ipv6 and proxy""" - def __init__(self, reset): - super(NetworkSettings, self).__init__() - self.reset = reset - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("NetworkSettings") - self.resize(300, 330) - self.setMinimumSize(QtCore.QSize(300, 330)) - self.setMaximumSize(QtCore.QSize(300, 330)) - self.setBaseSize(QtCore.QSize(300, 330)) - self.ipv = QtGui.QCheckBox(self) - self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22)) - self.ipv.setObjectName("ipv") - self.udp = QtGui.QCheckBox(self) - self.udp.setGeometry(QtCore.QRect(150, 10, 97, 22)) - self.udp.setObjectName("udp") - self.proxy = QtGui.QCheckBox(self) - self.proxy.setGeometry(QtCore.QRect(20, 40, 97, 22)) - self.http = QtGui.QCheckBox(self) - self.http.setGeometry(QtCore.QRect(20, 70, 97, 22)) - self.proxy.setObjectName("proxy") - self.proxyip = LineEdit(self) - self.proxyip.setGeometry(QtCore.QRect(40, 130, 231, 27)) - self.proxyip.setObjectName("proxyip") - self.proxyport = LineEdit(self) - self.proxyport.setGeometry(QtCore.QRect(40, 190, 231, 27)) - self.proxyport.setObjectName("proxyport") - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(40, 100, 66, 17)) - self.label_2 = QtGui.QLabel(self) - self.label_2.setGeometry(QtCore.QRect(40, 165, 66, 17)) - self.reconnect = QtGui.QPushButton(self) - self.reconnect.setGeometry(QtCore.QRect(40, 230, 231, 30)) - self.reconnect.clicked.connect(self.restart_core) - settings = Settings.get_instance() - self.ipv.setChecked(settings['ipv6_enabled']) - self.udp.setChecked(settings['udp_enabled']) - self.proxy.setChecked(settings['proxy_type']) - self.proxyip.setText(settings['proxy_host']) - self.proxyport.setText(str(settings['proxy_port'])) - self.http.setChecked(settings['proxy_type'] == 1) - self.warning = QtGui.QLabel(self) - self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60)) - self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') - self.retranslateUi() - self.proxy.stateChanged.connect(lambda x: self.activate()) - self.activate() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("NetworkSettings", "Network settings", None, QtGui.QApplication.UnicodeUTF8)) - self.ipv.setText(QtGui.QApplication.translate("Form", "IPv6", None, QtGui.QApplication.UnicodeUTF8)) - self.udp.setText(QtGui.QApplication.translate("Form", "UDP", None, QtGui.QApplication.UnicodeUTF8)) - self.proxy.setText(QtGui.QApplication.translate("Form", "Proxy", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "IP:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("Form", "Port:", None, QtGui.QApplication.UnicodeUTF8)) - self.reconnect.setText(QtGui.QApplication.translate("NetworkSettings", "Restart TOX core", None, QtGui.QApplication.UnicodeUTF8)) - self.http.setText(QtGui.QApplication.translate("Form", "HTTP", None, QtGui.QApplication.UnicodeUTF8)) - self.warning.setText(QtGui.QApplication.translate("Form", "WARNING:\nusing proxy with enabled UDP\ncan produce IP leak", - None, QtGui.QApplication.UnicodeUTF8)) - - def activate(self): - bl = self.proxy.isChecked() - self.proxyip.setEnabled(bl) - self.http.setEnabled(bl) - self.proxyport.setEnabled(bl) - - def restart_core(self): - try: - settings = Settings.get_instance() - settings['ipv6_enabled'] = self.ipv.isChecked() - settings['udp_enabled'] = self.udp.isChecked() - settings['proxy_type'] = 2 - int(self.http.isChecked()) if self.proxy.isChecked() else 0 - settings['proxy_host'] = str(self.proxyip.text()) - settings['proxy_port'] = int(self.proxyport.text()) - settings.save() - # recreate tox instance - Profile.get_instance().reset(self.reset) - self.close() - except Exception as ex: - log('Exception in restart: ' + str(ex)) - - -class PrivacySettings(CenteredWidget): - """Privacy settings form: history, typing notifications""" - - def __init__(self): - super(PrivacySettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("privacySettings") - self.resize(370, 600) - self.setMinimumSize(QtCore.QSize(370, 600)) - self.setMaximumSize(QtCore.QSize(370, 600)) - self.saveHistory = QtGui.QCheckBox(self) - self.saveHistory.setGeometry(QtCore.QRect(10, 20, 350, 22)) - self.saveUnsentOnly = QtGui.QCheckBox(self) - self.saveUnsentOnly.setGeometry(QtCore.QRect(10, 60, 350, 22)) - - self.fileautoaccept = QtGui.QCheckBox(self) - self.fileautoaccept.setGeometry(QtCore.QRect(10, 100, 350, 22)) - - self.typingNotifications = QtGui.QCheckBox(self) - self.typingNotifications.setGeometry(QtCore.QRect(10, 140, 350, 30)) - self.inlines = QtGui.QCheckBox(self) - self.inlines.setGeometry(QtCore.QRect(10, 180, 350, 30)) - self.auto_path = QtGui.QLabel(self) - self.auto_path.setGeometry(QtCore.QRect(10, 230, 350, 30)) - self.path = QtGui.QPlainTextEdit(self) - self.path.setGeometry(QtCore.QRect(10, 265, 350, 45)) - self.change_path = QtGui.QPushButton(self) - self.change_path.setGeometry(QtCore.QRect(10, 320, 350, 30)) - settings = Settings.get_instance() - self.typingNotifications.setChecked(settings['typing_notifications']) - self.fileautoaccept.setChecked(settings['allow_auto_accept']) - self.saveHistory.setChecked(settings['save_history']) - self.inlines.setChecked(settings['allow_inline']) - self.saveUnsentOnly.setChecked(settings['save_unsent_only']) - self.saveUnsentOnly.setEnabled(settings['save_history']) - self.saveHistory.stateChanged.connect(self.update) - self.path.setPlainText(settings['auto_accept_path'] or curr_directory()) - self.change_path.clicked.connect(self.new_path) - self.block_user_label = QtGui.QLabel(self) - self.block_user_label.setGeometry(QtCore.QRect(10, 360, 350, 30)) - self.block_id = QtGui.QPlainTextEdit(self) - self.block_id.setGeometry(QtCore.QRect(10, 390, 350, 30)) - self.block = QtGui.QPushButton(self) - self.block.setGeometry(QtCore.QRect(10, 430, 350, 30)) - self.block.clicked.connect(lambda: Profile.get_instance().block_user(self.block_id.toPlainText()) or self.close()) - self.blocked_users_label = QtGui.QLabel(self) - self.blocked_users_label.setGeometry(QtCore.QRect(10, 470, 350, 30)) - self.comboBox = QtGui.QComboBox(self) - self.comboBox.setGeometry(QtCore.QRect(10, 500, 350, 30)) - self.comboBox.addItems(settings['blocked']) - self.unblock = QtGui.QPushButton(self) - self.unblock.setGeometry(QtCore.QRect(10, 540, 350, 30)) - self.unblock.clicked.connect(lambda: self.unblock_user()) - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("privacySettings", "Privacy settings", None, QtGui.QApplication.UnicodeUTF8)) - self.saveHistory.setText(QtGui.QApplication.translate("privacySettings", "Save chat history", None, QtGui.QApplication.UnicodeUTF8)) - self.fileautoaccept.setText(QtGui.QApplication.translate("privacySettings", "Allow file auto accept", None, QtGui.QApplication.UnicodeUTF8)) - self.typingNotifications.setText(QtGui.QApplication.translate("privacySettings", "Send typing notifications", None, QtGui.QApplication.UnicodeUTF8)) - self.auto_path.setText(QtGui.QApplication.translate("privacySettings", "Auto accept default path:", None, QtGui.QApplication.UnicodeUTF8)) - self.change_path.setText(QtGui.QApplication.translate("privacySettings", "Change", None, QtGui.QApplication.UnicodeUTF8)) - self.inlines.setText(QtGui.QApplication.translate("privacySettings", "Allow inlines", None, QtGui.QApplication.UnicodeUTF8)) - self.block_user_label.setText(QtGui.QApplication.translate("privacySettings", "Block by public key:", None, QtGui.QApplication.UnicodeUTF8)) - self.blocked_users_label.setText(QtGui.QApplication.translate("privacySettings", "Blocked users:", None, QtGui.QApplication.UnicodeUTF8)) - self.unblock.setText(QtGui.QApplication.translate("privacySettings", "Unblock", None, QtGui.QApplication.UnicodeUTF8)) - self.block.setText(QtGui.QApplication.translate("privacySettings", "Block user", None, QtGui.QApplication.UnicodeUTF8)) - self.saveUnsentOnly.setText(QtGui.QApplication.translate("privacySettings", "Save unsent messages only", None, QtGui.QApplication.UnicodeUTF8)) - - def update(self, new_state): - self.saveUnsentOnly.setEnabled(new_state) - if not new_state: - self.saveUnsentOnly.setChecked(False) - - def unblock_user(self): - if not self.comboBox.count(): - return - title = QtGui.QApplication.translate("privacySettings", "Add to friend list", None, QtGui.QApplication.UnicodeUTF8) - info = QtGui.QApplication.translate("privacySettings", "Do you want to add this user to friend list?", None, QtGui.QApplication.UnicodeUTF8) - reply = QtGui.QMessageBox.question(None, title, info, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) - Profile.get_instance().unblock_user(self.comboBox.currentText(), reply == QtGui.QMessageBox.Yes) - self.close() - - def closeEvent(self, event): - settings = Settings.get_instance() - settings['typing_notifications'] = self.typingNotifications.isChecked() - settings['allow_auto_accept'] = self.fileautoaccept.isChecked() - - if settings['save_history'] and not self.saveHistory.isChecked(): # clear history - reply = QtGui.QMessageBox.question(None, - QtGui.QApplication.translate("privacySettings", - 'Chat history', - None, QtGui.QApplication.UnicodeUTF8), - QtGui.QApplication.translate("privacySettings", - 'History will be cleaned! Continue?', - None, QtGui.QApplication.UnicodeUTF8), - QtGui.QMessageBox.Yes, - QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: - Profile.get_instance().clear_history() - settings['save_history'] = self.saveHistory.isChecked() - else: - settings['save_history'] = self.saveHistory.isChecked() - if self.saveUnsentOnly.isChecked() and not settings['save_unsent_only']: - reply = QtGui.QMessageBox.question(None, - QtGui.QApplication.translate("privacySettings", - 'Chat history', - None, QtGui.QApplication.UnicodeUTF8), - QtGui.QApplication.translate("privacySettings", - 'History will be cleaned! Continue?', - None, QtGui.QApplication.UnicodeUTF8), - QtGui.QMessageBox.Yes, - QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: - Profile.get_instance().clear_history(None, True) - settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() - else: - settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() - settings['auto_accept_path'] = self.path.toPlainText() - settings['allow_inline'] = self.inlines.isChecked() - settings.save() - - def new_path(self): - directory = QtGui.QFileDialog.getExistingDirectory(options=QtGui.QFileDialog.DontUseNativeDialog) + '/' - if directory != '/': - self.path.setPlainText(directory) - - -class NotificationsSettings(CenteredWidget): - """Notifications settings form""" - - def __init__(self): - super(NotificationsSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("notificationsForm") - self.resize(350, 180) - self.setMinimumSize(QtCore.QSize(350, 180)) - self.setMaximumSize(QtCore.QSize(350, 180)) - self.enableNotifications = QtGui.QCheckBox(self) - self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18)) - self.callsSound = QtGui.QCheckBox(self) - self.callsSound.setGeometry(QtCore.QRect(10, 120, 340, 18)) - self.soundNotifications = QtGui.QCheckBox(self) - self.soundNotifications.setGeometry(QtCore.QRect(10, 70, 340, 18)) - font = QtGui.QFont() - font.setPointSize(12) - self.callsSound.setFont(font) - self.soundNotifications.setFont(font) - self.enableNotifications.setFont(font) - s = Settings.get_instance() - self.enableNotifications.setChecked(s['notifications']) - self.soundNotifications.setChecked(s['sound_notifications']) - self.callsSound.setChecked(s['calls_sound']) - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("notificationsForm", "Notification settings", None, QtGui.QApplication.UnicodeUTF8)) - self.enableNotifications.setText(QtGui.QApplication.translate("notificationsForm", "Enable notifications", None, QtGui.QApplication.UnicodeUTF8)) - self.callsSound.setText(QtGui.QApplication.translate("notificationsForm", "Enable call\'s sound", None, QtGui.QApplication.UnicodeUTF8)) - self.soundNotifications.setText(QtGui.QApplication.translate("notificationsForm", "Enable sound notifications", None, QtGui.QApplication.UnicodeUTF8)) - - def closeEvent(self, *args, **kwargs): - settings = Settings.get_instance() - settings['notifications'] = self.enableNotifications.isChecked() - settings['sound_notifications'] = self.soundNotifications.isChecked() - settings['calls_sound'] = self.callsSound.isChecked() - settings.save() - - -class InterfaceSettings(CenteredWidget): - """Interface settings form""" - def __init__(self): - super(InterfaceSettings, self).__init__() - self.initUI() - self.center() - - def initUI(self): - self.setObjectName("interfaceForm") - self.setMinimumSize(QtCore.QSize(400, 550)) - self.setMaximumSize(QtCore.QSize(400, 550)) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(30, 10, 370, 20)) - font = QtGui.QFont() - font.setPointSize(14) - font.setBold(True) - self.label.setFont(font) - self.themeSelect = QtGui.QComboBox(self) - self.themeSelect.setGeometry(QtCore.QRect(30, 40, 120, 30)) - list_of_themes = ['dark'] - self.themeSelect.addItems(list_of_themes) - settings = Settings.get_instance() - theme = settings['theme'] - if theme in list_of_themes: - index = list_of_themes.index(theme) - else: - index = 0 - self.themeSelect.setCurrentIndex(index) - self.lang_choose = QtGui.QComboBox(self) - self.lang_choose.setGeometry(QtCore.QRect(30, 110, 120, 30)) - supported = sorted(Settings.supported_languages().keys(), reverse=True) - for key in supported: - self.lang_choose.insertItem(0, key) - if settings['language'] == key: - self.lang_choose.setCurrentIndex(0) - self.lang = QtGui.QLabel(self) - self.lang.setGeometry(QtCore.QRect(30, 80, 370, 20)) - self.lang.setFont(font) - self.mirror_mode = QtGui.QCheckBox(self) - self.mirror_mode.setGeometry(QtCore.QRect(30, 160, 370, 20)) - self.mirror_mode.setChecked(settings['mirror_mode']) - self.smileys = QtGui.QCheckBox(self) - self.smileys.setGeometry(QtCore.QRect(30, 190, 120, 20)) - self.smileys.setChecked(settings['smileys']) - self.smiley_pack_label = QtGui.QLabel(self) - self.smiley_pack_label.setGeometry(QtCore.QRect(30, 230, 370, 20)) - self.smiley_pack_label.setFont(font) - self.smiley_pack = QtGui.QComboBox(self) - self.smiley_pack.setGeometry(QtCore.QRect(30, 260, 160, 30)) - sm = smileys.SmileyLoader.get_instance() - self.smiley_pack.addItems(sm.get_packs_list()) - try: - ind = sm.get_packs_list().index(settings['smiley_pack']) - except: - ind = sm.get_packs_list().index('default') - self.smiley_pack.setCurrentIndex(ind) - self.messages_font_size_label = QtGui.QLabel(self) - self.messages_font_size_label.setGeometry(QtCore.QRect(30, 300, 370, 20)) - self.messages_font_size_label.setFont(font) - self.messages_font_size = QtGui.QComboBox(self) - self.messages_font_size.setGeometry(QtCore.QRect(30, 330, 160, 30)) - self.messages_font_size.addItems([str(x) for x in range(10, 19)]) - self.messages_font_size.setCurrentIndex(settings['message_font_size'] - 10) - - self.unread = QtGui.QPushButton(self) - self.unread.setGeometry(QtCore.QRect(30, 425, 340, 30)) - self.unread.clicked.connect(self.select_color) - - self.compact_mode = QtGui.QCheckBox(self) - self.compact_mode.setGeometry(QtCore.QRect(30, 380, 370, 20)) - self.compact_mode.setChecked(settings['compact_mode']) - - self.import_smileys = QtGui.QPushButton(self) - self.import_smileys.setGeometry(QtCore.QRect(30, 465, 340, 30)) - self.import_smileys.clicked.connect(self.import_sm) - - self.import_stickers = QtGui.QPushButton(self) - self.import_stickers.setGeometry(QtCore.QRect(30, 505, 340, 30)) - self.import_stickers.clicked.connect(self.import_st) - - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("interfaceForm", "Interface settings", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("interfaceForm", "Theme:", None, QtGui.QApplication.UnicodeUTF8)) - self.lang.setText(QtGui.QApplication.translate("interfaceForm", "Language:", None, QtGui.QApplication.UnicodeUTF8)) - self.smileys.setText(QtGui.QApplication.translate("interfaceForm", "Smileys", None, QtGui.QApplication.UnicodeUTF8)) - self.smiley_pack_label.setText(QtGui.QApplication.translate("interfaceForm", "Smiley pack:", None, QtGui.QApplication.UnicodeUTF8)) - self.mirror_mode.setText(QtGui.QApplication.translate("interfaceForm", "Mirror mode", None, QtGui.QApplication.UnicodeUTF8)) - self.messages_font_size_label.setText(QtGui.QApplication.translate("interfaceForm", "Messages font size:", None, QtGui.QApplication.UnicodeUTF8)) - self.unread.setText(QtGui.QApplication.translate("interfaceForm", "Select unread messages notification color", None, QtGui.QApplication.UnicodeUTF8)) - self.compact_mode.setText(QtGui.QApplication.translate("interfaceForm", "Compact contact list", None, QtGui.QApplication.UnicodeUTF8)) - self.import_smileys.setText(QtGui.QApplication.translate("interfaceForm", "Import smiley pack", None, QtGui.QApplication.UnicodeUTF8)) - self.import_stickers.setText(QtGui.QApplication.translate("interfaceForm", "Import sticker pack", None, QtGui.QApplication.UnicodeUTF8)) - - def import_st(self): - directory = QtGui.QFileDialog.getExistingDirectory(self, - QtGui.QApplication.translate("MainWindow", - 'Choose folder with sticker pack', - None, - QtGui.QApplication.UnicodeUTF8), - curr_directory(), - QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) - - if directory: - src = directory + '/' - dest = curr_directory() + '/stickers/' + os.path.basename(directory) + '/' - copy(src, dest) - - def import_sm(self): - directory = QtGui.QFileDialog.getExistingDirectory(self, - QtGui.QApplication.translate("MainWindow", - 'Choose folder with smiley pack', - None, - QtGui.QApplication.UnicodeUTF8), - curr_directory(), - QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog) - - if directory: - src = directory + '/' - dest = curr_directory() + '/smileys/' + os.path.basename(directory) + '/' - copy(src, dest) - - def select_color(self): - col = QtGui.QColorDialog.getColor() - - if col.isValid(): - settings = Settings.get_instance() - name = col.name() - settings['unread_color'] = name - settings.save() - - def closeEvent(self, event): - settings = Settings.get_instance() - settings['theme'] = str(self.themeSelect.currentText()) - settings['smileys'] = self.smileys.isChecked() - restart = False - if settings['mirror_mode'] != self.mirror_mode.isChecked(): - settings['mirror_mode'] = self.mirror_mode.isChecked() - restart = True - if settings['compact_mode'] != self.compact_mode.isChecked(): - settings['compact_mode'] = self.compact_mode.isChecked() - restart = True - settings['smiley_pack'] = self.smiley_pack.currentText() - smileys.SmileyLoader.get_instance().load_pack() - language = self.lang_choose.currentText() - if settings['language'] != language: - settings['language'] = language - text = self.lang_choose.currentText() - path = Settings.supported_languages()[text] - app = QtGui.QApplication.instance() - app.removeTranslator(app.translator) - app.translator.load(curr_directory() + '/translations/' + path) - app.installTranslator(app.translator) - settings['message_font_size'] = self.messages_font_size.currentIndex() + 10 - Profile.get_instance().update() - settings.save() - if restart: - msgBox = QtGui.QMessageBox() - text = QtGui.QApplication.translate("interfaceForm", 'Restart app to apply settings', None, - QtGui.QApplication.UnicodeUTF8) - msgBox.setWindowTitle(QtGui.QApplication.translate("interfaceForm", 'Restart required', None, - QtGui.QApplication.UnicodeUTF8)) - msgBox.setText(text) - msgBox.exec_() - - -class AudioSettings(CenteredWidget): - """ - Audio calls settings form - """ - - def __init__(self): - super(AudioSettings, self).__init__() - self.initUI() - self.retranslateUi() - self.center() - - def initUI(self): - self.setObjectName("audioSettingsForm") - self.resize(400, 150) - self.setMinimumSize(QtCore.QSize(400, 150)) - self.setMaximumSize(QtCore.QSize(400, 150)) - self.in_label = QtGui.QLabel(self) - self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20)) - self.out_label = QtGui.QLabel(self) - self.out_label.setGeometry(QtCore.QRect(25, 65, 350, 20)) - font = QtGui.QFont() - font.setPointSize(16) - font.setBold(True) - self.in_label.setFont(font) - self.out_label.setFont(font) - self.input = QtGui.QComboBox(self) - self.input.setGeometry(QtCore.QRect(25, 30, 350, 30)) - self.output = QtGui.QComboBox(self) - self.output.setGeometry(QtCore.QRect(25, 90, 350, 30)) - p = pyaudio.PyAudio() - settings = Settings.get_instance() - self.in_indexes, self.out_indexes = [], [] - for i in range(p.get_device_count()): - device = p.get_device_info_by_index(i) - if device["maxInputChannels"]: - self.input.addItem(str(device["name"])) - self.in_indexes.append(i) - if device["maxOutputChannels"]: - self.output.addItem(str(device["name"])) - self.out_indexes.append(i) - self.input.setCurrentIndex(self.in_indexes.index(settings.audio['input'])) - self.output.setCurrentIndex(self.out_indexes.index(settings.audio['output'])) - QtCore.QMetaObject.connectSlotsByName(self) - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("audioSettingsForm", "Audio settings", None, QtGui.QApplication.UnicodeUTF8)) - self.in_label.setText(QtGui.QApplication.translate("audioSettingsForm", "Input device:", None, QtGui.QApplication.UnicodeUTF8)) - self.out_label.setText(QtGui.QApplication.translate("audioSettingsForm", "Output device:", None, QtGui.QApplication.UnicodeUTF8)) - - def closeEvent(self, event): - settings = Settings.get_instance() - settings.audio['input'] = self.in_indexes[self.input.currentIndex()] - settings.audio['output'] = self.out_indexes[self.output.currentIndex()] - settings.save() - - -class PluginsSettings(CenteredWidget): - """ - Plugins settings form - """ - - def __init__(self): - super(PluginsSettings, self).__init__() - self.initUI() - self.center() - self.retranslateUi() - - def initUI(self): - self.resize(400, 210) - self.setMinimumSize(QtCore.QSize(400, 210)) - self.setMaximumSize(QtCore.QSize(400, 210)) - self.comboBox = QtGui.QComboBox(self) - self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30)) - self.label = QtGui.QLabel(self) - self.label.setGeometry(QtCore.QRect(30, 40, 340, 90)) - self.label.setWordWrap(True) - self.button = QtGui.QPushButton(self) - self.button.setGeometry(QtCore.QRect(30, 130, 340, 30)) - self.button.clicked.connect(self.button_click) - self.open = QtGui.QPushButton(self) - self.open.setGeometry(QtCore.QRect(30, 170, 340, 30)) - self.open.clicked.connect(self.open_plugin) - self.pl_loader = plugin_support.PluginLoader.get_instance() - self.update_list() - self.comboBox.currentIndexChanged.connect(self.show_data) - self.show_data() - - def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate('PluginsForm', "Plugins", None, QtGui.QApplication.UnicodeUTF8)) - self.open.setText(QtGui.QApplication.translate('PluginsForm', "Open selected plugin", None, QtGui.QApplication.UnicodeUTF8)) - - def open_plugin(self): - ind = self.comboBox.currentIndex() - plugin = self.data[ind] - window = self.pl_loader.plugin_window(plugin[-1]) - if window is not None: - self.window = window - self.window.show() - else: - msgBox = QtGui.QMessageBox() - text = QtGui.QApplication.translate("PluginsForm", 'No GUI found for this plugin', None, - QtGui.QApplication.UnicodeUTF8) - msgBox.setWindowTitle(QtGui.QApplication.translate("PluginsForm", 'Error', None, - QtGui.QApplication.UnicodeUTF8)) - msgBox.setText(text) - msgBox.exec_() - - def update_list(self): - self.comboBox.clear() - data = self.pl_loader.get_plugins_list() - self.comboBox.addItems(list(map(lambda x: x[0], data))) - self.data = data - - def show_data(self): - ind = self.comboBox.currentIndex() - if len(self.data): - plugin = self.data[ind] - descr = plugin[2] or QtGui.QApplication.translate("PluginsForm", "No description available", None, QtGui.QApplication.UnicodeUTF8) - self.label.setText(descr) - if plugin[1]: - self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8)) - else: - self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8)) - else: - self.open.setVisible(False) - self.button.setVisible(False) - self.label.setText(QtGui.QApplication.translate("PluginsForm", "No plugins found", None, QtGui.QApplication.UnicodeUTF8)) - - def button_click(self): - ind = self.comboBox.currentIndex() - plugin = self.data[ind] - self.pl_loader.toggle_plugin(plugin[-1]) - plugin[1] = not plugin[1] - if plugin[1]: - self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8)) - else: - self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/toxygen/messages.py b/toxygen/messages.py deleted file mode 100644 index 87a1cc2..0000000 --- a/toxygen/messages.py +++ /dev/null @@ -1,101 +0,0 @@ - - -MESSAGE_TYPE = { - 'TEXT': 0, - 'ACTION': 1, - 'FILE_TRANSFER': 2, - 'INLINE': 3, - 'INFO_MESSAGE': 4 -} - - -class Message: - - def __init__(self, message_type, owner, time): - self._time = time - self._type = message_type - self._owner = owner - - def get_type(self): - return self._type - - def get_owner(self): - return self._owner - - def mark_as_sent(self): - self._owner = 0 - - -class TextMessage(Message): - """ - Plain text or action message - """ - - def __init__(self, message, owner, time, message_type): - super(TextMessage, self).__init__(message_type, owner, time) - self._message = message - - def get_data(self): - return self._message, self._owner, self._time, self._type - - -class TransferMessage(Message): - """ - Message with info about file transfer - """ - - def __init__(self, owner, time, status, size, name, friend_number, file_number): - super(TransferMessage, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], owner, time) - self._status = status - self._size = size - self._file_name = name - self._friend_number, self._file_number = friend_number, file_number - - def is_active(self, file_number): - return self._file_number == file_number and self._status not in (2, 3) - - def get_friend_number(self): - return self._friend_number - - def get_file_number(self): - return self._file_number - - def get_status(self): - return self._status - - def set_status(self, value): - self._status = value - - def get_data(self): - return self._file_name, self._size, self._time, self._owner, self._friend_number, self._file_number, self._status - - -class UnsentFile(Message): - def __init__(self, path, data, time): - super(UnsentFile, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], 0, time) - self._data, self._path = data, path - - def get_data(self): - return self._path, self._data, self._time - - def get_status(self): - return None - - -class InlineImage(Message): - """ - Inline image - """ - - def __init__(self, data): - super(InlineImage, self).__init__(MESSAGE_TYPE['INLINE'], None, None) - self._data = data - - def get_data(self): - return self._data - - -class InfoMessage(TextMessage): - - def __init__(self, message, time): - super(InfoMessage, self).__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/__init__.py b/toxygen/messenger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py new file mode 100644 index 0000000..d44a7a9 --- /dev/null +++ b/toxygen/messenger/messages.py @@ -0,0 +1,243 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os.path + +from history.database import MESSAGE_AUTHOR +from ui.messages_widgets import * + +MESSAGE_TYPE = { + 'TEXT': 0, + 'ACTION': 1, + 'FILE_TRANSFER': 2, + 'INLINE': 3, + 'INFO_MESSAGE': 4 +} + +PAGE_SIZE = 42 + + +class MessageAuthor: + + def __init__(self, author_name, author_type): + self._name = author_name + self._type = author_type + + def get_name(self): + return self._name + + name = property(get_name) + + def get_type(self): + return self._type + + def set_type(self, value): + self._type = value + + type = property(get_type, set_type) + + +class Message: + + MESSAGE_ID = 0 + + def __init__(self, message_type, author, iTime): + self._time = iTime + self._type = message_type + self._author = author + self._widget = None + self._message_id = self._get_id() + + def get_type(self): + return self._type + + type = property(get_type) + + def get_author(self): + return self._author + + author = property(get_author) + + def get_time(self): + return self._time + + time = property(get_time) + + def get_message_id(self): + return self._message_id + + message_id = property(get_message_id) + + def get_widget(self, *args): + # FixMe + self._widget = self._create_widget(*args) # pylint: disable=assignment-from-none + + return self._widget + + widget = property(get_widget) + + def remove_widget(self): + self._widget = None + + def mark_as_sent(self): + self._author.type = MESSAGE_AUTHOR['ME'] + if self._widget is not None: + self._widget.mark_as_sent() + + def _create_widget(self, *args): + # overridden + return None + + @staticmethod + def _get_id() -> int: + Message.MESSAGE_ID += 1 + + return int(Message.MESSAGE_ID) + + +class TextMessage(Message): + """ + Plain text or action message + """ + + def __init__(self, message, owner, iTime, message_type, message_id=0): + super().__init__(message_type, owner, iTime) + self._message = message + self._id = message_id + + def get_text(self) -> str: + return self._message + + text = property(get_text) + + def get_id(self): + return self._id + + id = property(get_id) + + def is_saved(self): + return self._id > 0 + + def _create_widget(self, *args): + return MessageItem(self, *args) + + +class OutgoingTextMessage(TextMessage): + + def __init__(self, message, owner, iTime, message_type, tox_message_id=0): + super().__init__(message, owner, iTime, message_type) + self._tox_message_id = tox_message_id + + def get_tox_message_id(self): + return self._tox_message_id + + def set_tox_message_id(self, tox_message_id): + self._tox_message_id = tox_message_id + + tox_message_id = property(get_tox_message_id, set_tox_message_id) + + +class GroupChatMessage(TextMessage): + + def __init__(self, cid, message, owner, iTime, message_type, name): + super().__init__(cid, message, owner, iTime, message_type) + self._user_name = name + + +class TransferMessage(Message): + """ + Message with info about file transfer + """ + + def __init__(self, author, iTime, state, size, file_name, friend_number, file_number): + super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], author, iTime) + self._state = state + self._size = size + self._file_name = file_name + self._friend_number, self._file_number = friend_number, file_number + + def is_active(self, file_number) -> bool: + if self._file_number != file_number: + return False + + return self._state not in (FILE_TRANSFER_STATE['FINISHED'], FILE_TRANSFER_STATE['CANCELLED']) + + def get_friend_number(self) -> int: + return self._friend_number + + friend_number = property(get_friend_number) + + def get_file_number(self): + return self._file_number + + file_number = property(get_file_number) + + def get_state(self): + return self._state + + def set_state(self, value): + self._state = value + + state = property(get_state, set_state) + + def get_size(self): + return self._size + + size = property(get_size) + + def get_file_name(self): + return self._file_name + + file_name = property(get_file_name) + + def transfer_updated(self, state, percentage, iTime): + self._state = state + if self._widget is not None: + self._widget.update_transfer_state(state, percentage, iTime) + + def _create_widget(self, *args): + return FileTransferItem(self, *args) + + +class UnsentFileMessage(TransferMessage): + + def __init__(self, path, data, iTime, author, size, friend_number): + file_name = os.path.basename(path) + super().__init__(author, iTime, FILE_TRANSFER_STATE['UNSENT'], size, file_name, friend_number, -1) + self._data, self._path = data, path + + def get_data(self): + return self._data + + data = property(get_data) + + def get_path(self): + return self._path + + path = property(get_path) + + def _create_widget(self, *args): + return UnsentFileItem(self, *args) + + +class InlineImageMessage(Message): + """ + Inline image + """ + + def __init__(self, data): + super().__init__(MESSAGE_TYPE['INLINE'], None, None) + self._data = data + + def get_data(self): + return self._data + + data = property(get_data) + + def _create_widget(self, *args): + return InlineImageItem(self, *args) + + +class InfoMessage(TextMessage): + + def __init__(self, message, iTime): + super().__init__(message, None, iTime, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py new file mode 100644 index 0000000..c38bc31 --- /dev/null +++ b/toxygen/messenger/messenger.py @@ -0,0 +1,367 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import logging +import common.tox_save as tox_save +import utils.ui as util_ui + +from messenger.messages import * +from toxygen_wrapper.tests.support_testing import assert_main_thread +from toxygen_wrapper.toxcore_enums_and_consts import TOX_MAX_MESSAGE_LENGTH + +global LOG +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) + +class Messenger(tox_save.ToxSave): + + def __init__(self, tox, plugin_loader, screen, contacts_manager, contacts_provider, items_factory, profile, + calls_manager): + super().__init__(tox) + self._plugin_loader = plugin_loader + self._screen = screen + self._contacts_manager = contacts_manager + self._contacts_provider = contacts_provider + self._items_factory = items_factory + self._profile = profile + self._profile_name = profile.name + + profile.name_changed_event.add_callback(self._on_profile_name_changed) + calls_manager.call_started_event.add_callback(self._on_call_started) + calls_manager.call_finished_event.add_callback(self._on_call_finished) + + def __repr__(self): + return "" + + def get_last_message(self) -> str: + contact = self._contacts_manager.get_curr_contact() + if contact is None: + return str() + + return contact.get_last_message_text() + + # Messaging - friends + + def new_message(self, friend_number, message_type, message) -> None: + """ + Current user gets new message + :param friend_number: friend_num of friend who sent message + :param message_type: message type - plain text or action message (/me) + :param message: text of message + """ + t = util.get_unix_time() + friend = self._get_friend_by_number(friend_number) + text_message = TextMessage(message, MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND']), t, message_type) + self._add_message(text_message, friend) + + def send_message(self) -> None: + text = self._screen.messageEdit.toPlainText() + + plugin_command_prefix = '/plugin ' + if text.startswith(plugin_command_prefix): + self._plugin_loader.command(text[len(plugin_command_prefix):]) + self._screen.messageEdit.clear() + return + + message_type = TOX_MESSAGE_TYPE['NORMAL'] + if False: # undocumented + action_message_prefix = '/me ' + if text.startswith(action_message_prefix): + message_type = TOX_MESSAGE_TYPE['ACTION'] + text = text[len(action_message_prefix):] + + if len(text) > TOX_MAX_MESSAGE_LENGTH: + text = text[:TOX_MAX_MESSAGE_LENGTH] # 1372 + try: + if self._contacts_manager.is_active_a_friend(): + self.send_message_to_friend(text, message_type) + elif self._contacts_manager.is_active_a_group(): + self.send_message_to_group('~'+text, message_type) + elif self._contacts_manager.is_active_a_group_chat_peer(): + self.send_message_to_group_peer(text, message_type) + else: + LOG.warn(f'Unknown friend type for Messenger send_message') + except Exception as e: + LOG.error(f'Messenger send_message {e}') + import traceback + LOG.warn(traceback.format_exc()) + title = 'Messenger send_message Error' + text = 'Error: ' + str(e) + assert_main_thread() + util_ui.message_box(text, title) + + def send_message_to_friend(self, text, message_type, friend_number=None) -> None: + """ + Send message + :param text: message text + :param friend_number: number of friend + from Qt callback + """ + if not text: + return + if friend_number is None: + friend_number = self._contacts_manager.get_active_number() + if friend_number is None or friend_number < 0: + LOG.error(f"No _contacts_manager.get_active_number") + return + assert_main_thread() + + friend = self._get_friend_by_number(friend_number) + if not friend: + LOG.error(f"No self._get_friend_by_number") + return + messages = self._split_message(text.encode('utf-8')) + t = util.get_unix_time() + for message in messages: + if friend.status is not None: + message_id = self._tox.friend_send_message(friend_number, message_type, message) + else: + message_id = 0 + message_author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['NOT_SENT']) + message = OutgoingTextMessage(text, message_author, t, message_type, message_id) + friend.append_message(message) + if not self._contacts_manager.is_friend_active(friend_number): + return + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + + def send_messages(self, friend_number:int) -> None: + """ + Send 'offline' messages to friend + """ + friend = self._get_friend_by_number(friend_number) + friend.load_corr() + messages = friend.get_unsent_messages() + try: + for message in messages: + message_id = self._tox.friend_send_message(friend_number, message.type, message.text.encode('utf-8')) + message.tox_message_id = message_id + except Exception as ex: + LOG.warn('Sending pending messages failed with ' + str(ex)) + + # Messaging - groups + + def send_message_to_group(self, text, message_type, group_number=None) -> None: + if group_number is None: + group_number = self._contacts_manager.get_active_number() + + if not text or group_number < 0: + return + + group = self._get_group_by_number(group_number) + messages = self._split_message(text.encode('utf-8')) + t = util.get_unix_time() + for message in messages: + self._tox.group_send_message(group_number, message_type, message) + message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) + message = OutgoingTextMessage(text, message_author, t, message_type) + group.append_message(message) + if not self._contacts_manager.is_group_active(group_number): + return + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + + def new_group_message(self, group_number, message_type, message, peer_id) -> None: + """ + Current user gets new message + :param message_type: message type - plain text or action message (/me) + :param message: text of message + """ + t = util.get_unix_time() + group = self._get_group_by_number(group_number) + if not group: + LOG.error(f"FixMe new_group_message _get_group_by_number({group_number})") + return + peer = group.get_peer_by_id(peer_id) + if not peer: + LOG.error('FixMe new_group_message group.get_peer_by_id ' + str(peer_id)) + return + text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) + self._add_message(text_message, group) + + # Messaging - group peers + + def send_message_to_group_peer(self, text, message_type, group_number=None, peer_id=None) -> None: + if group_number is None or peer_id is None: + group_peer_contact = self._contacts_manager.get_curr_contact() + peer_id = group_peer_contact.number + group = self._get_group_by_public_key(group_peer_contact.group_pk) + group_number = group.number + + if not text: + return + if group.number < 0: + return + if peer_id is not None and peer_id < 0: + return + + assert_main_thread() + group = self._get_group_by_number(group_number) + messages = self._split_message(text.encode('utf-8')) + + # FixMe: peer_id is None? + group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) + if group_peer_contact is None: + LOG.warn("M.group_send_private_message group_peer_contact is None") + return + # group_peer_contact now may be None + + t = util.get_unix_time() + for message in messages: + bRet = self._tox.group_send_private_message(group_number, peer_id, message_type, message) + if not bRet: + LOG.warn("M.group_send_private_messag failed") + continue + message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) + message = OutgoingTextMessage(text, message_author, t, message_type) + # AttributeError: 'GroupChatPeer' object has no attribute 'append_message' + if not hasattr(group_peer_contact, 'append_message'): + LOG.warn("M. group_peer_contact has no append_message group_peer_contact={group_peer_contact}") + else: + group_peer_contact.append_message(message) + if not self._contacts_manager.is_contact_active(group_peer_contact): + return + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + + def new_group_private_message(self, group_number, message_type, message, peer_id) -> None: + """ + Current user gets new message + :param message: text of message + """ + t = util.get_unix_time() + group = self._get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + if not peer: + LOG.warn('FixMe new_group_private_message group.get_peer_by_id ' + str(peer_id)) + return + text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), + t, message_type) + group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id) + if not group_peer_contact: + LOG.warn('FixMe new_group_private_message group_peer_contact ' + str(peer_id)) + return + self._add_message(text_message, group_peer_contact) + + # Message receipts + + def receipt(self, friend_number, message_id) -> None: + friend = self._get_friend_by_number(friend_number) + friend.mark_as_sent(message_id) + + # Typing notifications + + def send_typing(self, typing) -> None: + """ + Send typing notification to a friend + """ + if not self._contacts_manager.can_send_typing_notification(): + return + contact = self._contacts_manager.get_curr_contact() + contact.typing_notification_handler.send(self._tox, typing) + + def friend_typing(self, friend_number, typing) -> None: + """ + Display incoming typing notification + """ + if self._contacts_manager.is_friend_active(friend_number): + self._screen.typing.setVisible(typing) + + # Contact info updated + + def new_friend_name(self, friend, old_name, new_name) -> None: + if old_name == new_name or friend.has_alias(): + return + message = util_ui.tr('User {} is now known as {}') + message = message.format(old_name, new_name) + if not self._contacts_manager.is_friend_active(friend.number): + friend.actions = True + self._add_info_message(friend.number, message) + + # Private methods + + @staticmethod + def _split_message(message) -> list: + messages = [] + while len(message) > TOX_MAX_MESSAGE_LENGTH: + size = TOX_MAX_MESSAGE_LENGTH * 4 // 5 + last_part = message[size:TOX_MAX_MESSAGE_LENGTH] + if b' ' in last_part: + index = last_part.index(b' ') + elif b',' in last_part: + index = last_part.index(b',') + elif b'.' in last_part: + index = last_part.index(b'.') + else: + index = TOX_MAX_MESSAGE_LENGTH - size - 1 + index += size + 1 + messages.append(message[:index]) + message = message[index:] + if message: + messages.append(message) + + return messages + + def _get_friend_by_number(self, friend_number:int): + return self._contacts_provider.get_friend_by_number(friend_number) + + def _get_group_by_number(self, group_number): + return self._contacts_provider.get_group_by_number(group_number) + + def _get_group_by_public_key(self, public_key): + return self._contacts_provider.get_group_by_public_key( public_key) + + def _on_profile_name_changed(self, new_name) -> None: + if self._profile_name == new_name: + return + message = util_ui.tr('User {} is now known as {}') + message = message.format(self._profile_name, new_name) + for friend in self._contacts_provider.get_all_friends(): + self._add_info_message(friend.number, message) + self._profile_name = new_name + + def _on_call_started(self, friend_number, audio, video, is_outgoing) -> None: + if is_outgoing: + text = util_ui.tr("Outgoing video call") if video else util_ui.tr("Outgoing audio call") + else: + text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call") + self._add_info_message(friend_number, text) + + def _on_call_finished(self, friend_number, is_declined) -> None: + text = util_ui.tr("Call declined") if is_declined else util_ui.tr("Call finished") + self._add_info_message(friend_number, text) + + def _add_info_message(self, friend_number, text) -> None: + friend = self._get_friend_by_number(friend_number) + assert friend + message = InfoMessage(text, util.get_unix_time()) + friend.append_message(message) + if self._contacts_manager.is_friend_active(friend_number): + self._create_info_message_item(message) + + def _create_info_message_item(self, message) -> None: + assert_main_thread() + self._items_factory.create_message_item(message) + self._screen.messages.scrollToBottom() + + def _add_message(self, text_message, contact) -> None: + assert_main_thread() + if not contact: + LOG.warn("_add_message null contact") + return + if self._contacts_manager.is_contact_active(contact): # add message to list +# LOG.debug("_add_message is_contact_active(contact)") + self._create_message_item(text_message) + self._screen.messages.scrollToBottom() + self._contacts_manager.get_curr_contact().append_message(text_message) + else: +# LOG.debug("_add_message not is_contact_active(contact)") + contact.inc_messages() + contact.append_message(text_message) + if not contact.visibility: + self._contacts_manager.update_filtration() + + def _create_message_item(self, text_message) -> None: + # pixmap = self._contacts_manager.get_curr_contact().get_pixmap() + self._items_factory.create_message_item(text_message) diff --git a/toxygen/middleware/__init__.py b/toxygen/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py new file mode 100644 index 0000000..e0842f7 --- /dev/null +++ b/toxygen/middleware/callbacks.py @@ -0,0 +1,775 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import sys +import os +import threading +from qtpy import QtGui +from toxygen_wrapper.toxcore_enums_and_consts import * +from toxygen_wrapper.toxav_enums import * +from toxygen_wrapper.tox import bin_to_string +import utils.ui as util_ui +import utils.util as util +from middleware.threads import invoke_in_main_thread, execute +from notifications.tray import tray_notification +from notifications.sound import * +from datetime import datetime + +iMAX_INT32 = 4294967295 +# callbacks can be called in any thread so were being careful +def LOG_ERROR(l): print(f"EROR. {l}") +def LOG_WARN(l): print(f"WARN. {l}") +def LOG_INFO(l): + bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel <= 20-1 # pylint dusable=undefined-variable + if bIsVerbose: print(f"INFO. {l}") +def LOG_DEBUG(l): + bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel <= 10-1 # pylint dusable=undefined-variable + if bIsVerbose: print(f"DBUG. {l}") +def LOG_TRACE(l): + bIsVerbose = not hasattr(__builtins__, 'app') or app.oArgs.loglevel < 10-1 # pylint dusable=undefined-variable + pass # print(f"TRACE. {l}") + +global aTIMES +aTIMES=dict() +def bTooSoon(key, sSlot, fSec=10.0): + # rate limiting + global aTIMES + if sSlot not in aTIMES: + aTIMES[sSlot] = dict() + OTIME = aTIMES[sSlot] + now = datetime.now() + if key not in OTIME: + OTIME[key] = now + return False + delta = now - OTIME[key] + OTIME[key] = now + if delta.total_seconds() < fSec: return True + return False + +# TODO: refactoring. Use contact provider instead of manager + +# Callbacks - current user + +global iBYTES +iBYTES=0 +def sProcBytes(sFile=None): + if sys.platform == 'win32': return '' + global iBYTES + if sFile is None: + pid = os.getpid() + sFile = f"/proc/{pid}/net/softnet_stat" + if os.path.exists(sFile): + total = 0 + with open(sFile, 'r') as iFd: + for elt in iFd.readlines(): + i = elt.find(' ') + p = int(elt[:i], 16) + total = total + p + if iBYTES == 0: + iBYTES = total + return '' + diff = total - iBYTES + s = f' {diff // 1024} Kbytes' + else: + s = '' + return s + +def self_connection_status(tox, profile): + """ + Current user changed connection status (offline, TCP, UDP) + """ + sSlot = 'self connection status' + def wrapped(tox_link, connection, user_data): + key = f"connection {connection}" + if bTooSoon(key, sSlot, 10): return + s = sProcBytes() + try: + status = tox.self_get_status() if connection != TOX_CONNECTION['NONE'] else None + if status: + LOG_DEBUG(f"self_connection_status: connection={connection} status={status}" +' '+s) + invoke_in_main_thread(profile.set_status, status) + except Exception as e: + LOG_ERROR(f"self_connection_status: {e}") + pass + + return wrapped + + +# Callbacks - friends + + +def friend_status(contacts_manager, file_transfer_handler, profile, settings): + sSlot = 'friend status' + def wrapped(tox, friend_number, new_status, user_data): + """ + Check friend's status (none, busy, away) + """ + LOG_INFO(f"Friend's #{friend_number} status changed") + key = f"friend_number {friend_number}" + if bTooSoon(key, sSlot, 10): return + friend = contacts_manager.get_friend_by_number(friend_number) + if friend.status is None and settings['sound_notifications'] and \ + profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) + invoke_in_main_thread(friend.set_status, new_status) + + def set_timer(): + t = threading.Timer(5, lambda: file_transfer_handler.send_files(friend_number)) + t.start() + invoke_in_main_thread(set_timer) + invoke_in_main_thread(contacts_manager.update_filtration) + + return wrapped + + +def friend_connection_status(contacts_manager, profile, settings, plugin_loader, file_transfer_handler, + messenger, calls_manager): + def wrapped(tox, friend_number, new_status, user_data): + """ + Check friend's connection status (offline, udp, tcp) + """ + LOG_DEBUG(f"Friend #{friend_number} connection status: {new_status}") + friend = contacts_manager.get_friend_by_number(friend_number) + if new_status == TOX_CONNECTION['NONE']: + invoke_in_main_thread(friend.set_status, None) + invoke_in_main_thread(file_transfer_handler.friend_exit, friend_number) + invoke_in_main_thread(contacts_manager.update_filtration) + invoke_in_main_thread(messenger.friend_typing, friend_number, False) + invoke_in_main_thread(calls_manager.friend_exit, friend_number) + if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) + elif friend.status is None: + invoke_in_main_thread(file_transfer_handler.send_avatar, friend_number) + invoke_in_main_thread(plugin_loader.friend_online, friend_number) + + return wrapped + + +def friend_name(contacts_provider, messenger): + sSlot = 'friend_name' + def wrapped(tox, friend_number, name, size, user_data): + """ + Friend changed his name + """ + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 60): return + friend = contacts_provider.get_friend_by_number(friend_number) + old_name = friend.name + new_name = str(name, 'utf-8') + LOG_DEBUG(f"get_friend_by_number #{friend_number} {new_name}") + invoke_in_main_thread(friend.set_name, new_name) + invoke_in_main_thread(messenger.new_friend_name, friend, old_name, new_name) + + return wrapped + +def friend_status_message(contacts_manager, messenger): + sSlot = 'status_message' + def wrapped(tox, friend_number, status_message, size, user_data): + """ + :return: function for callback friend_status_message. It updates friend's status message + and calls window repaint + """ + friend = contacts_manager.get_friend_by_number(friend_number) + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 10): return + + invoke_in_main_thread(friend.set_status_message, str(status_message, 'utf-8')) + LOG_DEBUG(f'User #{friend_number} has new status message') + invoke_in_main_thread(messenger.send_messages, friend_number) + + return wrapped + + +def friend_message(messenger, contacts_manager, profile, settings, window, tray): + def wrapped(tox, friend_number, message_type, message, size, user_data): + """ + New message from friend + """ + LOG_DEBUG(f"friend_message #{friend_number}") + message = str(message, 'utf-8') + invoke_in_main_thread(messenger.new_message, friend_number, message_type, message) + if not window.isActiveWindow(): + friend = contacts_manager.get_friend_by_number(friend_number) + if settings['notifications'] \ + and profile.status != TOX_USER_STATUS['BUSY'] \ + and not settings.locked: + invoke_in_main_thread(tray_notification, friend.name, message, tray, window) + if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['MESSAGE']) + icon = os.path.join(util.get_images_directory(), 'icon_new_messages.png') + if tray: + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + + return wrapped + + +def friend_request(contacts_manager): + def wrapped(tox, public_key, message, message_size, user_data): + """ + Called when user get new friend request + """ + LOG_DEBUG(f'Friend request') + key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) + tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE) + invoke_in_main_thread(contacts_manager.process_friend_request, tox_id, str(message, 'utf-8')) + + return wrapped + + +def friend_typing(messenger): + sSlot = "friend_typing" + def wrapped(tox, friend_number, typing, user_data): + key = f"friend_number={friend_number}" + if bTooSoon(key, sSlot, 10): return + LOG_DEBUG(f"friend_typing #{friend_number}") + invoke_in_main_thread(messenger.friend_typing, friend_number, typing) + return wrapped + + +def friend_read_receipt(messenger): + def wrapped(tox, friend_number, message_id, user_data): + invoke_in_main_thread(messenger.receipt, friend_number, message_id) + + return wrapped + + +# Callbacks - file transfers + + +def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager, settings): + """ + New incoming file + """ + def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data): + if file_type == TOX_FILE_KIND['DATA']: + LOG_INFO(f'file_transfer_handler File friend_number={friend_number}') + try: + file_name = str(file_name[:file_name_size], 'utf-8') + except: + file_name = 'toxygen_file' + invoke_in_main_thread(file_transfer_handler.incoming_file_transfer, + friend_number, + file_number, + size, + file_name) + if not window.isActiveWindow(): + friend = contacts_manager.get_friend_by_number(friend_number) + if settings['notifications'] \ + and profile.status != TOX_USER_STATUS['BUSY'] \ + and not settings.locked: + file_from = util_ui.tr("File from") + invoke_in_main_thread(tray_notification, file_from + ' ' + friend.name, file_name, tray, window) + if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER']) + if tray: + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + else: # avatar + LOG_DEBUG(f'file_transfer_handler Avatar') + invoke_in_main_thread(file_transfer_handler.incoming_avatar, + friend_number, + file_number, + size) + return wrapped + + +def file_recv_chunk(file_transfer_handler): + """ + Incoming chunk + """ + def wrapped(tox, friend_number, file_number, position, chunk, length, user_data): + chunk = chunk[:length] if length else None + execute(file_transfer_handler.incoming_chunk, friend_number, file_number, position, chunk) + + return wrapped + + +def file_chunk_request(file_transfer_handler): + """ + Outgoing chunk + """ + def wrapped(tox, friend_number, file_number, position, size, user_data): + execute(file_transfer_handler.outgoing_chunk, friend_number, file_number, position, size) + + return wrapped + + +def file_recv_control(file_transfer_handler): + """ + Friend cancelled, paused or resumed file transfer + """ + def wrapped(tox, friend_number, file_number, file_control, user_data): + if file_control == TOX_FILE_CONTROL['CANCEL']: + file_transfer_handler.cancel_transfer(friend_number, file_number, True) + elif file_control == TOX_FILE_CONTROL['PAUSE']: + file_transfer_handler.pause_transfer(friend_number, file_number, True) + elif file_control == TOX_FILE_CONTROL['RESUME']: + file_transfer_handler.resume_transfer(friend_number, file_number, True) + + return wrapped + +# Callbacks - custom packets + + +def lossless_packet(plugin_loader): + def wrapped(tox, friend_number, data, length, user_data): + """ + Incoming lossless packet + """ + data = data[:length] + invoke_in_main_thread(plugin_loader.callback_lossless, friend_number, data) + + return wrapped + + +def lossy_packet(plugin_loader): + def wrapped(tox, friend_number, data, length, user_data): + """ + Incoming lossy packet + """ + data = data[:length] + invoke_in_main_thread(plugin_loader.callback_lossy, friend_number, data) + + return wrapped + + +# Callbacks - audio + +def call_state(calls_manager): + def wrapped(iToxav, friend_number, mask, user_data): + """ + New call state + """ + LOG_DEBUG(f"call_state #{friend_number}") + if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: + invoke_in_main_thread(calls_manager.stop_call, friend_number, True) + else: + # guessing was calls_manager. + #? incoming_call + calls_manager._call.toxav_call_state_cb(friend_number, mask) + + return wrapped + + +def call(calls_manager): + def wrapped(toxav, friend_number, audio, video, user_data): + """ + Incoming call from friend + """ + LOG_DEBUG(f"Incoming call from {friend_number} {audio} {video}") + invoke_in_main_thread(calls_manager.incoming_call, audio, video, friend_number) + + return wrapped + + +def callback_audio(calls_manager): + def wrapped(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data): + """ + New audio chunk + """ +#trace LOG_DEBUG(f"callback_audio #{friend_number}") + # dunno was .call + calls_manager._call.audio_chunk( + bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]), + audio_channels_count, + rate) + + return wrapped + +# Callbacks - video + + +def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data): + """ + Creates yuv frame from y, u, v and shows it using OpenCV + For yuv => bgr we need this YUV420 frame: + + width + ------------------------- + | | + | Y | height + | | + ------------------------- + | | | + | U even | U odd | height // 4 + | | | + ------------------------- + | | | + | V even | V odd | height // 4 + | | | + ------------------------- + + width // 2 width // 2 + + It can be created from initial y, u, v using slices + """ + LOG_DEBUG(f"video_receive_frame from toxav_video_receive_frame_cb={friend_number}") + with ts.ignoreStdout(): import cv2 + import numpy as np + try: + y_size = abs(max(width, abs(ystride))) + u_size = abs(max(width // 2, abs(ustride))) + v_size = abs(max(width // 2, abs(vstride))) + + y = np.asarray(y[:y_size * height], dtype=np.uint8).reshape(height, y_size) + u = np.asarray(u[:u_size * height // 2], dtype=np.uint8).reshape(height // 2, u_size) + v = np.asarray(v[:v_size * height // 2], dtype=np.uint8).reshape(height // 2, v_size) + + width -= width % 4 + height -= height % 4 + + frame = np.zeros((int(height * 1.5), width), dtype=np.uint8) + + frame[:height, :] = y[:height, :width] + frame[height:height * 5 // 4, :width // 2] = u[:height // 2:2, :width // 2] + frame[height:height * 5 // 4, width // 2:] = u[1:height // 2:2, :width // 2] + + frame[height * 5 // 4:, :width // 2] = v[:height // 2:2, :width // 2] + frame[height * 5 // 4:, width // 2:] = v[1:height // 2:2, :width // 2] + + frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) # pylint: disable=no-member + # imshow + invoke_in_main_thread(cv2.imshow, str(friend_number), frame) # pylint: disable=no-member + except Exception as ex: + LOG_ERROR(f"video_receive_frame {ex} #{friend_number}") + pass + +# Callbacks - groups + + +def group_message(window, tray, tox, messenger, settings, profile): + """ + New message in group chat + """ + def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data): + LOG_DEBUG(f"group_message #{group_number}") + message = str(message[:length], 'utf-8') + invoke_in_main_thread(messenger.new_group_message, group_number, message_type, message, peer_id) + if window.isActiveWindow(): + return + bl = settings['notify_all_gc'] or profile.name in message + name = tox.group_peer_get_name(group_number, peer_id) + if settings['sound_notifications'] and bl and \ + profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['MESSAGE']) + if False and settings['tray_icon'] and tray: + if settings['notifications'] and \ + profile.status != TOX_USER_STATUS['BUSY'] and \ + (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + if tray: + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + + return wrapped + + +def group_private_message(window, tray, tox, messenger, settings, profile): + """ + New private message in group chat + """ + def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data): + LOG_DEBUG(f"group_private_message #{group_number}") + message = str(message[:length], 'utf-8') + invoke_in_main_thread(messenger.new_group_private_message, group_number, message_type, message, peer_id) + if window.isActiveWindow(): + return + bl = settings['notify_all_gc'] or profile.name in message + try: + name = tox.group_peer_get_name(group_number, peer_id) + except Exception as e: + LOG_WARN("tox.group_peer_get_name {group_number} {peer_id}") + name = '' + if settings['notifications'] and settings['tray_icon'] \ + and profile.status != TOX_USER_STATUS['BUSY'] \ + and (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['MESSAGE']) + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + if tray and hasattr(tray, 'setIcon'): + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + + return wrapped + +# Exception ignored on calling ctypes callback function: .wrapped at 0x7ffede910700> +def group_invite(window, settings, tray, profile, groups_service, contacts_provider): + def wrapped(tox, friend_number, invite_data, length, group_name, group_name_length, user_data): + LOG_DEBUG(f"group_invite friend_number={friend_number}") + group_name = str(bytes(group_name[:group_name_length]), 'utf-8') + invoke_in_main_thread(groups_service.process_group_invite, + friend_number, group_name, + bytes(invite_data[:length])) + if window.isActiveWindow(): + return + bHasTray = tray and settings['tray_icon'] + if settings['notifications'] \ + and bHasTray \ + and profile.status != TOX_USER_STATUS['BUSY'] \ + and not settings.locked: + friend = contacts_provider.get_friend_by_number(friend_number) + title = util_ui.tr('New invite to group chat') + text = util_ui.tr('{} invites you to group "{}"').format(friend.name, group_name) + invoke_in_main_thread(tray_notification, title, text, tray, window) + if tray: + icon = util.join_path(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + + return wrapped + + +def group_self_join(contacts_provider, contacts_manager, groups_service): + sSlot = 'group_self_join' + def wrapped(tox, group_number, user_data): + if group_number is None: + LOG_ERROR(f"group_self_join NULL group_number #{group_number}") + return + LOG_DEBUG(f"group_self_join #{group_number}") + key = f"group_number {group_number}" + if bTooSoon(key, sSlot, 10): return + group = contacts_provider.get_group_by_number(group_number) + if group is None: + LOG_ERROR(f"group_self_join NULL group #{group}") + return + invoke_in_main_thread(group.set_status, TOX_USER_STATUS['NONE']) + invoke_in_main_thread(groups_service.update_group_info, group) + invoke_in_main_thread(contacts_manager.update_filtration) + + return wrapped + +def group_peer_join(contacts_provider, groups_service): + sSlot = "group_peer_join" + def wrapped(tox, group_number, peer_id, user_data): + key = f"group_peer_join #{group_number} peer_id={peer_id}" + if bTooSoon(key, sSlot, 20): return + group = contacts_provider.get_group_by_number(group_number) + if group is None: + LOG_ERROR(f"group_peer_join NULL group #{group} group_number={group_number}") + return + if peer_id > group._peers_limit: + LOG_ERROR(key +f" {peer_id} > {group._peers_limit}") + return + LOG_DEBUG(f"group_peer_join group={group}") + group.add_peer(peer_id) + invoke_in_main_thread(groups_service.generate_peers_list) + invoke_in_main_thread(groups_service.update_group_info, group) + + return wrapped + + +def group_peer_exit(contacts_provider, groups_service, contacts_manager): + def wrapped(tox, + group_number, peer_id, + exit_type, name, name_length, + message, length, + user_data): + group = contacts_provider.get_group_by_number(group_number) + if group: + LOG_DEBUG(f"group_peer_exit #{group_number} peer_id={peer_id} exit_type={exit_type}") + group.remove_peer(peer_id) + invoke_in_main_thread(groups_service.generate_peers_list) + else: + LOG_WARN(f"group_peer_exit group not found #{group_number} peer_id={peer_id}") + + return wrapped + +def group_peer_name(contacts_provider, groups_service): + def wrapped(tox, group_number, peer_id, name, length, user_data): + LOG_DEBUG(f"group_peer_name #{group_number} peer_id={peer_id}") + group = contacts_provider.get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + if peer: + peer.name = str(name[:length], 'utf-8') + invoke_in_main_thread(groups_service.generate_peers_list) + else: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + return + + return wrapped + + +def group_peer_status(contacts_provider, groups_service): + def wrapped(tox, group_number, peer_id, peer_status, user_data): + LOG_DEBUG(f"group_peer_status #{group_number} peer_id={peer_id}") + group = contacts_provider.get_group_by_number(group_number) + peer = group.get_peer_by_id(peer_id) + if peer: + peer.status = peer_status + else: + # _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + # TODO: add info message + invoke_in_main_thread(groups_service.generate_peers_list) + + return wrapped + + +def group_topic(contacts_provider): + def wrapped(tox, group_number, peer_id, topic, length, user_data): + LOG_DEBUG(f"group_topic #{group_number} peer_id={peer_id}") + group = contacts_provider.get_group_by_number(group_number) + if group: + topic = str(topic[:length], 'utf-8') + invoke_in_main_thread(group.set_status_message, topic) + else: + _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_WARN(f"group_topic {group} has no peer_id={peer_id} in {_peers}") + # TODO: add info message + + return wrapped + +def group_moderation(groups_service, contacts_provider, contacts_manager, messenger): + def update_peer_role(group, mod_peer_id, peer_id, new_role): + peer = group.get_peer_by_id(peer_id) + if peer: + peer.role = new_role + # TODO: add info message + else: + # FixMe: known signal to revalidate roles... + # _peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"update_peer_role group {group} has no peer_id={peer_id} in _peers!r") + # TODO: add info message + + def remove_peer(group, mod_peer_id, peer_id, is_ban): + peer = group.get_peer_by_id(peer_id) + if peer: + contacts_manager.remove_group_peer_by_id(group, peer_id) + group.remove_peer(peer_id) + else: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + # TODO: add info message + + # source_peer_number, target_peer_number, + def wrapped(tox, group_number, mod_peer_id, peer_id, event_type, user_data): + if mod_peer_id == iMAX_INT32 or peer_id == iMAX_INT32: + # FixMe: known signal to revalidate roles... + return + LOG_DEBUG(f"group_moderation #{group_number} mod_id={mod_peer_id} peer_id={peer_id} event_type={event_type}") + group = contacts_provider.get_group_by_number(group_number) + mod_peer = group.get_peer_by_id(mod_peer_id) + if not mod_peer: + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no mod_peer_id={mod_peer_id} in _peers!r") + return + peer = group.get_peer_by_id(peer_id) + if not peer: + # FixMe: known signal to revalidate roles... + #_peers = [(p._name, p._peer_id) for p in group.get_peers()] + LOG_TRACE(f"remove_peer group {group} has no peer_id={peer_id} in _peers!r") + return + + if event_type == TOX_GROUP_MOD_EVENT['KICK']: + remove_peer(group, mod_peer_id, peer_id, False) + elif event_type == TOX_GROUP_MOD_EVENT['OBSERVER']: + update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['OBSERVER']) + elif event_type == TOX_GROUP_MOD_EVENT['USER']: + update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['USER']) + elif event_type == TOX_GROUP_MOD_EVENT['MODERATOR']: + update_peer_role(group, mod_peer_id, peer_id, TOX_GROUP_ROLE['MODERATOR']) + + invoke_in_main_thread(groups_service.generate_peers_list) + + return wrapped + + +def group_password(contacts_provider): + + def wrapped(tox_link, group_number, password, length, user_data): + LOG_DEBUG(f"group_password #{group_number}") + password = str(password[:length], 'utf-8') + group = contacts_provider.get_group_by_number(group_number) + group.password = password + + return wrapped + + +def group_peer_limit(contacts_provider): + + def wrapped(tox_link, group_number, peer_limit, user_data): + LOG_DEBUG(f"group_peer_limit #{group_number}") + group = contacts_provider.get_group_by_number(group_number) + group.peer_limit = peer_limit + + return wrapped + + +def group_privacy_state(contacts_provider): + + def wrapped(tox_link, group_number, privacy_state, user_data): + LOG_DEBUG(f"group_privacy_state #{group_number}") + group = contacts_provider.get_group_by_number(group_number) + group.is_private = privacy_state == TOX_GROUP_PRIVACY_STATE['PRIVATE'] + + return wrapped + +# Callbacks - initialization + + +def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, + calls_manager, file_transfer_handler, main_window, tray, messenger, groups_service, + contacts_provider, ms=None): + """ + Initialization of all callbacks. + :param tox: Tox instance + :param profile: Profile instance + :param settings: Settings instance + :param contacts_manager: ContactsManager instance + :param contacts_manager: ContactsManager instance + :param calls_manager: CallsManager instance + :param file_transfer_handler: FileTransferHandler instance + :param plugin_loader: PluginLoader instance + :param main_window: MainWindow instance + :param tray: tray (for notifications) + :param messenger: Messenger instance + :param groups_service: GroupsService instance + :param contacts_provider: ContactsProvider instance + """ + + # self callbacks + tox.callback_self_connection_status(self_connection_status(tox, profile)) + + # friend callbacks + tox.callback_friend_status(friend_status(contacts_manager, file_transfer_handler, profile, settings)) + tox.callback_friend_message(friend_message(messenger, contacts_manager, profile, settings, main_window, tray)) + tox.callback_friend_connection_status(friend_connection_status(contacts_manager, profile, settings, plugin_loader, + file_transfer_handler, messenger, calls_manager)) + tox.callback_friend_name(friend_name(contacts_provider, messenger)) + tox.callback_friend_status_message(friend_status_message(contacts_manager, messenger)) + tox.callback_friend_request(friend_request(contacts_manager)) + tox.callback_friend_typing(friend_typing(messenger)) + tox.callback_friend_read_receipt(friend_read_receipt(messenger)) + + # file transfer + tox.callback_file_recv(tox_file_recv(main_window, tray, profile, file_transfer_handler, + contacts_manager, settings)) + tox.callback_file_recv_chunk(file_recv_chunk(file_transfer_handler)) + tox.callback_file_chunk_request(file_chunk_request(file_transfer_handler)) + tox.callback_file_recv_control(file_recv_control(file_transfer_handler)) + + # av + toxav = tox.AV + toxav.callback_call_state(call_state(calls_manager), 0) + toxav.callback_call(call(calls_manager), 0) + toxav.callback_audio_receive_frame(callback_audio(calls_manager), 0) + toxav.callback_video_receive_frame(video_receive_frame, 0) + + # custom packets + tox.callback_friend_lossless_packet(lossless_packet(plugin_loader)) + tox.callback_friend_lossy_packet(lossy_packet(plugin_loader)) + + # gc callbacks + tox.callback_group_message(group_message(main_window, tray, tox, messenger, settings, profile), 0) + tox.callback_group_private_message(group_private_message(main_window, tray, tox, messenger, settings, profile), 0) + tox.callback_group_invite(group_invite(main_window, settings, tray, profile, groups_service, contacts_provider), 0) + tox.callback_group_self_join(group_self_join(contacts_provider, contacts_manager, groups_service), 0) + tox.callback_group_peer_join(group_peer_join(contacts_provider, groups_service), 0) + tox.callback_group_peer_exit(group_peer_exit(contacts_provider, groups_service, contacts_manager), 0) + tox.callback_group_peer_name(group_peer_name(contacts_provider, groups_service), 0) + tox.callback_group_peer_status(group_peer_status(contacts_provider, groups_service), 0) + tox.callback_group_topic(group_topic(contacts_provider), 0) + tox.callback_group_moderation(group_moderation(groups_service, contacts_provider, contacts_manager, messenger), 0) + tox.callback_group_password(group_password(contacts_provider), 0) + tox.callback_group_peer_limit(group_peer_limit(contacts_provider), 0) + tox.callback_group_privacy_state(group_privacy_state(contacts_provider), 0) diff --git a/toxygen/middleware/threads.py b/toxygen/middleware/threads.py new file mode 100644 index 0000000..75e3fc9 --- /dev/null +++ b/toxygen/middleware/threads.py @@ -0,0 +1,223 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import sys +import threading +import queue +from qtpy import QtCore + +from bootstrap.bootstrap import * +from bootstrap.bootstrap import download_nodes_list +from toxygen_wrapper.toxcore_enums_and_consts import TOX_USER_STATUS, TOX_CONNECTION +import toxygen_wrapper.tests.support_testing as ts +from utils import util + +import time +sleep = time.sleep + +# LOG=util.log +global LOG +import logging +LOG = logging.getLogger('app.'+'threads') +# log = lambda x: LOG.info(x) + +def LOG_ERROR(l): print('EROR+ '+l) +def LOG_WARN(l): print('WARN+ '+l) +def LOG_INFO(l): print('INFO+ '+l) +def LOG_DEBUG(l): print('DBUG+ '+l) +def LOG_TRACE(l): pass # print('TRAC+ '+l) + +iLAST_CONN = 0 +iLAST_DELTA = 60 + +# Base threads + +class BaseThread(threading.Thread): + + def __init__(self, name=None, target=None): + self._stop_thread = False + if name: + super().__init__(name=name, target=target) + else: + super().__init__(target=target) + + def stop_thread(self, timeout=-1): + self._stop_thread = True + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.join(timeout) + if not self.is_alive(): break + i = i + 1 + else: + LOG_WARN(f"BaseThread {self.name} BLOCKED after {ts.iTHREAD_JOINS}") + +class BaseQThread(QtCore.QThread): + + def __init__(self, name=None): + # NO name=name + super().__init__() + self._stop_thread = False + self.name = str(id(self)) + + def stop_thread(self, timeout=-1): + self._stop_thread = True + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.wait(timeout) + if not self.isRunning(): break + i = i + 1 + sleep(ts.iTHREAD_TIMEOUT) + else: + LOG_WARN(f"BaseQThread {self.name} BLOCKED") + +# Toxcore threads + +class InitThread(BaseThread): + + def __init__(self, tox, plugin_loader, settings, app, is_first_start): + super().__init__(name='InitThread') + self._tox = tox + self._plugin_loader = plugin_loader + self._settings = settings + self._app = app + self._is_first_start = is_first_start + + def run(self): + # DBUG+ InitThread run: ERROR name 'ts' is not defined + import toxygen_wrapper.tests.support_testing as ts + LOG_DEBUG('InitThread run: ') + try: + if self._is_first_start and ts.bAreWeConnected() and \ + self._settings['download_nodes_list']: + LOG_INFO(f"downloading list of nodes {self._settings['download_nodes_list']}") + download_nodes_list(self._settings, oArgs=self._app._args) + + if ts.bAreWeConnected(): + LOG_INFO(f"calling test_net nodes") + self._app.test_net(oThread=self, iMax=4) + + if self._is_first_start: + LOG_INFO('starting plugins') + self._plugin_loader.load() + + except Exception as e: + LOG_DEBUG(f"InitThread run: ERROR {e}") + pass + + for _ in range(ts.iTHREAD_JOINS): + if self._stop_thread: + return + sleep(ts.iTHREAD_SLEEP) + return + +class ToxIterateThread(BaseQThread): + + def __init__(self, tox, app=None): + super().__init__() + self._tox = tox + self._app = app + + def run(self): + LOG_DEBUG('ToxIterateThread run: ') + while not self._stop_thread: + try: + iMsec = self._tox.iteration_interval() + self._tox.iterate() + except Exception as e: + # Fatal Python error: Segmentation fault + LOG_ERROR(f"ToxIterateThread run: {e}") + else: + sleep(iMsec / 1000.0) + + global iLAST_CONN + if not iLAST_CONN: + iLAST_CONN = time.time() + # TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes + + # and segv + if time.time() - iLAST_CONN > iLAST_DELTA and \ + ts.bAreWeConnected() and \ + self._tox.self_get_status() == TOX_USER_STATUS['NONE'] and \ + self._tox.self_get_connection_status() == TOX_CONNECTION['NONE']: + iLAST_CONN = time.time() + LOG_INFO(f"ToxIterateThread calling test_net") + invoke_in_main_thread( + self._app.test_net, oThread=self, iMax=2) + + +class ToxAVIterateThread(BaseQThread): + def __init__(self, toxav): + super().__init__() + self._toxav = toxav + + def run(self): + LOG_DEBUG('ToxAVIterateThread run: ') + while not self._stop_thread: + self._toxav.iterate() + sleep(self._toxav.iteration_interval() / 1000) + + +# File transfers thread + +class FileTransfersThread(BaseQThread): + + def __init__(self): + super().__init__('FileTransfers') + self._queue = queue.Queue() + self._timeout = 0.01 + + def execute(self, func, *args, **kwargs): + self._queue.put((func, args, kwargs)) + + def run(self): + while not self._stop_thread: + try: + func, args, kwargs = self._queue.get(timeout=self._timeout) + func(*args, **kwargs) + except queue.Empty: + pass + except queue.Full: + LOG_WARN('Queue is full in _thread') + except Exception as ex: + LOG_ERROR('in _thread: ' + str(ex)) + + +_thread = FileTransfersThread() +def start_file_transfer_thread(): + _thread.start() + + +def stop_file_transfer_thread(): + _thread.stop_thread() + + +def execute(func, *args, **kwargs): + _thread.execute(func, *args, **kwargs) + + +# Invoking in main thread + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) diff --git a/toxygen/middleware/tox_factory.py b/toxygen/middleware/tox_factory.py new file mode 100644 index 0000000..1216dd8 --- /dev/null +++ b/toxygen/middleware/tox_factory.py @@ -0,0 +1,89 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import ctypes +import traceback +import os +from ctypes import * + +import user_data.settings +import toxygen_wrapper.tox +import toxygen_wrapper.toxcore_enums_and_consts as enums +from toxygen_wrapper.tests import support_testing as ts +# callbacks can be called in any thread so were being careful +# tox.py can be called by callbacks +from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +global LOG +import logging +LOG = logging.getLogger('app.'+'tox_factory') + +from utils import util +from utils import ui as util_ui + + +def tox_factory(data=None, settings=None, args=None, app=None): + """ + :param data: user data from .tox file. None = no saved data, create new profile + :param settings: current profile settings. None = default settings will be used + :return: new tox instance + """ + if not settings: + LOG_WARN("tox_factory using get_default_settings") + settings = user_data.settings.Settings.get_default_settings() + else: + user_data.settings.clean_settings(settings) + + try: + tox_options = toxygen_wrapper.tox.Tox.options_new() + tox_options.contents.ipv6_enabled = settings['ipv6_enabled'] + tox_options.contents.udp_enabled = settings['udp_enabled'] + tox_options.contents.proxy_type = int(settings['proxy_type']) + if type(settings['proxy_host']) == str: + tox_options.contents.proxy_host = bytes(settings['proxy_host'],'UTF-8') + elif type(settings['proxy_host']) == bytes: + tox_options.contents.proxy_host = settings['proxy_host'] + else: + tox_options.contents.proxy_host = b'' + tox_options.contents.proxy_port = int(settings['proxy_port']) + tox_options.contents.start_port = settings['start_port'] + tox_options.contents.end_port = settings['end_port'] + tox_options.contents.tcp_port = settings['tcp_port'] + tox_options.contents.local_discovery_enabled = settings['local_discovery_enabled'] + tox_options.contents.dht_announcements_enabled = settings['dht_announcements_enabled'] + tox_options.contents.hole_punching_enabled = settings['hole_punching_enabled'] + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = ctypes.c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.ipv6_enabled = False + tox_options.contents.hole_punching_enabled = False + + LOG.debug("toxygen_wrapper.tox.Tox settings: " +repr(settings)) + + if 'trace_enabled' in settings and not settings['trace_enabled']: + LOG_DEBUG("settings['trace_enabled' disabled" ) + elif tox_options._options_pointer and \ + 'trace_enabled' in settings and settings['trace_enabled']: + ts.vAddLoggerCallback(tox_options) + LOG_INFO("c-toxcore trace_enabled enabled" ) + else: + LOG_WARN("No tox_options._options_pointer to add self_logger_cb" ) + + retval = toxygen_wrapper.tox.Tox(tox_options) + except Exception as e: + if app and hasattr(app, '_log'): + pass + LOG_ERROR(f"toxygen_wrapper.tox.Tox failed: {e}") + LOG_WARN(traceback.format_exc()) + raise + + if app and hasattr(app, '_log'): + app._log("DEBUG: toxygen_wrapper.tox.Tox succeeded") + return retval diff --git a/toxygen/network/__init__.py b/toxygen/network/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/network/tox_dns.py b/toxygen/network/tox_dns.py new file mode 100644 index 0000000..58c9da1 --- /dev/null +++ b/toxygen/network/tox_dns.py @@ -0,0 +1,102 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import json +import urllib.request +import logging + +try: + import requests +except ImportError: + requests = None +from qtpy import QtNetwork, QtCore + +import utils.util as util + +global LOG + +iTIMEOUT=60 +LOG = logging.getLogger('app.'+__name__) + +class ToxDns: + + def __init__(self, settings, log=None): + self._settings = settings + self._log = log + + @staticmethod + def _send_request(url, data): + if requests: + LOG.info('send_request loading with requests: ' + str(url)) + headers = dict() + headers['Content-Type'] = 'application/json' + req = requests.get(url, headers=headers) + if req.status_code < 300: + retval = req.content + else: + raise LookupError(str(req.status_code)) + else: + req = urllib.request.Request(url) + req.add_header('Content-Type', 'application/json') + response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) + retval = response.read() + res = json.loads(str(retval, 'utf-8')) + if not res['c']: + return res['tox_id'] + else: + raise LookupError() + + def lookup(self, email): + """ + TOX DNS 4 + :param email: data like 'groupbot@toxme.io' + :return: tox id on success else None + """ + site = email.split('@')[1] + data = {"action": 3, "name": "{}".format(email)} + urls = ('https://{}/api'.format(site), 'http://{}/api'.format(site)) + if requests: + for url in urls: + LOG.info('TOX nodes loading with requests: ' + str(url)) + try: + headers = dict() + headers['Content-Type'] = 'application/json' + req = requests.get(url, headers=headers, timeout=iTIMEOUT) + if req.status_code < 300: + result = req.content + return result + except Exception as ex: + LOG.error('ERROR: TOX DNS loading error with requests: ' + str(ex)) + + elif not self._settings['proxy_type']: # no proxy + for url in urls: + try: + return self._send_request(url, data) + except Exception as ex: + LOG.error('ERROR: TOX DNS ' + str(ex)) + else: # proxy + netman = QtNetwork.QNetworkAccessManager() + proxy = QtNetwork.QNetworkProxy() + if self._settings['proxy_type'] == 2: + proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy) + else: + proxy.setType(QtNetwork.QNetworkProxy.HttpProxy) + proxy.setHostName(self._settings['proxy_host']) + proxy.setPort(self._settings['proxy_port']) + netman.setProxy(proxy) + for url in urls: + try: + request = QtNetwork.QNetworkRequest() + request.setUrl(QtCore.QUrl(url)) + request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json") + reply = netman.post(request, bytes(json.dumps(data), 'utf-8')) + + while not reply.isFinished(): + QtCore.QThread.msleep(1) + QtCore.QCoreApplication.processEvents() + data = bytes(reply.readAll().data()) + result = json.loads(str(data, 'utf-8')) + if not result['c']: + return result['tox_id'] + except Exception as ex: + LOG.error('ERROR: TOX DNS ' + str(ex)) + + return None # error diff --git a/toxygen/notifications.py b/toxygen/notifications.py deleted file mode 100644 index 81a8a05..0000000 --- a/toxygen/notifications.py +++ /dev/null @@ -1,75 +0,0 @@ -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from util import curr_directory -import wave -import pyaudio - - -SOUND_NOTIFICATION = { - 'MESSAGE': 0, - 'FRIEND_CONNECTION_STATUS': 1, - 'FILE_TRANSFER': 2 -} - - -def tray_notification(title, text, tray, window): - """ - Show tray notification and activate window icon - NOTE: different behaviour on different OS - :param title: Name of user who sent message or file - :param text: text of message or file info - :param tray: ref to tray icon - :param window: main window - """ - if QtGui.QSystemTrayIcon.isSystemTrayAvailable(): - if len(text) > 30: - text = text[:27] + '...' - tray.showMessage(title, text, QtGui.QSystemTrayIcon.NoIcon, 3000) - QtGui.QApplication.alert(window, 0) - - def message_clicked(): - window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) - window.activateWindow() - tray.connect(tray, QtCore.SIGNAL("messageClicked()"), message_clicked) - - -class AudioFile: - chunk = 1024 - - def __init__(self, fl): - self.wf = wave.open(fl, 'rb') - self.p = pyaudio.PyAudio() - self.stream = self.p.open( - format=self.p.get_format_from_width(self.wf.getsampwidth()), - channels=self.wf.getnchannels(), - rate=self.wf.getframerate(), - output=True - ) - - def play(self): - data = self.wf.readframes(self.chunk) - while data: - self.stream.write(data) - data = self.wf.readframes(self.chunk) - - def close(self): - self.stream.close() - self.p.terminate() - - -def sound_notification(t): - """ - Plays sound notification - :param t: type of notification - """ - if t == SOUND_NOTIFICATION['MESSAGE']: - f = curr_directory() + '/sounds/message.wav' - elif t == SOUND_NOTIFICATION['FILE_TRANSFER']: - f = curr_directory() + '/sounds/file.wav' - else: - f = curr_directory() + '/sounds/contact.wav' - a = AudioFile(f) - a.play() - a.close() diff --git a/toxygen/notifications/__init__.py b/toxygen/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/notifications/sound.py b/toxygen/notifications/sound.py new file mode 100644 index 0000000..9df46b2 --- /dev/null +++ b/toxygen/notifications/sound.py @@ -0,0 +1,73 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os.path +import wave + +import utils.util + +import toxygen_wrapper.tests.support_testing as ts +with ts.ignoreStderr(): + import pyaudio + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +SOUND_NOTIFICATION = { + 'MESSAGE': 0, + 'FRIEND_CONNECTION_STATUS': 1, + 'FILE_TRANSFER': 2 +} + + +class AudioFile: + chunk = 1024 + + def __init__(self, fl): + self.wf = wave.open(fl, 'rb') + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wf.getsampwidth()), + channels=self.wf.getnchannels(), + rate=self.wf.getframerate(), + output=True) + + def play(self): + data = self.wf.readframes(self.chunk) + try: + while data: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + except Exception as e: + LOG.error(f"Error during AudioFile play {e}") + LOG.debug("Error during AudioFile play " \ + +' rate=' +str(self.wf.getframerate()) \ + + 'format=' +str(self.p.get_format_from_width(self.wf.getsampwidth())) \ + +' channels=' +str(self.wf.getnchannels()) \ + ) + + raise + + def close(self): + self.stream.close() + self.p.terminate() + + +def sound_notification(t): + """ + Plays sound notification + :param t: type of notification + """ + if t == SOUND_NOTIFICATION['MESSAGE']: + f = get_file_path('message.wav') + elif t == SOUND_NOTIFICATION['FILE_TRANSFER']: + f = get_file_path('file.wav') + else: + f = get_file_path('contact.wav') + a = AudioFile(f) + a.play() + a.close() + + +def get_file_path(file_name): + return os.path.join(utils.util.get_sounds_directory(), file_name) diff --git a/toxygen/notifications/tray.py b/toxygen/notifications/tray.py new file mode 100644 index 0000000..0a6bca3 --- /dev/null +++ b/toxygen/notifications/tray.py @@ -0,0 +1,23 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import QtCore, QtWidgets + +def tray_notification(title, text, tray, window): + """ + Show tray notification and activate window icon + NOTE: different behaviour on different OS + :param title: Name of user who sent message or file + :param text: text of message or file info + :param tray: ref to tray icon + :param window: main window + """ + if tray and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + if len(text) > 30: + text = text[:27] + '...' + tray.showMessage(title, text, QtWidgets.QSystemTrayIcon.NoIcon, 3000) + QtWidgets.QApplication.alert(window, 0) + + def message_clicked(): + window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + window.activateWindow() + tray.messageClicked.connect(message_clicked) diff --git a/toxygen/plugin_support.py b/toxygen/plugin_support.py deleted file mode 100644 index 944c353..0000000 --- a/toxygen/plugin_support.py +++ /dev/null @@ -1,157 +0,0 @@ -import util -import profile -import os -import importlib -import inspect -import plugins.plugin_super_class as pl -import toxencryptsave -import sys - - -class PluginLoader(util.Singleton): - - def __init__(self, tox, settings): - super().__init__() - self._profile = profile.Profile.get_instance() - self._settings = settings - self._plugins = {} # dict. key - plugin unique short name, value - tuple (plugin instance, is active) - self._tox = tox - self._encr = toxencryptsave.ToxEncryptSave.get_instance() - - def set_tox(self, tox): - """ - New tox instance - """ - self._tox = tox - for value in self._plugins.values(): - value[0].set_tox(tox) - - def load(self): - """ - Load all plugins in plugins folder - """ - path = util.curr_directory() + '/plugins/' - if not os.path.exists(path): - util.log('Plugin dir not found') - return - else: - sys.path.append(path) - files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] - for fl in files: - if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): - continue - name = fl[:-3] # module name without .py - try: - module = importlib.import_module(name) # import plugin - except ImportError: - util.log('Import error in module ' + name) - continue - except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) - continue - for elem in dir(module): - obj = getattr(module, elem) - if inspect.isclass(obj) and hasattr(obj, 'is_plugin') and obj.is_plugin: # looking for plugin class in module - print('Plugin', elem) - try: # create instance of plugin class - inst = obj(self._tox, self._profile, self._settings, self._encr) - autostart = inst.get_short_name() in self._settings['plugins'] - if autostart: - inst.start() - except Exception as ex: - util.log('Exception in module ' + name + ' Exception: ' + str(ex)) - continue - self._plugins[inst.get_short_name()] = [inst, autostart] # (inst, is active) - break - - def callback_lossless(self, friend_number, data, length): - """ - New incoming custom lossless packet (callback) - """ - l = data[0] - pl.LOSSLESS_FIRST_BYTE - name = ''.join(chr(x) for x in data[1:l + 1]) - if name in self._plugins and self._plugins[name][1]: - self._plugins[name][0].lossless_packet(''.join(chr(x) for x in data[l + 1:length]), friend_number) - - def callback_lossy(self, friend_number, data, length): - """ - New incoming custom lossy packet (callback) - """ - l = data[0] - pl.LOSSY_FIRST_BYTE - name = ''.join(chr(x) for x in data[1:l + 1]) - if name in self._plugins and self._plugins[name][1]: - self._plugins[name][0].lossy_packet(''.join(chr(x) for x in data[l + 1:length]), friend_number) - - def friend_online(self, friend_number): - """ - Friend with specified number is online - """ - for elem in self._plugins.values(): - if elem[1]: - elem[0].friend_connected(friend_number) - - def get_plugins_list(self): - """ - Returns list of all plugins - """ - result = [] - for data in self._plugins.values(): - result.append([data[0].get_name(), # plugin full name - data[1], # is enabled - data[0].get_description(), # plugin description - data[0].get_short_name()]) # key - short unique name - return result - - def plugin_window(self, key): - """ - Return window or None for specified plugin - """ - return self._plugins[key][0].get_window() - - def toggle_plugin(self, key): - """ - Enable/disable plugin - :param key: plugin short name - """ - plugin = self._plugins[key] - if plugin[1]: - plugin[0].stop() - else: - plugin[0].start() - plugin[1] = not plugin[1] - if plugin[1]: - self._settings['plugins'].append(key) - else: - self._settings['plugins'].remove(key) - self._settings.save() - - def command(self, text): - """ - New command for plugin - """ - text = text.strip() - name = text.split()[0] - if name in self._plugins and self._plugins[name][1]: - self._plugins[name][0].command(text[len(name) + 1:]) - - def get_menu(self, menu, num): - """ - Return list of items for menu - """ - result = [] - for elem in self._plugins.values(): - if elem[1]: - try: - result.extend(elem[0].get_menu(menu, num)) - except: - continue - return result - - def stop(self): - """ - App is closing, stop all plugins - """ - for key in list(self._plugins.keys()): - if self._plugins[key][1]: - self._plugins[key][0].close() - del self._plugins[key] diff --git a/toxygen/plugin_support/__init__.py b/toxygen/plugin_support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/plugin_support/plugin_support.py b/toxygen/plugin_support/plugin_support.py new file mode 100644 index 0000000..f180e4d --- /dev/null +++ b/toxygen/plugin_support/plugin_support.py @@ -0,0 +1,231 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +import importlib +import inspect +import sys +import logging + +import utils.util as util +import plugins.plugin_super_class as pl + +# LOG=util.log +global LOG +LOG = logging.getLogger('plugin_support') +def trace(msg, *args, **kwargs): LOG._log(0, msg, []) +LOG.trace = trace + +log = lambda x: LOG.info(x) + +class Plugin: + + def __init__(self, plugin, is_active): + self._instance = plugin + self._is_active = is_active + + def get_instance(self): + return self._instance + + instance = property(get_instance) + + def get_is_active(self): + return self._is_active + + def set_is_active(self, is_active): + self._is_active = is_active + + is_active = property(get_is_active, set_is_active) + + +class PluginLoader: + + def __init__(self, settings, app): + self._settings = settings + self._app = app + self._plugins = {} # dict. key - plugin unique short name, value - Plugin instance + + def set_tox(self, tox) -> None: + """ + New tox instance + """ + for plugin in self._plugins.values(): + plugin.instance.set_tox(tox) + + def load(self) -> None: + """ + Load all plugins in plugins folder + """ + path = util.get_plugins_directory() + if not os.path.exists(path): + self._app._LOG('WARN: Plugin directory not found: ' + path) + return + + sys.path.append(path) + files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] + for fl in files: + if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): + continue + base_name = fl[:-3] # module name without .py + try: + module = importlib.import_module(base_name) # import plugin + LOG.trace('Imported module: ' +base_name +' file: ' +fl) + except ImportError as e: + LOG.warn(f"Import error: {e}" +' file: ' +fl) + continue + except Exception as ex: + LOG.error('importing ' + base_name + ' Exception: ' + str(ex)) + continue + for elem in dir(module): + obj = getattr(module, elem) + # looking for plugin class in module + if not inspect.isclass(obj) or not hasattr(obj, 'is_plugin') or not obj.is_plugin: + continue + try: # create instance of plugin class + instance = obj(self._app) # name, short_name, app + # needed by bday... + instance._profile=self._app._ms._profile + instance._settings=self._settings + short_name = instance.get_short_name() + is_active = short_name in self._settings['plugins'] + if is_active: + try: + instance.start() + self._app._log('INFO: Started Plugin ' +short_name) + except Exception as e: + self._app._log.error(f"Starting Plugin ' +short_name +' {e}") + # else: LOG.info('Defined Plugin ' +short_name) + except Exception as ex: + LOG.error('in module ' + short_name + ' Exception: ' + str(ex)) + continue + short_name = instance.get_short_name() + self._plugins[short_name] = Plugin(instance, is_active) + LOG.info('Added plugin: ' +short_name +' from file: ' +fl) + break + + def callback_lossless(self, friend_number, data) -> None: + """ + New incoming custom lossless packet (callback) + """ + l = data[0] - pl.LOSSLESS_FIRST_BYTE + name = ''.join(chr(x) for x in data[1:l + 1]) + if name in self._plugins and self._plugins[name].is_active: + self._plugins[name].instance.lossless_packet(''.join(chr(x) for x in data[l + 1:]), friend_number) + + def callback_lossy(self, friend_number, data): + """ + New incoming custom lossy packet (callback) + """ + l = data[0] - pl.LOSSY_FIRST_BYTE + name = ''.join(chr(x) for x in data[1:l + 1]) + if name in self._plugins and self._plugins[name].is_active: + self._plugins[name].instance.lossy_packet(''.join(chr(x) for x in data[l + 1:]), friend_number) + + def friend_online(self, friend_number:int) -> None: + """ + Friend with specified number is online + """ + for plugin in self._plugins.values(): + if plugin.is_active: + plugin.instance.friend_connected(friend_number) + + def get_plugins_list(self) -> list: + """ + Returns list of all plugins + """ + result = [] + for plugin in self._plugins.values(): + try: + result.append([plugin.instance.get_name(), # plugin full name + plugin.is_active, # is enabled + plugin.instance.get_description(), # plugin description + plugin.instance.get_short_name()]) # key - short unique name + except: + continue + + return result + + def plugin_window(self, key): + """ + Return window or None for specified plugin + """ + try: + if key in self._plugins and hasattr(self._plugins[key], 'instance'): + return self._plugins[key].instance.get_window() + except Exception as e: + self._app._log('WARN: ' +key +' _plugins no slot instance: ' +str(e)) + + return None + + def toggle_plugin(self, key) -> None: + """ + Enable/disable plugin + :param key: plugin short name + """ + plugin = self._plugins[key] + if plugin.is_active: + plugin.instance.stop() + else: + plugin.instance.start() + plugin.is_active = not plugin.is_active + if plugin.is_active: + self._settings['plugins'].append(key) + else: + self._settings['plugins'].remove(key) + self._settings.save() + + def command(self, text) -> None: + """ + New command for plugin + """ + text = text.strip() + name = text.split()[0] + if name in self._plugins and self._plugins[name].is_active: + self._plugins[name].instance.command(text[len(name) + 1:]) + + def get_menu(self, num): + """ + Return list of items for menu + """ + result = [] + for plugin in self._plugins.values(): + if not plugin.is_active: + continue + + try: + result.extend(plugin.instance.get_menu(num)) + except: + continue + return result + + def get_message_menu(self, menu, selected_text): + result = [] + for plugin in self._plugins.values(): + if not plugin.is_active: + continue + if not hasattr(plugin.instance, 'get_message_menu'): + name = plugin.instance.get_short_name() + self._app._log('WARN: get_message_menu not found: ' + name) + continue + try: + result.extend(plugin.instance.get_message_menu(menu, selected_text)) + except: + pass + return result + + def stop(self) -> None: + """ + App is closing, stop all plugins + """ + for key in list(self._plugins.keys()): + if self._plugins[key].is_active: + self._plugins[key].instance.close() + del self._plugins[key] + + def reload(self) -> None: + path = util.get_plugins_directory() + if not os.path.exists(path): + self._app._log('WARN: Plugin directory not found: ' + path) + return + + self.stop() + self._app._log('INFO: Reloading plugins from ' +path) + self.load() diff --git a/toxygen/plugins/README.md b/toxygen/plugins/README.md 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 4eb833e..4c6287d 100644 --- a/toxygen/plugins/plugin_super_class.py +++ b/toxygen/plugins/plugin_super_class.py @@ -1,9 +1,10 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- import os -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui +from qtpy import QtCore, QtWidgets + +import utils.ui as util_ui +import common.tox_save as tox_save MAX_SHORT_NAME_LENGTH = 5 @@ -11,7 +12,6 @@ LOSSY_FIRST_BYTE = 200 LOSSLESS_FIRST_BYTE = 160 - def path_to_data(name): """ :param name: plugin unique name @@ -20,46 +20,40 @@ def path_to_data(name): return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/' -def log(name, data): +def log(name, data=''): """ :param name: plugin unique name :param data: data for saving in log """ with open(path_to_data(name) + 'logs.txt', 'a') as fl: - fl.write(bytes(data, 'utf-8') + b'\n') + fl.write(str(data) + '\n') -class PluginSuperClass: +class PluginSuperClass(tox_save.ToxSave): """ - Superclass for all plugins. Plugin is python module with at least one class derived from PluginSuperClass. + Superclass for all plugins. Plugin is Python3 module with at least one class derived from PluginSuperClass. """ is_plugin = True - def __init__(self, name, short_name, tox=None, profile=None, settings=None, encrypt_save=None): + def __init__(self, name, short_name, app): """ - Constructor. In plugin __init__ should take only 4 last arguments + Constructor. In plugin __init__ should take only 1 last argument :param name: plugin full name :param short_name: plugin unique short name (length of short name should not exceed MAX_SHORT_NAME_LENGTH) - :param tox: tox instance - :param profile: profile instance - :param settings: profile settings - :param encrypt_save: LibToxEncryptSave instance. + :param app: App instance """ - self._settings = settings - self._profile = profile - self._tox = tox + tox = getattr(app, '_tox') + super().__init__(tox) + self._settings = getattr(app, '_settings') name = name.strip() short_name = short_name.strip() if not name or not short_name: - 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 - self._encrypt_save = encrypt_save - # ----------------------------------------------------------------------------------------------------------------- # Get methods - # ----------------------------------------------------------------------------------------------------------------- def get_name(self): """ @@ -79,11 +73,19 @@ class PluginSuperClass: """ return self.__doc__ - def get_menu(self, menu, 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 menu: menu instance :param row_number: number of selected row in list of contacts + :return list of tuples (text, handler) + """ + return [] + + def get_message_menu(self, menu, text): + """ + This method creates items for menu which called on right click in message + :param menu: menu instance + :param text: selected text :return list of QAction's """ return [] @@ -94,15 +96,7 @@ class PluginSuperClass: """ return None - def set_tox(self, tox): - """ - New tox instance - """ - self._tox = tox - - # ----------------------------------------------------------------------------------------------------------------- # Plugin was stopped, started or new command received - # ----------------------------------------------------------------------------------------------------------------- def start(self): """ @@ -120,7 +114,7 @@ class PluginSuperClass: """ App is closing """ - pass + self.stop() def command(self, command): """ @@ -128,21 +122,17 @@ class PluginSuperClass: :param command: string with command """ if command == 'help': - msgbox = QtGui.QMessageBox() - title = QtGui.QApplication.translate("PluginWindow", "List of commands for plugin {}", None, QtGui.QApplication.UnicodeUTF8) - msgbox.setWindowTitle(title.format(self._name)) - msgbox.setText(QtGui.QApplication.translate("PluginWindow", "No commands available", None, QtGui.QApplication.UnicodeUTF8)) - msgbox.exec_() + text = util_ui.tr('No commands available') + title = util_ui.tr('List of commands for plugin {}').format(self._name) + util_ui.message_box(text, title) - # ----------------------------------------------------------------------------------------------------------------- # Translations support - # ----------------------------------------------------------------------------------------------------------------- def load_translator(self): """ This method loads translations for GUI """ - app = QtGui.QApplication.instance() + app = QtWidgets.QApplication.instance() langs = self._settings.supported_languages() curr_lang = self._settings['language'] if curr_lang in langs: @@ -153,13 +143,12 @@ class PluginSuperClass: self._translator.load(path_to_data(self._short_name) + lang_path) app.installTranslator(self._translator) - # ----------------------------------------------------------------------------------------------------------------- # Settings loading and saving - # ----------------------------------------------------------------------------------------------------------------- def load_settings(self): """ This method loads settings of plugin and returns raw data + If file doesn't exist this method raises exception """ with open(path_to_data(self._short_name) + 'settings.json', 'rb') as fl: data = fl.read() @@ -173,9 +162,7 @@ class PluginSuperClass: 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): """ @@ -193,15 +180,13 @@ class PluginSuperClass: """ 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/profile.py b/toxygen/profile.py deleted file mode 100644 index 9d5f1ab..0000000 --- a/toxygen/profile.py +++ /dev/null @@ -1,1205 +0,0 @@ -from list_items import * -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui -from friend import * -from settings import * -from toxcore_enums_and_consts import * -from ctypes import * -from util import log, Singleton, curr_directory -from tox_dns import tox_dns -from history import * -from file_transfers import * -import time -import calls -import avwidgets -import plugin_support - - -class Profile(contact.Contact, Singleton): - """ - Profile of current toxygen user. Contains friends list, tox instance - """ - def __init__(self, tox, screen): - """ - :param tox: tox instance - :param screen: ref to main screen - """ - contact.Contact.__init__(self, - tox.self_get_name(), - tox.self_get_status_message(), - screen.user_info, - tox.self_get_address()) - Singleton.__init__(self) - self._screen = screen - self._messages = screen.messages - self._tox = tox - self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number) - self._call = calls.AV(tox.AV) # object with data about calls - self._incoming_calls = set() - self._load_history = True - settings = Settings.get_instance() - self._show_online = settings['show_online_friends'] - screen.online_contacts.setCurrentIndex(int(self._show_online)) - aliases = settings['friends_aliases'] - data = tox.self_get_friend_list() - self._history = History(tox.self_get_public_key()) # connection to db - self._friends, self._active_friend = [], -1 - for i in data: # creates list of friends - tox_id = tox.friend_get_public_key(i) - try: - alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] - except: - alias = '' - item = self.create_friend_item() - name = alias or tox.friend_get_name(i) or tox_id - status_message = tox.friend_get_status_message(i) - if not self._history.friend_exists_in_db(tox_id): - self._history.add_friend_to_db(tox_id) - message_getter = self._history.messages_getter(tox_id) - friend = Friend(message_getter, i, name, status_message, item, tox_id) - friend.set_alias(alias) - self._friends.append(friend) - self.filtration(self._show_online) - - # ----------------------------------------------------------------------------------------------------------------- - # Edit current user's data - # ----------------------------------------------------------------------------------------------------------------- - - def change_status(self): - """ - Changes status of user (online, away, busy) - """ - if self._status is not None: - self.set_status((self._status + 1) % 3) - - def set_status(self, status): - super(Profile, self).set_status(status) - if status is not None: - self._tox.self_set_status(status) - - def set_name(self, value): - if self.name == value: - return - tmp = self.name - super(Profile, self).set_name(value.encode('utf-8')) - self._tox.self_set_name(self._name.encode('utf-8')) - message = QtGui.QApplication.translate("MainWindow", 'User {} is now known as {}', None, - QtGui.QApplication.UnicodeUTF8) - message = message.format(tmp, value) - for friend in self._friends: - friend.append_message(InfoMessage(message, time.time())) - if self._active_friend + 1: - self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - - def set_status_message(self, value): - super(Profile, self).set_status_message(value) - self._tox.self_set_status_message(self._status_message.encode('utf-8')) - - def new_nospam(self): - """Sets new nospam part of tox id""" - import random - self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32 - self._tox_id = self._tox.self_get_address() - return self._tox_id - - # ----------------------------------------------------------------------------------------------------------------- - # Filtration - # ----------------------------------------------------------------------------------------------------------------- - - def filtration(self, show_online=True, filter_str=''): - """ - Filtration of friends list - :param show_online: show online only contacts - :param filter_str: show contacts which name contains this substring - """ - filter_str = filter_str.lower() - settings = Settings.get_instance() - for index, friend in enumerate(self._friends): - friend.visibility = (friend.status is not None or not show_online) and (filter_str in friend.name.lower()) - friend.visibility = friend.visibility or friend.messages or friend.actions - if friend.visibility: - self._screen.friends_list.item(index).setSizeHint(QtCore.QSize(250, - 40 if settings['compact_mode'] else 70)) - else: - self._screen.friends_list.item(index).setSizeHint(QtCore.QSize(250, 0)) - self._show_online, self._filter_string = show_online, filter_str - settings['show_online_friends'] = self._show_online - settings.save() - - def update_filtration(self): - """ - Update list of contacts when 1 of friends change connection status - """ - self.filtration(self._show_online, self._filter_string) - - def get_friend_by_number(self, num): - return list(filter(lambda x: x.number == num, self._friends))[0] - - def get_friend(self, num): - return self._friends[num] - - # ----------------------------------------------------------------------------------------------------------------- - # Work with active friend - # ----------------------------------------------------------------------------------------------------------------- - - def get_active(self): - return self._active_friend - - def set_active(self, value=None): - """ - Change current active friend or update info - :param value: number of new active friend in friend's list or None to update active user's data - """ - if value is None and self._active_friend == -1: # nothing to update - return - if value == -1: # all friends were deleted - self._screen.account_name.setText('') - self._screen.account_status.setText('') - self._active_friend = -1 - self._screen.account_avatar.setHidden(True) - self._messages.clear() - self._screen.messageEdit.clear() - return - try: - self.send_typing(False) - self._screen.typing.setVisible(False) - if value is not None: - if self._active_friend + 1: - try: - self._friends[self._active_friend].curr_text = self._screen.messageEdit.toPlainText() - except: - pass - self._active_friend = value - friend = self._friends[value] - self._friends[value].reset_messages() - self._screen.messageEdit.setPlainText(friend.curr_text) - self._messages.clear() - friend.load_corr() - messages = friend.get_corr()[-PAGE_SIZE:] - self._load_history = False - for message in messages: - if message.get_type() <= 1: - data = message.get_data() - self.create_message_item(data[0], - data[2], - data[1], - data[3]) - elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']: - if message.get_status() is None: - self.create_unsent_file_item(message) - continue - item = self.create_file_transfer_item(message) - if message.get_status() in ACTIVE_FILE_TRANSFERS: # active file transfer - try: - ft = self._file_transfers[(message.get_friend_number(), message.get_file_number())] - ft.set_state_changed_handler(item.update) - ft.signal() - except: - print('Incoming not started transfer - no info found') - elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline - self.create_inline_item(message.get_data()) - else: # info message - data = message.get_data() - self.create_message_item(data[0], - data[2], - '', - data[3]) - self._messages.scrollToBottom() - self._load_history = True - if value in self._call: - self._screen.active_call() - elif value in self._incoming_calls: - self._screen.incoming_call() - else: - self._screen.call_finished() - else: - friend = self._friends[self._active_friend] - - self._screen.account_name.setText(friend.name) - self._screen.account_status.setText(friend.status_message) - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(friend.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if not os.path.isfile(avatar_path): # load default image - avatar_path = curr_directory() + '/images/avatar.png' - os.chdir(os.path.dirname(avatar_path)) - pixmap = QtGui.QPixmap(QtCore.QSize(64, 64)) - pixmap.load(avatar_path) - self._screen.account_avatar.setScaledContents(False) - self._screen.account_avatar.setPixmap(pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio)) - self._screen.account_avatar.repaint() # comment? - self.update_filtration() - except Exception as ex: # no friend found. ignore - log('Friend value: ' + str(value)) - log('Error: ' + str(ex)) - raise - - active_friend = property(get_active, set_active) - - def get_last_message(self): - return self._friends[self._active_friend].get_last_message_text() - - def get_active_number(self): - return self._friends[self._active_friend].number if self._active_friend + 1 else -1 - - def get_active_name(self): - return self._friends[self._active_friend].name if self._active_friend + 1 else '' - - def is_active_online(self): - return self._active_friend + 1 and self._friends[self._active_friend].status is not None - - def new_name(self, number, name): - friend = self.get_friend_by_number(number) - tmp = friend.name - friend.set_name(name) - name = str(name, 'utf-8') - if friend.name == name and tmp != name: - message = QtGui.QApplication.translate("MainWindow", 'User {} is now known as {}', None, QtGui.QApplication.UnicodeUTF8) - message = message.format(tmp, name) - friend.append_message(InfoMessage(message, time.time())) - friend.actions = True - if number == self.get_active_number(): - self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - self.set_active(None) - - def update(self): - if self._active_friend + 1: - self.set_active(self._active_friend) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend connection status callbacks - # ----------------------------------------------------------------------------------------------------------------- - - def send_files(self, friend_number): - friend = self.get_friend_by_number(friend_number) - files = friend.get_unsent_files() - try: - for fl in files: - data = fl.get_data() - if data[1] is not None: - self.send_inline(data[1], data[0], friend_number, True) - else: - self.send_file(data[0], friend_number, True) - friend.clear_unsent_files() - if friend_number == self.get_active_number(): - self.update() - except Exception as ex: - print('Exception in file sending: ' + str(ex)) - - def friend_exit(self, friend_number): - """ - Friend with specified number quit - """ - # TODO: fix and add full file resuming support - self.get_friend_by_number(friend_number).status = None - self.friend_typing(friend_number, False) - if friend_number in self._call: - self._call.finish_call(friend_number, True) - - # ----------------------------------------------------------------------------------------------------------------- - # Typing notifications - # ----------------------------------------------------------------------------------------------------------------- - - def send_typing(self, typing): - """ - Send typing notification to a friend - """ - if Settings.get_instance()['typing_notifications'] and self._active_friend + 1: - friend = self._friends[self._active_friend] - if friend.status is not None: - self._tox.self_set_typing(friend.number, typing) - - def friend_typing(self, friend_number, typing): - """ - Display incoming typing notification - """ - if friend_number == self.get_active_number(): - self._screen.typing.setVisible(typing) - - # ----------------------------------------------------------------------------------------------------------------- - # Private messages - # ----------------------------------------------------------------------------------------------------------------- - - def receipt(self): - i = 0 - while i < self._messages.count() and not self._messages.itemWidget(self._messages.item(i)).mark_as_sent(): - i += 1 - - def send_messages(self, friend_number): - """ - Send 'offline' messages to friend - """ - friend = self.get_friend_by_number(friend_number) - friend.load_corr() - messages = friend.get_unsent_messages() - try: - for message in messages: - self.split_and_send(friend_number, message.get_data()[-1], message.get_data()[0].encode('utf-8')) - friend.inc_receipts() - except: - pass - - def split_and_send(self, number, message_type, message): - """ - Message splitting - :param number: friend's number - :param message_type: type of message - :param message: message text - """ - while len(message) > TOX_MAX_MESSAGE_LENGTH: - size = TOX_MAX_MESSAGE_LENGTH * 4 / 5 - last_part = message[size:TOX_MAX_MESSAGE_LENGTH] - if ' ' in last_part: - index = last_part.index(' ') - elif ',' in last_part: - index = last_part.index(',') - elif '.' in last_part: - index = last_part.index('.') - else: - index = TOX_MAX_MESSAGE_LENGTH - size - 1 - index += size + 1 - self._tox.friend_send_message(number, message_type, message[:index]) - message = message[index:] - self._tox.friend_send_message(number, message_type, message) - - def new_message(self, friend_num, message_type, message): - """ - Current user gets new message - :param friend_num: friend_num of friend who sent message - :param message_type: message type - plain text or action message (/me) - :param message: text of message - """ - if friend_num == self.get_active_number(): # add message to list - t = time.time() - self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) - self._messages.scrollToBottom() - self._friends[self._active_friend].append_message( - TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type)) - else: - friend = self.get_friend_by_number(friend_num) - friend.inc_messages() - friend.append_message( - TextMessage(message, MESSAGE_OWNER['FRIEND'], time.time(), message_type)) - if not friend.visibility: - self.update_filtration() - - def send_message(self, text, friend_num=None): - """ - Send message - :param text: message text - :param friend_num: num of friend - """ - if friend_num is None: - friend_num = self.get_active_number() - if text.startswith('/plugin '): - plugin_support.PluginLoader.get_instance().command(text[8:]) - self._screen.messageEdit.clear() - elif text and friend_num + 1: - text = ''.join(c if c <= '\u10FFFF' else '\u25AF' for c in text) - if text.startswith('/me '): - message_type = TOX_MESSAGE_TYPE['ACTION'] - text = text[4:] - else: - message_type = TOX_MESSAGE_TYPE['NORMAL'] - friend = self.get_friend_by_number(friend_num) - friend.inc_receipts() - if friend.status is not None: - self.split_and_send(friend.number, message_type, text.encode('utf-8')) - if friend.number == self.get_active_number(): - t = time.time() - self.create_message_item(text, t, MESSAGE_OWNER['NOT_SENT'], message_type) - self._screen.messageEdit.clear() - self._messages.scrollToBottom() - friend.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type)) - - def delete_message(self, time): - friend = self._friends[self._active_friend] - friend.delete_message(time) - self._history.delete_message(friend.tox_id, time) - self.update() - - # ----------------------------------------------------------------------------------------------------------------- - # History support - # ----------------------------------------------------------------------------------------------------------------- - - def save_history(self): - """ - Save history to db - """ - s = Settings.get_instance() - if hasattr(self, '_history'): - if s['save_history']: - for friend in self._friends: - if not self._history.friend_exists_in_db(friend.tox_id): - self._history.add_friend_to_db(friend.tox_id) - if not s['save_unsent_only']: - messages = friend.get_corr_for_saving() - else: - messages = friend.get_unsent_messages_for_saving() - self._history.delete_messages(friend.tox_id) - self._history.save_messages_to_db(friend.tox_id, messages) - unsent_messages = friend.get_unsent_messages() - unsent_time = unsent_messages[0].get_data()[2] if len(unsent_messages) else time.time() + 1 - self._history.update_messages(friend.tox_id, unsent_time) - self._history.save() - del self._history - - def clear_history(self, num=None, save_unsent=False): - """ - Clear chat history - """ - if num is not None: - friend = self._friends[num] - friend.clear_corr(save_unsent) - if self._history.friend_exists_in_db(friend.tox_id): - self._history.delete_messages(friend.tox_id) - self._history.delete_friend_from_db(friend.tox_id) - else: # clear all history - for number in range(len(self._friends)): - self.clear_history(number, save_unsent) - if num is None or num == self.get_active_number(): - self.update() - - def load_history(self): - """ - Tries to load next part of messages - """ - if not self._load_history: - return - self._load_history = False - friend = self._friends[self._active_friend] - friend.load_corr(False) - data = friend.get_corr() - if not data: - return - data.reverse() - data = data[self._messages.count():self._messages.count() + PAGE_SIZE] - for message in data: - if message.get_type() <= 1: # text message - data = message.get_data() - self.create_message_item(data[0], - data[2], - data[1], - data[3], - False) - elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']: - if message.get_status() is None: - self.create_unsent_file_item(message) - continue - item = self.create_file_transfer_item(message, False) - if message.get_status() in ACTIVE_FILE_TRANSFERS: # active file transfer - try: - ft = self._file_transfers[(message.get_friend_number(), message.get_file_number())] - ft.set_state_changed_handler(item.update) - ft.signal() - except: - print('Incoming not started transfer - no info found') - elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline - self.create_inline_item(message.get_data()) - else: # info message - data = message.get_data() - self.create_message_item(data[0], - data[2], - '', - data[3]) - self._load_history = True - - def export_history(self, directory): - self._history.export(directory) - - # ----------------------------------------------------------------------------------------------------------------- - # Factories for friend, message and file transfer items - # ----------------------------------------------------------------------------------------------------------------- - - def create_friend_item(self): - """ - Method-factory - :return: new widget for friend instance - """ - item = ContactItem() - elem = QtGui.QListWidgetItem(self._screen.friends_list) - elem.setSizeHint(QtCore.QSize(250, item.height())) - self._screen.friends_list.addItem(elem) - self._screen.friends_list.setItemWidget(elem, item) - return item - - def create_message_item(self, text, time, owner, message_type, append=True): - if message_type == MESSAGE_TYPE['INFO_MESSAGE']: - name = '' - elif owner == MESSAGE_OWNER['FRIEND']: - name = self.get_active_name() - else: - name = self._name - item = MessageItem(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], message_type, self._messages) - elem = QtGui.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - - def create_file_transfer_item(self, tm, append=True): - data = list(tm.get_data()) - data[3] = self.get_friend_by_number(data[4]).name if data[3] else self._name - data.append(self._messages.width()) - item = FileTransferItem(*data) - elem = QtGui.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - return item - - def create_unsent_file_item(self, message, append=True): - data = message.get_data() - item = UnsentFileItem(os.path.basename(data[0]), - os.path.getsize(data[0]) if data[1] is None else len(data[1]), - self.name, - data[2], - self._messages.width()) - elem = QtGui.QListWidgetItem() - elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - - def create_inline_item(self, data, append=True): - elem = QtGui.QListWidgetItem() - item = InlineImageItem(data, self._messages.width(), elem) - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - if append: - self._messages.addItem(elem) - else: - self._messages.insertItem(0, elem) - self._messages.setItemWidget(elem, item) - - # ----------------------------------------------------------------------------------------------------------------- - # Work with friends (remove, block, set alias, get public key) - # ----------------------------------------------------------------------------------------------------------------- - - def set_alias(self, num): - """ - Set new alias for friend - """ - friend = self._friends[num] - name = friend.name - dialog = QtGui.QApplication.translate('MainWindow', - "Enter new alias for friend {} or leave empty to use friend's name:", - None, QtGui.QApplication.UnicodeUTF8) - dialog = dialog.format(name) - title = QtGui.QApplication.translate('MainWindow', - 'Set alias', - None, QtGui.QApplication.UnicodeUTF8) - text, ok = QtGui.QInputDialog.getText(None, - title, - dialog, - QtGui.QLineEdit.Normal, - name) - if ok: - settings = Settings.get_instance() - aliases = settings['friends_aliases'] - if text: - friend.name = bytes(text, 'utf-8') - try: - index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) - aliases[index] = (friend.tox_id, text) - except: - aliases.append((friend.tox_id, text)) - friend.set_alias(text) - else: # use default name - friend.name = bytes(self._tox.friend_get_name(friend.number), 'utf-8') - friend.set_alias('') - try: - index = list(map(lambda x: x[0], aliases)).index(friend.tox_id) - del aliases[index] - except: - pass - settings.save() - if num == self.get_active_number(): - self.update() - - def friend_public_key(self, num): - return self._friends[num].tox_id - - def delete_friend(self, num): - """ - Removes friend from contact list - :param num: number of friend in list - """ - friend = self._friends[num] - settings = Settings.get_instance() - try: - index = list(map(lambda x: x[0], settings['friends_aliases'])).index(friend.tox_id) - del settings['friends_aliases'][index] - except: - pass - if friend.tox_id in settings['notes']: - del settings['notes'][friend.tox_id] - settings.save() - self.clear_history(num) - if self._history.friend_exists_in_db(friend.tox_id): - self._history.delete_friend_from_db(friend.tox_id) - self._tox.friend_delete(friend.number) - del self._friends[num] - self._screen.friends_list.takeItem(num) - if num == self._active_friend: # active friend was deleted - if not len(self._friends): # last friend was deleted - self.set_active(-1) - else: - self.set_active(0) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - - def add_friend(self, tox_id): - """ - Adds friend to list - """ - num = self._tox.friend_add_norequest(tox_id) # num - friend number - item = self.create_friend_item() - try: - if not self._history.friend_exists_in_db(tox_id): - self._history.add_friend_to_db(tox_id) - message_getter = self._history.messages_getter(tox_id) - except Exception as ex: # something is wrong - log('Accept friend request failed! ' + str(ex)) - message_getter = None - friend = Friend(message_getter, num, tox_id, '', item, tox_id) - self._friends.append(friend) - - def block_user(self, tox_id): - """ - Block user with specified tox id (or public key) - delete from friends list and ignore friend requests - """ - tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] - if tox_id == self.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]: - return - settings = Settings.get_instance() - if tox_id not in settings['blocked']: - settings['blocked'].append(tox_id) - settings.save() - try: - num = self._tox.friend_by_public_key(tox_id) - self.delete_friend(num) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - except: # not in friend list - pass - - def unblock_user(self, tox_id, add_to_friend_list): - """ - Unblock user - :param tox_id: tox id of contact - :param add_to_friend_list: add this contact to friend list or not - """ - s = Settings.get_instance() - s['blocked'].remove(tox_id) - s.save() - if add_to_friend_list: - self.add_friend(tox_id) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend requests - # ----------------------------------------------------------------------------------------------------------------- - - def send_friend_request(self, tox_id, message): - """ - Function tries to send request to contact with specified id - :param tox_id: id of new contact or tox dns 4 value - :param message: additional message - :return: True on success else error string - """ - try: - message = message or 'Hello! Add me to your contact list please' - if '@' in tox_id: # value like groupbot@toxme.io - tox_id = tox_dns(tox_id) - if tox_id is None: - raise Exception('TOX DNS lookup failed') - if len(tox_id) == TOX_PUBLIC_KEY_SIZE * 2: # public key - self.add_friend(tox_id) - msgBox = QtGui.QMessageBox() - msgBox.setWindowTitle(QtGui.QApplication.translate("MainWindow", "Friend added", None, QtGui.QApplication.UnicodeUTF8)) - text = (QtGui.QApplication.translate("MainWindow", 'Friend added without sending friend request', None, QtGui.QApplication.UnicodeUTF8)) - msgBox.setText(text) - msgBox.exec_() - else: - result = self._tox.friend_add(tox_id, message.encode('utf-8')) - tox_id = tox_id[:TOX_PUBLIC_KEY_SIZE * 2] - item = self.create_friend_item() - if not self._history.friend_exists_in_db(tox_id): - self._history.add_friend_to_db(tox_id) - message_getter = self._history.messages_getter(tox_id) - friend = Friend(message_getter, result, tox_id, '', item, tox_id) - self._friends.append(friend) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - return True - except Exception as ex: # wrong data - log('Friend request failed with ' + str(ex)) - return str(ex) - - def process_friend_request(self, tox_id, message): - """ - Accept or ignore friend request - :param tox_id: tox id of contact - :param message: message - """ - try: - text = QtGui.QApplication.translate('MainWindow', 'User {} wants to add you to contact list. Message:\n{}', None, QtGui.QApplication.UnicodeUTF8) - info = text.format(tox_id, message) - fr_req = QtGui.QApplication.translate('MainWindow', 'Friend request', None, QtGui.QApplication.UnicodeUTF8) - reply = QtGui.QMessageBox.question(None, fr_req, info, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: # accepted - self.add_friend(tox_id) - data = self._tox.get_savedata() - ProfileHelper.get_instance().save_profile(data) - except Exception as ex: # something is wrong - log('Accept friend request failed! ' + str(ex)) - - # ----------------------------------------------------------------------------------------------------------------- - # Reset - # ----------------------------------------------------------------------------------------------------------------- - - def reset(self, restart): - """ - Recreate tox instance - :param restart: method which calls restart and returns new tox instance - """ - # TODO: file transfers!! - for key in list(self._file_transfers.keys()): - self._file_transfers[key].cancelled() - del self._file_transfers[key] - self._call.stop() - del self._tox - self._tox = restart() - self.status = None - for friend in self._friends: - friend.status = None - self.update_filtration() - - def close(self): - if hasattr(self, '_call'): - self._call.stop() - del self._call - for i in range(len(self._friends)): - del self._friends[0] - - # ----------------------------------------------------------------------------------------------------------------- - # File transfers support - # ----------------------------------------------------------------------------------------------------------------- - - def incoming_file_transfer(self, friend_number, file_number, size, file_name): - """ - New transfer - :param friend_number: number of friend who sent file - :param file_number: file number - :param size: file size in bytes - :param file_name: file name without path - """ - settings = Settings.get_instance() - friend = self.get_friend_by_number(friend_number) - auto = settings['allow_auto_accept'] and friend.tox_id in settings['auto_accept_from_friends'] - inline = (file_name in ALLOWED_FILES) and settings['allow_inline'] - if inline and size < 1024 * 1024: - self.accept_transfer(None, '', friend_number, file_number, size, True) - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['RUNNING'], - size, - file_name, - friend_number, - file_number) - - elif auto: - path = settings['auto_accept_path'] or curr_directory() - self.accept_transfer(None, path + '/' + file_name, friend_number, file_number, size) - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['RUNNING'], - size, - file_name, - friend_number, - file_number) - else: - tm = TransferMessage(MESSAGE_OWNER['FRIEND'], - time.time(), - TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'], - size, - file_name, - friend_number, - file_number) - if friend_number == self.get_active_number(): - item = self.create_file_transfer_item(tm) - if (inline and size < 1024 * 1024) or auto: - self._file_transfers[(friend_number, file_number)].set_state_changed_handler(item.update) - self._messages.scrollToBottom() - else: - friend.actions = True - - friend.append_message(tm) - - def cancel_transfer(self, friend_number, file_number, already_cancelled=False): - """ - Stop transfer - :param friend_number: number of friend - :param file_number: file number - :param already_cancelled: was cancelled by friend - """ - i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['CANCELLED']) - if (friend_number, file_number) in self._file_transfers: - tr = self._file_transfers[(friend_number, file_number)] - if not already_cancelled: - tr.cancel() - else: - tr.cancelled() - if (friend_number, file_number) in self._file_transfers: - del tr - del self._file_transfers[(friend_number, file_number)] - else: - if not already_cancelled: - self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) - if friend_number == self.get_active_number(): - tmp = self._messages.count() + i - if tmp >= 0: - self._messages.itemWidget(self._messages.item(tmp)).update(TOX_FILE_TRANSFER_STATE['CANCELLED'], - 0, -1) - - def cancel_not_started_transfer(self, time): - self._friends[self._active_friend].delete_one_unsent_file(time) - self.update() - - def pause_transfer(self, friend_number, file_number, by_friend=False): - """ - Pause transfer with specified data - """ - tr = self._file_transfers[(friend_number, file_number)] - tr.pause(by_friend) - t = TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'] if by_friend else TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER'] - self.get_friend_by_number(friend_number).update_transfer_data(file_number, t) - - def resume_transfer(self, friend_number, file_number, by_friend=False): - """ - Resume transfer with specified data - """ - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['RUNNING']) - # if (friend_number, file_number) not in self._file_transfers: - # print self._file_transfers - # print (friend_number, file_number) - # return - tr = self._file_transfers[(friend_number, file_number)] - if by_friend: - tr.state = TOX_FILE_TRANSFER_STATE['RUNNING'] - tr.signal() - else: # send seek control? - tr.send_control(TOX_FILE_CONTROL['RESUME']) - - def accept_transfer(self, item, path, friend_number, file_number, size, inline=False): - """ - :param item: transfer item. - :param path: path for saving - :param friend_number: friend number - :param file_number: file number - :param size: file size - :param inline: is inline image - """ - path, file_name = os.path.split(path) - new_file_name, i = file_name, 1 - while os.path.isfile(path + '/' + new_file_name): # file with same name already exists - if '.' in file_name: # has extension - d = file_name.rindex('.') - else: # no extension - d = len(file_name) - new_file_name = file_name[:d] + ' ({})'.format(i) + file_name[d:] - i += 1 - path = os.path.join(path, new_file_name) - if not inline: - rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number) - else: - rt = ReceiveToBuffer(self._tox, friend_number, size, file_number) - self._file_transfers[(friend_number, file_number)] = rt - self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['RESUME']) - if item is not None: - rt.set_state_changed_handler(item.update) - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['RUNNING']) - - def send_screenshot(self, data): - """ - Send screenshot to current active friend - :param data: raw data - png - """ - self.send_inline(data, 'toxygen_inline.png') - - def send_sticker(self, path): - with open(path, 'rb') as fl: - data = fl.read() - self.send_inline(data, 'sticker.png') - - def send_inline(self, data, file_name, friend_number=None, is_resend=False): - friend_number = friend_number or self.get_active_number() - friend = self.get_friend_by_number(friend_number) - if friend.status is None and not is_resend: - m = UnsentFile(file_name, data, time.time()) - friend.append_message(m) - self.update() - return - elif friend.status is None and is_resend: - raise RuntimeError() - st = SendFromBuffer(self._tox, friend.number, data, file_name) - self._file_transfers[(friend.number, st.get_file_number())] = st - tm = TransferMessage(MESSAGE_OWNER['ME'], - time.time(), - TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'], - len(data), - file_name, - friend.number, - st.get_file_number()) - item = self.create_file_transfer_item(tm) - friend.append_message(tm) - st.set_state_changed_handler(item.update) - self._messages.scrollToBottom() - - def send_file(self, path, number=None, is_resend=False): - """ - Send file to current active friend - :param path: file path - :param number: friend_number - :param is_resend: is 'offline' message - """ - friend_number = number or self.get_active_number() - friend = self.get_friend_by_number(friend_number) - if friend.status is None and not is_resend: - m = UnsentFile(path, None, time.time()) - friend.append_message(m) - self.update() - return - elif friend.status is None and is_resend: - print('Error in sending') - raise RuntimeError() - st = SendTransfer(path, self._tox, friend_number) - self._file_transfers[(friend_number, st.get_file_number())] = st - tm = TransferMessage(MESSAGE_OWNER['ME'], - time.time(), - TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'], - os.path.getsize(path), - os.path.basename(path), - friend_number, - st.get_file_number()) - item = self.create_file_transfer_item(tm) - st.set_state_changed_handler(item.update) - self._friends[self._active_friend].append_message(tm) - self._messages.scrollToBottom() - - def incoming_chunk(self, friend_number, file_number, position, data): - """ - Incoming chunk - """ - if (friend_number, file_number) in self._file_transfers: - transfer = self._file_transfers[(friend_number, file_number)] - transfer.write_chunk(position, data) - if transfer.state not in ACTIVE_FILE_TRANSFERS: # finished or cancelled - if type(transfer) is ReceiveAvatar: - self.get_friend_by_number(friend_number).load_avatar() - self.set_active(None) - elif type(transfer) is ReceiveToBuffer: # inline image - print('inline') - inline = InlineImage(transfer.get_data()) - i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['FINISHED'], - inline) - if friend_number == self.get_active_number(): - count = self._messages.count() - if count + i + 1 >= 0: - elem = QtGui.QListWidgetItem() - item = InlineImageItem(transfer.get_data(), self._messages.width(), elem) - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - self._messages.insertItem(count + i + 1, elem) - self._messages.setItemWidget(elem, item) - else: - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['FINISHED']) - del self._file_transfers[(friend_number, file_number)] - - def outgoing_chunk(self, friend_number, file_number, position, size): - """ - Outgoing chunk - """ - if (friend_number, file_number) in self._file_transfers: - transfer = self._file_transfers[(friend_number, file_number)] - transfer.send_chunk(position, size) - if transfer.state not in ACTIVE_FILE_TRANSFERS: # finished or cancelled - del self._file_transfers[(friend_number, file_number)] - if type(transfer) is not SendAvatar: - if type(transfer) is SendFromBuffer and Settings.get_instance()['allow_inline']: # inline - inline = InlineImage(transfer.get_data()) - print('inline') - i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE[ - 'FINISHED'], - inline) - if friend_number == self.get_active_number(): - count = self._messages.count() - if count + i + 1 >= 0: - elem = QtGui.QListWidgetItem() - item = InlineImageItem(transfer.get_data(), self._messages.width(), elem) - elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) - self._messages.insertItem(count + i + 1, elem) - self._messages.setItemWidget(elem, item) - else: - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['FINISHED']) - - # ----------------------------------------------------------------------------------------------------------------- - # Avatars support - # ----------------------------------------------------------------------------------------------------------------- - - def send_avatar(self, friend_number): - """ - :param friend_number: number of friend who should get new avatar - """ - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if not os.path.isfile(avatar_path): # reset image - avatar_path = None - sa = SendAvatar(avatar_path, self._tox, friend_number) - self._file_transfers[(friend_number, sa.get_file_number())] = sa - - def incoming_avatar(self, friend_number, file_number, size): - """ - Friend changed avatar - :param friend_number: friend number - :param file_number: file number - :param size: size of avatar or 0 (default avatar) - """ - ra = ReceiveAvatar(self._tox, friend_number, size, file_number) - if ra.state != TOX_FILE_TRANSFER_STATE['CANCELLED']: - self._file_transfers[(friend_number, file_number)] = ra - else: - self.get_friend_by_number(friend_number).load_avatar() - if self.get_active_number() == friend_number: - self.set_active(None) - - def reset_avatar(self): - super(Profile, self).reset_avatar() - for friend in filter(lambda x: x.status is not None, self._friends): - self.send_avatar(friend.number) - - def set_avatar(self, data): - super(Profile, self).set_avatar(data) - for friend in filter(lambda x: x.status is not None, self._friends): - self.send_avatar(friend.number) - - # ----------------------------------------------------------------------------------------------------------------- - # AV support - # ----------------------------------------------------------------------------------------------------------------- - - def get_call(self): - return self._call - - call = property(get_call) - - def call_click(self, audio=True, video=False): - """User clicked audio button in main window""" - num = self.get_active_number() - if num not in self._call and self.is_active_online(): # start call - self._call(num, audio, video) - self._screen.active_call() - if video: - text = QtGui.QApplication.translate("incoming_call", "Outgoing video call", None, - QtGui.QApplication.UnicodeUTF8) - else: - text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None, - QtGui.QApplication.UnicodeUTF8) - self._friends[self._active_friend].append_message(InfoMessage(text, time.time())) - self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - elif num in self._call: # finish or cancel call if you call with active friend - self.stop_call(num, False) - - def incoming_call(self, audio, video, friend_number): - """ - Incoming call from friend. Only audio is supported now - """ - friend = self.get_friend_by_number(friend_number) - if video: - text = QtGui.QApplication.translate("incoming_call", "Incoming video call", None, - QtGui.QApplication.UnicodeUTF8) - else: - text = QtGui.QApplication.translate("incoming_call", "Incoming audio call", None, - QtGui.QApplication.UnicodeUTF8) - friend.append_message(InfoMessage(text, time.time())) - self._incoming_calls.add(friend_number) - if friend_number == self.get_active_number(): - self._screen.incoming_call() - self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - else: - friend.actions = True - self._call_widget = avwidgets.IncomingCallWidget(friend_number, text, friend.name) - self._call_widget.set_pixmap(friend.get_pixmap()) - self._call_widget.show() - - def accept_call(self, friend_number, audio, video): - """ - Accept incoming call with audio or video - """ - self._call.accept_call(friend_number, audio, video) - self._screen.active_call() - self._incoming_calls.remove(friend_number) - if hasattr(self, '_call_widget'): - del self._call_widget - - def stop_call(self, friend_number, by_friend): - """ - Stop call with friend - """ - if friend_number in self._incoming_calls: - self._incoming_calls.remove(friend_number) - text = QtGui.QApplication.translate("incoming_call", "Call declined", None, QtGui.QApplication.UnicodeUTF8) - else: - text = QtGui.QApplication.translate("incoming_call", "Call finished", None, QtGui.QApplication.UnicodeUTF8) - self._screen.call_finished() - self._call.finish_call(friend_number, by_friend) # finish or decline call - if hasattr(self, '_call_widget'): - del self._call_widget - friend = self.get_friend_by_number(friend_number) - friend.append_message(InfoMessage(text, time.time())) - if friend_number == self.get_active_number(): - self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) - self._messages.scrollToBottom() - - -def tox_factory(data=None, settings=None): - """ - :param data: user data from .tox file. None = no saved data, create new profile - :param settings: current profile settings. None = default settings will be used - :return: new tox instance - """ - if settings is None: - settings = Settings.get_default_settings() - tox_options = Tox.options_new() - tox_options.contents.udp_enabled = settings['udp_enabled'] - tox_options.contents.proxy_type = settings['proxy_type'] - tox_options.contents.proxy_host = bytes(settings['proxy_host'], 'UTF-8') - tox_options.contents.proxy_port = settings['proxy_port'] - tox_options.contents.start_port = settings['start_port'] - tox_options.contents.end_port = settings['end_port'] - tox_options.contents.tcp_port = settings['tcp_port'] - if data: # load existing profile - tox_options.contents.savedata_type = TOX_SAVEDATA_TYPE['TOX_SAVE'] - tox_options.contents.savedata_data = c_char_p(data) - tox_options.contents.savedata_length = len(data) - else: # create new profile - tox_options.contents.savedata_type = TOX_SAVEDATA_TYPE['NONE'] - tox_options.contents.savedata_data = None - tox_options.contents.savedata_length = 0 - return Tox(tox_options) diff --git a/toxygen/settings.py b/toxygen/settings.py deleted file mode 100644 index f4c7746..0000000 --- a/toxygen/settings.py +++ /dev/null @@ -1,268 +0,0 @@ -from platform import system -import json -import os -import locale -from util import Singleton, curr_directory, log -import pyaudio -from toxencryptsave import ToxEncryptSave -import smileys - - -class Settings(dict, Singleton): - """ - Settings of current profile + global app settings - """ - - def __init__(self, name): - Singleton.__init__(self) - self.path = ProfileHelper.get_path() + str(name) + '.json' - self.name = name - if os.path.isfile(self.path): - with open(self.path, 'rb') as fl: - data = fl.read() - inst = ToxEncryptSave.get_instance() - try: - if inst.is_data_encrypted(data): - data = inst.pass_decrypt(data) - info = json.loads(str(data, 'utf-8')) - except Exception as ex: - info = Settings.get_default_settings() - log('Parsing settings error: ' + str(ex)) - super(Settings, self).__init__(info) - self.upgrade() - else: - super(Settings, self).__init__(Settings.get_default_settings()) - self.save() - smileys.SmileyLoader(self) - p = pyaudio.PyAudio() - self.locked = False - self.audio = {'input': p.get_default_input_device_info()['index'], - 'output': p.get_default_output_device_info()['index']} - - @staticmethod - def get_auto_profile(): - path = Settings.get_default_path() + 'toxygen.json' - if os.path.isfile(path): - with open(path) as fl: - data = fl.read() - auto = json.loads(data) - if 'path' in auto and 'name' in auto: - return str(auto['path']), str(auto['name']) - return '', '' - - @staticmethod - def set_auto_profile(path, name): - p = Settings.get_default_path() + 'toxygen.json' - with open(p) as fl: - data = fl.read() - data = json.loads(data) - data['path'] = str(path) - data['name'] = str(name) - with open(p, 'w') as fl: - fl.write(json.dumps(data)) - - @staticmethod - def reset_auto_profile(): - p = Settings.get_default_path() + 'toxygen.json' - with open(p) as fl: - data = fl.read() - data = json.loads(data) - if 'path' in data: - del data['path'] - del data['name'] - with open(p, 'w') as fl: - fl.write(json.dumps(data)) - - @staticmethod - def is_active_profile(path, name): - path = path + name + '.tox' - settings = Settings.get_default_path() + 'toxygen.json' - if os.path.isfile(settings): - with open(settings) as fl: - data = fl.read() - data = json.loads(data) - if 'active_profile' in data: - return path in data['active_profile'] - return False - - @staticmethod - def get_default_settings(): - """ - Default profile settings - """ - return { - 'theme': 'default', - 'ipv6_enabled': True, - 'udp_enabled': True, - 'proxy_type': 0, - 'proxy_host': '127.0.0.1', - 'proxy_port': 9050, - 'start_port': 0, - 'end_port': 0, - 'tcp_port': 0, - 'notifications': True, - 'sound_notifications': False, - 'language': 'English', - 'save_history': False, - 'allow_inline': True, - 'allow_auto_accept': True, - 'auto_accept_path': None, - 'show_online_friends': False, - 'auto_accept_from_friends': [], - 'friends_aliases': [], - 'typing_notifications': False, - 'calls_sound': True, - 'blocked': [], - 'plugins': [], - 'notes': {}, - 'smileys': True, - 'smiley_pack': 'default', - 'mirror_mode': False, - 'width': 920, - 'height': 500, - 'x': 400, - 'y': 400, - 'message_font_size': 14, - 'unread_color': 'red', - 'save_unsent_only': False, - 'compact_mode': False, - 'show_welcome_screen': True - } - - @staticmethod - def supported_languages(): - return { - 'English': 'en_EN', - 'Russian': 'ru_RU', - 'French': 'fr_FR' - } - - def upgrade(self): - default = Settings.get_default_settings() - for key in default: - if key not in self: - print(key) - self[key] = default[key] - self.save() - - def save(self): - text = json.dumps(self) - inst = ToxEncryptSave.get_instance() - if inst.has_password(): - text = bytes(inst.pass_encrypt(bytes(text, 'utf-8'))) - else: - text = bytes(text, 'utf-8') - with open(self.path, 'wb') as fl: - fl.write(text) - - def close(self): - path = Settings.get_default_path() + 'toxygen.json' - if os.path.isfile(path): - with open(path) as fl: - data = fl.read() - app_settings = json.loads(data) - try: - app_settings['active_profile'].remove(str(ProfileHelper.get_path() + self.name + '.tox')) - except: - pass - data = json.dumps(app_settings) - with open(path, 'w') as fl: - fl.write(data) - - def set_active_profile(self): - """ - Mark current profile as active - """ - path = Settings.get_default_path() + 'toxygen.json' - if os.path.isfile(path): - with open(path) as fl: - data = fl.read() - app_settings = json.loads(data) - else: - app_settings = {} - if 'active_profile' not in app_settings: - app_settings['active_profile'] = [] - profilepath = ProfileHelper.get_path() - app_settings['active_profile'].append(str(profilepath + str(self.name) + '.tox')) - data = json.dumps(app_settings) - with open(path, 'w') as fl: - fl.write(data) - - def export(self, path): - text = json.dumps(self) - with open(path + str(self.name) + '.json', 'w') as fl: - fl.write(text) - - @staticmethod - def get_default_path(): - if system() == 'Linux': - return os.getenv('HOME') + '/.config/tox/' - elif system() == 'Windows': - return os.getenv('APPDATA') + '/Tox/' - - -class ProfileHelper(Singleton): - """ - Class with methods for search, load and save profiles - """ - def __init__(self, path, name): - Singleton.__init__(self) - self._path = path + name + '.tox' - self._directory = path - # create /avatars if not exists: - directory = path + 'avatars' - if not os.path.exists(directory): - os.makedirs(directory) - - def open_profile(self): - with open(self._path, 'rb') as fl: - data = fl.read() - if data: - return data - else: - raise IOError('Save file has zero size!') - - def get_dir(self): - return self._directory - - def save_profile(self, data): - inst = ToxEncryptSave.get_instance() - if inst.has_password(): - data = inst.pass_encrypt(data) - with open(self._path, 'wb') as fl: - fl.write(data) - print('Profile saved successfully') - - def export_profile(self, new_path): - new_path += os.path.basename(self._path) - with open(self._path, 'rb') as fin: - data = fin.read() - with open(new_path, 'wb') as fout: - fout.write(data) - print('Profile exported successfully') - - @staticmethod - def find_profiles(): - """ - Find available tox profiles - """ - path = Settings.get_default_path() - result = [] - # check default path - if not os.path.exists(path): - os.makedirs(path) - for fl in os.listdir(path): - if fl.endswith('.tox'): - name = fl[:-4] - result.append((path, name)) - path = curr_directory() - # check current directory - for fl in os.listdir(path): - if fl.endswith('.tox'): - name = fl[:-4] - result.append((path + '/', name)) - return result - - @staticmethod - def get_path(): - return ProfileHelper.get_instance().get_dir() diff --git a/toxygen/smileys/__init__.py b/toxygen/smileys/__init__.py new file mode 100644 index 0000000..e69de29 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/config.json b/toxygen/smileys/default/config.json index 020a529..1b69dbc 100644 --- a/toxygen/smileys/default/config.json +++ b/toxygen/smileys/default/config.json @@ -1 +1 @@ -{ ":523:": "D83DDE12.png", ":)": "D83DDE0A.png", ":-*": "D83DDE1A.png", ":office:": "D83CDFE2.png", ":post_office:": "D83CDFE3.png", ":276:": "D83CDFE0.png", ":o": "D83DDE28.png", ":atm:": "D83CDFE7.png", ":european_post_office:": "D83CDFE4.png", ":hospital:": "D83CDFE5.png", ":convenience_store:": "D83CDFEA.png", ":school:": "D83CDFEB.png", ":hotel:": "D83CDFE8.png", ":love_hotel:": "D83CDFE9.png", ":izakaya_lantern:": "D83CDFEE.png", ":japanese_castle:": "D83CDFEF.png", ":department_store:": "D83CDFEC.png", ":factory:": "D83CDFED.png", ":|": "D83DDE10.png", ":yum:": "D83DDE0B.png", ":snowboarder:": "D83CDFC2.png", ":running:": "D83CDFC3.png", ":basketball:": "D83CDFC0.png", ":checkered_flag:": "D83CDFC1.png", ":trophy:": "D83CDFC6.png", ":horse_racing:": "D83CDFC7.png", ":surfer:": "D83CDFC4.png", ":D": "D83DDE03.png", ":football:": "D83CDFC8.png", ":rugby:": "D83CDFC9.png", ":]": "D83DDE0F.png", ":x": "D83DDE37.png", "⬜": "2B1C.png", ":vhs:": "D83DDCFC.png", ":233:": "D83DDCFA.png", ":212:": "D83DDCFB.png", "⬛": "2B1B.png", ":video_camera:": "D83DDCF9.png", ":signal_strength:": "D83DDCF6.png", ":camera:": "D83DDCF7.png", "➡": "27A1.png", ":no_mobile_phones:": "D83DDCF5.png", ":413:": "D83DDCF2.png", ":649:": "D83DDCF3.png", ":387:": "D83DDCF0.png", ":370:": "D83DDCF1.png", "📮": "D83DDCEE.png", ":110:": "D83DDCEF.png", ":mailbox:": "D83DDCEC.png", ":mailbox_closed:": "D83DDCED.png", ":mailbox_2:": "D83DDCEA.png", ":253:": "D83DDCEB.png", ":incoming_envelope:": "D83DDCE8.png", ":email:": "D83DDCE9.png", ":package:": "D83DDCE6.png", ":e-mail:": "D83DDCE7.png", ":445:": "D83DDCE4.png", ":inbox_tray:": "D83DDCE5.png", ":210:": "D83DDCE2.png", ":295:": "D83DDCE3.png", ":446:": "D83DDCE0.png", ":117:": "D83DDCBC.png", ":82:": "D83DDCDE.png", ":pager:": "D83DDCDF.png", ":scroll:": "D83DDCDC.png", ":memo:": "D83DDCDD.png", ":books:": "D83DDCDA.png", ":name_badge:": "D83DDCDB.png", ":blue_book:": "D83DDCD8.png", ":orange_book:": "D83DDCD9.png", ":book:": "D83DDCD6.png", ":green_book:": "D83DDCD7.png", ":notebook2:": "D83DDCD4.png", ":red_book:": "D83DDCD5.png", ":ledger:": "D83DDCD2.png", ":notebook:": "D83DDCD3.png", ":triangular_ruler:": "D83DDCD0.png", ":bookmark_tabs:": "D83DDCD1.png", ":paperclip:": "D83DDCCE.png", ":straight_ruler:": "D83DDCCF.png", ":pushpin:": "D83DDCCC.png", ":38:": "D83DDCCD.png", ":bar_chart:": "D83DDCCA.png", ":clipboard:": "D83DDCCB.png", ":chart_with_upwards_trend:": "D83DDCC8.png", ":chart_with_downwards_trend:": "D83DDCC9.png", ":calendar:": "D83DDCC6.png", ":card_index:": "D83DDCC7.png", ":page_facing_up:": "D83DDCC4.png", ":date:": "D83DDCC5.png", ":open_file_folder:": "D83DDCC2.png", ":page_with_curl:": "D83DDCC3.png", ":608:": "D83DDCC0.png", ":file_folder:": "D83DDCC1.png", ":265:": "D83DDEA1.png", ":527:": "D83DDEA0.png", ":rowboat:": "D83DDEA3.png", ":ship:": "D83DDEA2.png", ":traffic_light:": "D83DDEA5.png", ":speedboat:": "D83DDEA4.png", ":construction:": "D83DDEA7.png", ":v_traffic_light:": "D83DDEA6.png", ":triangular_flag_on_post:": "D83DDEA9.png", ":rotating_light:": "D83DDEA8.png", ":no_entry_sign:": "D83DDEAB.png", ":door:": "D83DDEAA.png", ":no_smoking:": "D83DDEAD.png", ":smoking:": "D83DDEAC.png", ":408:": "D83DDEAF.png", ":133:": "D83DDEAE.png", "◀": "25C0.png", ":potable_water:": "D83DDEB0.png", ":no_bicycles:": "D83DDEB3.png", ":bike:": "D83DDEB2.png", ":mountain_bicyclist:": "D83DDEB5.png", ":bicyclist:": "D83DDEB4.png", ":no_pedestrians:": "D83DDEB7.png", ":311:": "D83DDEB6.png", ":mens:": "D83DDEB9.png", ":children_crossing:": "D83DDEB8.png", ":restroom:": "D83DDEBB.png", ":womens:": "D83DDEBA.png", ":toilet:": "D83DDEBD.png", ":baby_symbol:": "D83DDEBC.png", ":shower:": "D83DDEBF.png", ":wc:": "D83DDEBE.png", ":helicopter:": "D83DDE81.png", ":rocket:": "D83DDE80.png", ":340:": "D83DDE83.png", ":steam_locomotive:": "D83DDE82.png", ":bullettrain_side:": "D83DDE85.png", ":391:": "D83DDE84.png", ":bullettrain_front:": "D83DDE87.png", ":train2:": "D83DDE86.png", ":station:": "D83DDE89.png", ":light_rail:": "D83DDE88.png", ":railway_car:": "D83DDE8B.png", ":tram:": "D83DDE8A.png", ":oncoming_bus:": "D83DDE8D.png", ":bus:": "D83DDE8C.png", ":busstop:": "D83DDE8F.png", ":trolleybus:": "D83DDE8E.png", ":ambulance:": "D83DDE91.png", ":minibus:": "D83DDE90.png", ":police_car:": "D83DDE93.png", ":pensive:": "D83DDE14.png", ":taxi:": "D83DDE95.png", ":oncoming_police_car:": "D83DDE94.png", ":car:": "D83DDE97.png", ":oncoming_taxi:": "D83DDE96.png", ":blue_car:": "D83DDE99.png", ":oncoming_automobile:": "D83DDE98.png", ":articulated_lorry:": "D83DDE9B.png", ":truck:": "D83DDE9A.png", ":monorail:": "D83DDE9D.png", ":tractor:": "D83DDE9C.png", ":suspension_railway:": "D83DDE9F.png", ":mountain_railway:": "D83DDE9E.png", ":538:": "D83DDCAF.png", ":571:": "D83DDE48.png", ":543:": "D83DDE49.png", ":168:": "D83DDE4A.png", ":raising_hand:": "D83DDE4B.png", ":raised_hands:": "D83DDE4C.png", ":639:": "D83DDE4D.png", ":357:": "D83DDE4E.png", ":pray:": "D83DDE4F.png", ":scream_cat:": "D83DDE40.png", ":no_good:": "D83DDE45.png", ":ok_woman:": "D83DDE46.png", ":bow:": "D83DDE47.png", ":non-potable_water:": "D83DDEB1.png", "⁉": "2049.png", ":mahjong:": "D83CDC04.png", "↪": "21AA.png", "↩": "21A9.png", "⌚": "231A.png", ":purple_heart:": "D83DDC9C.png", "↗": "2197.png", "↖": "2196.png", "↕": "2195.png", "↔": "2194.png", "↙": "2199.png", "↘": "2198.png", "⚾": "26BE.png", "⚽": "26BD.png", ":house_with_garden:": "D83CDFE1.png", "⚡": "26A1.png", "⚠": "26A0.png", "⚫": "26AB.png", ":rage:": "D83DDE21.png", "⚓": "2693.png", "0⃣": "003020E3.png", "1⃣": "003120E3.png", "2⃣": "003220E3.png", "3⃣": "003320E3.png", "4⃣": "003420E3.png", "5⃣": "003520E3.png", "6⃣": "003620E3.png", "7⃣": "003720E3.png", "8⃣": "003820E3.png", "9⃣": "003920E3.png", ":10:": "D83DDD1F.png", "⭐": "2B50.png", "⚪": "26AA.png", "⭕": "2B55.png", ":1234:": "D83DDD22.png", "Ⓜ": "24C2.png", ":european_castle:": "D83CDFF0.png", "⌛": "231B.png", "➗": "2797.png", ":((": "D83DDE29.png", ":high_heel:": "D83DDC60.png", ":swimmer:": "D83CDFCA.png", ":busts_in_silhouette:": "D83DDC65.png", "8-)": "D83DDE0D.png", "➕": "2795.png", "♈": "2648.png", ":two_women_holding_hands:": "D83DDC6D.png", ":424:": "D83DDCB9.png", ":money_with_wings:": "D83DDCB8.png", ":computer:": "D83DDCBB.png", ":348:": "D83DDCBA.png", ":minidisc:": "D83DDCBD.png", "8o": "D83DDE32.png", ":dvd:": "D83DDCBF.png", ":floppy_disc:": "D83DDCBE.png", ":59:": "D83DDCB1.png", ":moneybag:": "D83DDCB0.png", ":credit_card:": "D83DDCB3.png", ":$:": "D83DDCB2.png", ":dollar:": "D83DDCB5.png", "💴": "D83DDCB4.png", ":pound:": "D83DDCB7.png", ":yen:": "D83DDCB6.png", ":shit:": "D83DDCA9.png", ":dash:": "D83DDCA8.png", ":dizzy:": "D83DDCAB.png", ":muscle:": "D83DDCAA.png", ":thought_balloon:": "D83DDCAD.png", ":speech_balloon:": "D83DDCAC.png", "8|": "D83DDE33.png", ":28:": "D83DDCAE.png", ":bulb:": "D83DDCA1.png", ":534:": "D83DDCA0.png", ":bomb:": "D83DDCA3.png", ":297:": "D83DDCA2.png", ":boom:": "D83DDCA5.png", ":zzz:": "D83DDCA4.png", ":661:": "D83DDCA7.png", ":sweat_drops:": "D83DDCA6.png", ":blue_heart:": "D83DDC99.png", ":cupid:": "D83DDC98.png", ":yellow_heart:": "D83DDC9B.png", ":green_heart:": "D83DDC9A.png", ":56:": "D83DDC9D.png", ":32:": "D83DDC9F.png", ":revolving_hearts:": "D83DDC9E.png", ":couple_with_heart:": "D83DDC91.png", ":590:": "D83DDC90.png", ":336:": "D83DDC93.png", ":wedding:": "D83DDC92.png", ":247:": "D83DDC95.png", ":broken_heart:": "D83DDC94.png", ":heartpulse:": "D83DDC97.png", ":sparkling_heart:": "D83DDC96.png", ":syringe:": "D83DDC89.png", ":420:": "D83DDC88.png", ":kiss:": "D83DDC8B.png", ":pill:": "D83DDC8A.png", ":ring:": "D83DDC8D.png", ":383:": "D83DDC8C.png", "💏": "D83DDC8F.png", ":gem:": "D83DDC8E.png", ":information_desk_person:": "D83DDC81.png", ":skull:": "D83DDC80.png", ":dancer:": "D83DDC83.png", ":guardsman:": "D83DDC82.png", ":nail_care:": "D83DDC85.png", ":lipstick:": "D83DDC84.png", ":haircut:": "D83DDC87.png", ":massage:": "D83DDC86.png", ":bride_with_veil:": "D83DDC70.png", ":person_with_blond_hair:": "D83DDC71.png", ":man_with_gua_pi_mao:": "D83DDC72.png", ":man_with_turban:": "D83DDC73.png", ":older_man:": "D83DDC74.png", ":older_woman:": "D83DDC75.png", ":baby:": "D83DDC76.png", ":construction_worker:": "D83DDC77.png", ":princess:": "D83DDC78.png", ":japanese_ogre:": "D83DDC79.png", ":japanese_goblin:": "D83DDC7A.png", ":ghost:": "D83DDC7B.png", ":angel:": "D83DDC7C.png", ":alien:": "D83DDC7D.png", ":249:": "D83DDC7E.png", ":imp:": "D83DDC7F.png", "=(": "D83DDE20.png", ":sandal:": "D83DDC61.png", ":boot:": "D83DDC62.png", ":feet:": "D83DDC63.png", ":bust_in_silhouette:": "D83DDC64.png", ":mans_shoe:": "D83DDC5E.png", "👦": "D83DDC66.png", ":332:": "D83DDC67.png", ":man:": "D83DDC68.png", ":woman:": "D83DDC69.png", ":family:": "D83DDC6A.png", ":couple:": "D83DDC6B.png", ":two_men_holding_hands:": "D83DDC6C.png", ":point_up:": "261D.png", ":cop:": "D83DDC6E.png", ":290:": "D83DDC6F.png", ":open_hands:": "D83DDC50.png", ":crown:": "D83DDC51.png", ":womans_hat:": "D83DDC52.png", ":eyeglasses:": "D83DDC53.png", ":necktie:": "D83DDC54.png", ":shirt:": "D83DDC55.png", ":jeans:": "D83DDC56.png", ":dress:": "D83DDC57.png", ":kimono:": "D83DDC58.png", ":bikini:": "D83DDC59.png", ":womans_clothes:": "D83DDC5A.png", ":purse:": "D83DDC5B.png", ":handbag:": "D83DDC5C.png", ":pouch:": "D83DDC5D.png", "xD": "D83DDE06.png", ":shoe:": "D83DDC5F.png", ":eyes:": "D83DDC40.png", ":ear:": "D83DDC42.png", ":nose:": "D83DDC43.png", ":lips:": "D83DDC44.png", ":tongue:": "D83DDC45.png", ":point_up_2:": "D83DDC46.png", ":point_down:": "D83DDC47.png", ":point_left:": "D83DDC48.png", ":point_right:": "D83DDC49.png", ":facepunch:": "D83DDC4A.png", ":wave:": "D83DDC4B.png", ":ok:": "D83DDC4C.png", ":like:": "D83DDC4D.png", ":dislike:": "D83DDC4E.png", ":clap:": "D83DDC4F.png", ":^D": "D83DDE1B.png", ":3": "D83DDE19.png", ":kissing_heart:": "D83DDE18.png", ":((": "D83DDE1F.png", ":(": "D83DDE1E.png", ":stuck_out_tongue_closed_eyes:": "D83DDE1D.png", ":stuck_out_tongue_winking_eye:": "D83DDE1C.png", ":sweat:": "D83DDE13.png", ":593:": "D83DDE11.png", ":^)": "D83DDE17.png", ":confounded:": "D83DDE16.png", ":\\": "D83DDE15.png", ";-)": "D83DDE09.png", ":smiling_imp:": "D83DDE08.png", "B)": "D83DDE0E.png", "3-)": "D83DDE0C.png", ":*(": "D83DDE02.png", ":650:": "D83DDE01.png", ":smile:": "D83DDE00.png", ":610:": "D83DDE07.png", ":sweat_smile:": "D83DDE05.png", ":smile2:": "D83DDE04.png", ":heart_eyes_cat:": "D83DDE3B.png", ":smile_cat:": "D83DDE3A.png", ":joy_cat:": "D83DDE39.png", ":smiley_cat:": "D83DDE38.png", ":crying_cat_face:": "D83DDE3F.png", ":pouting_cat:": "D83DDE3E.png", ":kissing_cat:": "D83DDE3D.png", ":smirk_cat:": "D83DDE3C.png", ":scream:": "D83DDE31.png", ";o": "D83DDE30.png", ":no_mouth:": "D83DDE36.png", ":dizzy_face:": "D83DDE35.png", ":sleeping:": "D83DDE34.png", ":tired_face:": "D83DDE2B.png", ":389:": "D83DDE2A.png", ":0": "D83DDE2F.png", ":O": "D83DDE2E.png", ":sob:": "D83DDE2D.png", ":p": "D83DDE2C.png", ":58:": "D83DDE23.png", ":'(": "D83DDE22.png", ":206:": "D83DDE27.png", ":622:": "D83DDE26.png", ":461:": "D83DDE25.png", ":134:": "D83DDE24.png", ":25:": "D83CDFAD.png", ":244:": "D83CDFAC.png", ":darts:": "D83CDFAF.png", ":149:": "D83CDFAE.png", ":tophat:": "D83CDFA9.png", ":art:": "D83CDFA8.png", ":113:": "D83CDFAB.png", ":circus_tent:": "D83CDFAA.png", ":movie_camera:": "D83CDFA5.png", ":microphone:": "D83CDFA4.png", ":219:": "D83CDFA7.png", ":313:": "D83CDFA6.png", ":ferris_wheel:": "D83CDFA1.png", ":carousel_horse:": "D83CDFA0.png", ":637:": "D83CDFA3.png", ":roller_coaster:": "D83CDFA2.png", ":119:": "D83CDFBD.png", ":129:": "D83CDFBC.png", ":127:": "D83CDFBF.png", ":tennis:": "D83CDFBE.png", ":musical_keyboard:": "D83CDFB9.png", ":guitar:": "D83CDFB8.png", ":violin:": "D83CDFBB.png", ":trumpet:": "D83CDFBA.png", ":647:": "D83CDFB5.png", ":43:": "D83CDFB4.png", ":saxophone:": "D83CDFB7.png", ":85:": "D83CDFB6.png", ":8ball:": "D83CDFB1.png", ":565:": "D83CDFB0.png", ":bowling:": "D83CDFB3.png", ":game_die:": "D83CDFB2.png", ":245:": "D83CDF8D.png", ":crossed_flags:": "D83CDF8C.png", ":lags:": "D83CDF8F.png", ":dolls:": "D83CDF8E.png", ":tada:": "D83CDF89.png", ":balloon:": "D83CDF88.png", ":tanabata_tree:": "D83CDF8B.png", ":confetti_ball:": "D83CDF8A.png", ":santa:": "D83CDF85.png", ":christmas_tree:": "D83CDF84.png", ":sparkler:": "D83CDF87.png", ":fireworks:": "D83CDF86.png", ":gift:": "D83CDF81.png", ":42:": "D83CDF80.png", ":jack_o_lantern:": "D83CDF83.png", ":3:": "D83CDF82.png", ":444:": "D83DDCE1.png", ":61:": "D83CDF91.png", ":386:": "D83CDF90.png", ":440:": "D83CDF93.png", ":school_satchel:": "D83CDF92.png", "⛵": "26F5.png", "⛲": "26F2.png", "⛳": "26F3.png", "⛽": "26FD.png", "⛺": "26FA.png", "⛪": "26EA.png", "⛔": "26D4.png", "⛄": "26C4.png", "⛅": "26C5.png", "⛎": "26CE.png", "✨": "2728.png", "✴": "2734.png", "✳": "2733.png", "✌": "270C.png", "✏": "270F.png", "✉": "2709.png", "✈": "2708.png", "✋": "270B.png", "✊": "270A.png", "✅": "2705.png", "✂": "2702.png", "✔": "2714.png", "✖": "2716.png", "✒": "2712.png", ":crystal_ball:": "D83DDD2E.png", ":telescope:": "D83DDD2D.png", ":microscope:": "D83DDD2C.png", ":gun:": "D83DDD2B.png", ":hocho:": "D83DDD2A.png", ":63:": "D83DDD29.png", ":504:": "D83DDD28.png", ":301:": "D83DDD27.png", ":flashlight:": "D83DDD26.png", ":fire:": "D83DDD25.png", ":abc:": "D83DDD24.png", ":274:": "D83DDD23.png", ":abcd:": "D83DDD21.png", ":ABCD:": "D83DDD20.png", ":down:": "D83DDD3D.png", ":up:": "D83DDD3C.png", ":144:": "D83DDD3B.png", ":371:": "D83DDD3A.png", ":46:": "D83DDD39.png", ":240:": "D83DDD38.png", ":48:": "D83DDD37.png", ":298:": "D83DDD36.png", ":29:": "D83DDD35.png", ":250:": "D83DDD34.png", ":white_square_button:": "D83DDD33.png", ":black_square_button:": "D83DDD32.png", ":trident:": "D83DDD31.png", ":280:": "D83DDD0F.png", ":585:": "D83DDD0E.png", ":204:": "D83DDD0D.png", ":electric_plug:": "D83DDD0C.png", ":battery:": "D83DDD0B.png", ":speaker:": "D83DDD09.png", ":nosound:": "D83DDD07.png", ":low_brightness:": "D83DDD06.png", ":397:": "D83DDD05.png", ":268:": "D83DDD04.png", ":597:": "D83DDD03.png", ":584:": "D83DDD02.png", ":517:": "D83DDD01.png", ":374:": "D83DDD00.png", ":underage:": "D83DDD1E.png", ":173:": "D83DDD1D.png", ":soon:": "D83DDD1C.png", ":on:": "D83DDD1B.png", ":end:": "D83DDD1A.png", ":back:": "D83DDD19.png", ":583:": "D83DDD18.png", ":230:": "D83DDD17.png", ":629:": "D83DDD16.png", ":163:": "D83DDD15.png", ":426:": "D83DDD14.png", ":lock:": "D83DDD13.png", ":232:": "D83DDD12.png", ":key:": "D83DDD11.png", ":328:": "D83DDD10.png", ":black_joker:": "D83CDCCF.png", "©": "00A9.png", ":174:": "D83CDF64.png", ":235:": "D83CDF65.png", ":132:": "D83CDF66.png", ":417:": "D83CDF67.png", ":524:": "D83CDF60.png", ":105:": "D83CDF61.png", ":229:": "D83CDF62.png", ":50:": "D83CDF63.png", ":499:": "D83CDF6C.png", ":415:": "D83CDF6D.png", ":467:": "D83CDF6E.png", ":546:": "D83CDF6F.png", ":74:": "D83CDF68.png", ":306:": "D83CDF69.png", ":363:": "D83CDF6A.png", ":148:": "D83CDF6B.png", ":654:": "D83CDF74.png", ":251:": "D83CDF75.png", ":641:": "D83CDF76.png", ":439:": "D83CDF77.png", ":320:": "D83CDF70.png", ":44:": "D83CDF71.png", ":291:": "D83CDF72.png", ":495:": "D83CDF73.png", ":554:": "D83CDF7C.png", ":513:": "D83CDF78.png", ":384:": "D83CDF79.png", ":9:": "D83CDF7A.png", ":90:": "D83CDF7B.png", ":522:": "D83CDF44.png", ":498:": "D83CDF45.png", ":266:": "D83CDF46.png", ":16:": "D83CDF47.png", ":152:": "D83CDF40.png", ":560:": "D83CDF41.png", ":454:": "D83CDF42.png", ":302:": "D83CDF43.png", ":483:": "D83CDF4C.png", ":664:": "D83CDF4D.png", ":96:": "D83CDF4E.png", ":663:": "D83CDF4F.png", ":493:": "D83CDF48.png", ":539:": "D83CDF49.png", ":270:": "D83CDF4A.png", ":643:": "D83CDF4B.png", ":17:": "D83CDF54.png", ":343:": "D83CDF55.png", ":399:": "D83CDF56.png", ":450:": "D83CDF57.png", ":293:": "D83CDF50.png", ":184:": "D83CDF51.png", ":47:": "D83CDF52.png", ":403:": "D83CDF53.png", ":400:": "D83CDF5C.png", ":410:": "D83CDF5D.png", ":322:": "D83CDF5E.png", ":482:": "D83CDF5F.png", ":143:": "D83CDF58.png", ":542:": "D83CDF59.png", ":588:": "D83CDF5A.png", ":529:": "D83CDF5B.png", "☺": "263A.png", ":111:": "D83CDF08.png", "☑": "2611.png", "☕": "2615.png", ":22:": "D83CDF15.png", "☎": "260E.png", "☁": "2601.png", "☀": "2600.png", ":14:": "D83DDC33.png", ":409:": "D83DDC32.png", ":382:": "D83DDC31.png", ":460:": "D83DDC30.png", ":484:": "D83DDC37.png", ":288:": "D83DDC36.png", ":594:": "D83DDC35.png", ":591:": "D83DDC34.png", ":372:": "D83DDC3B.png", ":563:": "D83DDC3A.png", ":596:": "D83DDC39.png", ":595:": "D83DDC38.png", ":367:": "D83DDC3E.png", ":376:": "D83DDC3D.png", ":305:": "D83DDC3C.png", ":281:": "D83DDC23.png", ":452:": "D83DDC22.png", ":388:": "D83DDC21.png", ":39:": "D83DDC20.png", ":36:": "D83DDC27.png", ":189:": "D83DDC26.png", ":500:": "D83DDC25.png", ":277:": "D83DDC24.png", ":459:": "D83DDC2B.png", ":486:": "D83DDC2A.png", ":662:": "D83DDC28.png", ":509:": "D83DDC2F.png", ":365:": "D83DDC2E.png", ":34:": "D83DDC2D.png", ":319:": "D83DDC2C.png", ":118:": "D83DDC13.png", ":463:": "D83DDC12.png", ":220:": "D83DDC11.png", ":653:": "D83DDC10.png", ":599:": "D83DDC17.png", ":603:": "D83DDC16.png", ":489:": "D83DDC15.png", ":552:": "D83DDC14.png", ":181:": "D83DDC1B.png", ":208:": "D83DDC1A.png", ":19:": "D83DDC19.png", ":580:": "D83DDC18.png", ":398:": "D83DDC1F.png", ":310:": "D83DDC1E.png", ":4:": "D83DDC1D.png", ":564:": "D83DDC1C.png", ":135:": "D83DDC03.png", ":566:": "D83DDC02.png", ":211:": "D83DDC01.png", ":631:": "D83DDC00.png", ":496:": "D83DDC07.png", ":263:": "D83DDC06.png", ":447:": "D83DDC05.png", ":283:": "D83DDC04.png", ":607:": "D83DDC0B.png", ":23:": "D83DDC0A.png", ":634:": "D83DDC09.png", ":435:": "D83DDC08.png", ":349:": "D83DDC0F.png", ":53:": "D83DDC0E.png", ":101:": "D83DDC0D.png", ":567:": "D83DDC0C.png", "◻": "25FB.png", "❤": "2764.png", "◼": "25FC.png", "◽": "25FD.png", "◾": "25FE.png", "➿": "27BF.png", "⬅": "2B05.png", ":531:": "D83DDE92.png", ":116:": "D83CDD8E.png", ":466:": "D83CDD95.png", ":545:": "D83CDD94.png", ":202:": "D83CDD97.png", ":100:": "D83CDD96.png", ":CL:": "D83CDD91.png", ":free:": "D83CDD93.png", ":cool:": "D83CDD92.png", ":UP!:": "D83CDD99.png", ":SOS:": "D83CDD98.png", ":VS:": "D83CDD9A.png", ":525:": "D83CDF20.png", ":614:": "D83CDF37.png", ":13:": "D83CDF35.png", ":108:": "D83CDF34.png", ":289:": "D83CDF33.png", ":390:": "D83CDF32.png", ":222:": "D83CDF31.png", ":92:": "D83CDF30.png", ":205:": "D83CDF3F.png", ":7:": "D83CDF3E.png", ":142:": "D83CDF3D.png", ":536:": "D83CDF3C.png", ":153:": "D83CDF3B.png", ":307:": "D83CDF3A.png", ":304:": "D83CDF39.png", ":18:": "D83CDF38.png", ":558:": "D83CDF07.png", ":71:": "D83CDF06.png", ":421:": "D83CDF05.png", ":485:": "D83CDF04.png", ":223:": "D83CDF03.png", ":269:": "D83CDF02.png", ":574:": "D83CDF01.png", ":124:": "D83CDF00.png", ":65:": "D83CDF0F.png", ":15:": "D83CDF0E.png", ":411:": "D83CDF0D.png", ":272:": "D83CDF0C.png", ":335:": "D83CDF0B.png", ":379:": "D83CDF0A.png", ":192:": "D83CDF09.png", ":359:": "D83CDF17.png", ":436:": "D83CDF16.png", ":479:": "D83CDF14.png", ":231:": "D83CDF13.png", ":528:": "D83CDF12.png", ":341:": "D83CDF11.png", ":491:": "D83CDF10.png", ":520:": "D83CDF1F.png", ":185:": "D83CDF1E.png", ":568:": "D83CDF1D.png", ":40:": "D83CDF1C.png", ":393:": "D83CDF1B.png", ":275:": "D83CDF1A.png", ":72:": "D83CDF19.png", ":246:": "D83CDF18.png", "❓": "2753.png", "❗": "2757.png", "❔": "2754.png", "❕": "2755.png", "➰": "27B0.png", "❄": "2744.png", "❎": "274E.png", "❌": "274C.png", "⏰": "23F0.png", "⏳": "23F3.png", "⏩": "23E9.png", "⏪": "23EA.png", "⏫": "23EB.png", "⏬": "23EC.png", ":106:": "D83DDEC4.png", ":465:": "D83DDEC2.png", "▶": "25B6.png", ":236:": "D83DDEC0.png", ":271:": "D83DDEC1.png", "⤵": "2935.png", "⤴": "2934.png", "▫": "25AB.png", "▪": "25AA.png", "❇": "2747.png", ":510:": "D83CDD7E.png", ":480:": "D83CDD7F.png", "〽": "303D.png", "™": "2122.png", "〰": "3030.png", ":351:": "D83CDD70.png", ":237:": "D83CDD71.png", "ℹ": "2139.png", "☔": "2614.png", "⬇": "2B07.png", "➖": "2796.png", ":358:": "D83CDE01.png", "⬆": "2B06.png", "♿": "267F.png", "♻": "267B.png", "♨": "2668.png", "♦": "2666.png", "♥": "2665.png", "♣": "2663.png", "♠": "2660.png", "®": "00AE.png", "♒": "2652.png", "♓": "2653.png", "♐": "2650.png", "♑": "2651.png", "♎": "264E.png", "♏": "264F.png", "♌": "264C.png", "♍": "264D.png", "♊": "264A.png", "♋": "264B.png", ":592:": "D83DDCF4.png", "♉": "2649.png", ":472:": "D83DDDFB.png", ":84:": "D83DDDFE.png", ":254:": "D83DDDFF.png", ":316:": "D83DDDFC.png", ":226:": "D83DDDFD.png", "‼": "203C.png", ":621:": "D83DDEC5.png", ":140:": "D83DDEC3.png", ":tox:": "tox.png", ":wtox:": "wtox.png", ":td:": "td.png", ":cr:": "cr.png", ":gd:": "gd.png", ":si:": "si.png", ":ni:": "ni.png", ":gs:": "gs.png", ":er:": "er.png", ":pg:": "pg.png", ":jp:": "jp.png", ":hu:": "hu.png", ":is:": "is.png", ":ro:": "ro.png", ":europeanunion:": "europeanunion.png", ":bj:": "bj.png", ":mp:": "mp.png", ":tl:": "tl.png", ":gl:": "gl.png", ":sa:": "sa.png", ":na:": "na.png", ":as:": "as.png", ":lt:": "lt.png", ":za:": "za.png", ":bb:": "bb.png", ":mx:": "mx.png", ":np:": "np.png", ":ru:": "ru.png", ":vg:": "vg.png", ":sy:": "sy.png", ":fam:": "fam.png", ":bo:": "bo.png", ":um:": "um.png", ":fr:": "fr.png", ":sh:": "sh.png", ":gr:": "gr.png", ":nu:": "nu.png", ":mo:": "mo.png", ":ir:": "ir.png", ":de:": "de.png", ":tt:": "tt.png", ":lb:": "lb.png", ":ci:": "ci.png", ":tm:": "tm.png", ":pl:": "pl.png", ":vi:": "vi.png", ":hr:": "hr.png", ":ar:": "ar.png", ":bz:": "bz.png", ":ae:": "ae.png", ":lu:": "lu.png", ":cz:": "cz.png", ":dm:": "dm.png", ":ca:": "ca.png", ":yt:": "yt.png", ":va:": "va.png", ":ee:": "ee.png", ":pt:": "pt.png", ":az:": "az.png", ":br:": "br.png", ":dk:": "dk.png", ":am:": "am.png", ":tw:": "tw.png", ":gq:": "gq.png", ":et:": "et.png", ":pe:": "pe.png", ":pr:": "pr.png", ":kr:": "kr.png", ":st:": "st.png", ":ki:": "ki.png", ":bt:": "bt.png", ":mr:": "mr.png", ":nz:": "nz.png", ":lc:": "lc.png", ":mm:": "mm.png", ":ch:": "ch.png", ":gb:": "gb.png", ":so:": "so.png", ":nc:": "nc.png", ":gy:": "gy.png", ":scotland:": "scotland.png", ":pm:": "pm.png", ":ug:": "ug.png", ":sv:": "sv.png", ":gf:": "gf.png", ":kz:": "kz.png", ":lr:": "lr.png", ":cy:": "cy.png", ":ad:": "ad.png", ":mz:": "mz.png", ":fj:": "fj.png", ":sg:": "sg.png", ":au:": "au.png", ":bs:": "bs.png", ":england:": "england.png", ":al:": "al.png", ":bd:": "bd.png", ":fi:": "fi.png", ":md:": "md.png", ":rs:": "rs.png", ":ps:": "ps.png", ":uy:": "uy.png", ":tr:": "tr.png", ":kh:": "kh.png", ":zm:": "zm.png", ":ga:": "ga.png", ":ml:": "ml.png", ":hk:": "hk.png", ":ky:": "ky.png", ":it:": "it.png", ":cn:": "cn.png", ":ag:": "ag.png", ":ls:": "ls.png", ":tz:": "tz.png", ":wales:": "wales.png", ":bm:": "bm.png", ":co:": "co.png", ":tk:": "tk.png", ":gi:": "gi.png", ":eg:": "eg.png", ":ie:": "ie.png", ":at:": "at.png", ":mc:": "mc.png", ":by:": "by.png", ":ao:": "ao.png", ":lk:": "lk.png", ":me:": "me.png", ":be:": "be.png", ":cg:": "cg.png", ":sn:": "sn.png", ":kp:": "kp.png", ":mq:": "mq.png", ":kg:": "kg.png", ":bv:": "bv.png", ":mt:": "mt.png", ":qa:": "qa.png", ":la:": "la.png", ":th:": "th.png", ":cv:": "cv.png", ":id:": "id.png", ":sm:": "sm.png", ":ne:": "ne.png", ":il:": "il.png", ":bi:": "bi.png", ":jm:": "jm.png", ":af:": "af.png", ":bn:": "bn.png", ":ma:": "ma.png", ":gh:": "gh.png", ":se:": "se.png", ":pk:": "pk.png", ":ua:": "ua.png", ":to:": "to.png", ":aw:": "aw.png", ":gt:": "gt.png", ":mk:": "mk.png", ":an:": "an.png", ":bf:": "bf.png", ":tc:": "tc.png", ":nl:": "nl.png", ":cf:": "cf.png", ":gp:": "gp.png", ":ly:": "ly.png", ":mw:": "mw.png", ":bw:": "bw.png", ":cu:": "cu.png", ":mn:": "mn.png", ":nf:": "nf.png", ":sl:": "sl.png", ":io:": "io.png", ":cx:": "cx.png", ":kw:": "kw.png", ":py:": "py.png", ":us:": "us.png", ":pa:": "pa.png", ":kn:": "kn.png", ":zw:": "zw.png", ":cm:": "cm.png", ":fm:": "fm.png", ":sd:": "sd.png", ":ph:": "ph.png", ":fk:": "fk.png", ":tj:": "tj.png", ":ai:": "ai.png", ":li:": "li.png", ":mg:": "mg.png", ":bg:": "bg.png", ":ms:": "ms.png", ":gw:": "gw.png", ":vc:": "vc.png", ":hn:": "hn.png", ":ke:": "ke.png", ":ax:": "ax.png", ":mv:": "mv.png", ":tf:": "tf.png", ":vu:": "vu.png", ":sk:": "sk.png", ":ng:": "ng.png", ":vn:": "vn.png", ":in:": "in.png", ":sr:": "sr.png", ":iq:": "iq.png", ":hm:": "hm.png", ":pw:": "pw.png", ":ws:": "ws.png", ":jo:": "jo.png", ":km:": "km.png", ":bh:": "bh.png", ":tn:": "tn.png", ":cl:": "cl.png", ":gn:": "gn.png", ":sc:": "sc.png", ":eh:": "eh.png", ":ba:": "ba.png", ":re:": "re.png", ":lv:": "lv.png", ":cd:": "cd.png", ":ve:": "ve.png", ":om:": "om.png", ":cs:": "cs.png", ":ge:": "ge.png", ":tg:": "tg.png", ":es:": "es.png", ":sj:": "sj.png", ":pf:": "pf.png", ":ht:": "ht.png", ":dj:": "dj.png", ":ec:": "ec.png", ":tv:": "tv.png", ":wf:": "wf.png", ":catalonia:": "catalonia.png", ":ck:": "ck.png", ":fo:": "fo.png", ":gm:": "gm.png", ":mh:": "mh.png", ":ye:": "ye.png", ":sb:": "sb.png", ":pn:": "pn.png", ":mu:": "mu.png", ":do:": "do.png", ":my:": "my.png", ":nr:": "nr.png", ":cc:": "cc.png", ":no:": "no.png", ":gu:": "gu.png", ":rw:": "rw.png", ":dz:": "dz.png", ":sz:": "sz.png", ":uz:": "uz.png" } +{ ":523:": "D83DDE12.png", ":)": "D83DDE0A.png", ":-*": "D83DDE1A.png", ":office:": "D83CDFE2.png", ":post_office:": "D83CDFE3.png", ":276:": "D83CDFE0.png", ":o": "D83DDE28.png", ":atm:": "D83CDFE7.png", ":european_post_office:": "D83CDFE4.png", ":hospital:": "D83CDFE5.png", ":convenience_store:": "D83CDFEA.png", ":school:": "D83CDFEB.png", ":hotel:": "D83CDFE8.png", ":love_hotel:": "D83CDFE9.png", ":izakaya_lantern:": "D83CDFEE.png", ":japanese_castle:": "D83CDFEF.png", ":department_store:": "D83CDFEC.png", ":factory:": "D83CDFED.png", ":|": "D83DDE10.png", ":yum:": "D83DDE0B.png", ":snowboarder:": "D83CDFC2.png", ":running:": "D83CDFC3.png", ":basketball:": "D83CDFC0.png", ":checkered_flag:": "D83CDFC1.png", ":trophy:": "D83CDFC6.png", ":horse_racing:": "D83CDFC7.png", ":surfer:": "D83CDFC4.png", ":D": "D83DDE03.png", ":football:": "D83CDFC8.png", ":rugby:": "D83CDFC9.png", ":]": "D83DDE0F.png", ":x": "D83DDE37.png", "⬜": "2B1C.png", ":vhs:": "D83DDCFC.png", ":233:": "D83DDCFA.png", ":212:": "D83DDCFB.png", "⬛": "2B1B.png", ":video_camera:": "D83DDCF9.png", ":signal_strength:": "D83DDCF6.png", ":camera:": "D83DDCF7.png", "➡": "27A1.png", ":no_mobile_phones:": "D83DDCF5.png", ":413:": "D83DDCF2.png", ":649:": "D83DDCF3.png", ":387:": "D83DDCF0.png", ":370:": "D83DDCF1.png", "📮": "D83DDCEE.png", ":110:": "D83DDCEF.png", ":mailbox:": "D83DDCEC.png", ":mailbox_closed:": "D83DDCED.png", ":mailbox_2:": "D83DDCEA.png", ":253:": "D83DDCEB.png", ":incoming_envelope:": "D83DDCE8.png", ":email:": "D83DDCE9.png", ":package:": "D83DDCE6.png", ":e-mail:": "D83DDCE7.png", ":445:": "D83DDCE4.png", ":inbox_tray:": "D83DDCE5.png", ":210:": "D83DDCE2.png", ":295:": "D83DDCE3.png", ":446:": "D83DDCE0.png", ":117:": "D83DDCBC.png", ":82:": "D83DDCDE.png", ":pager:": "D83DDCDF.png", ":scroll:": "D83DDCDC.png", ":memo:": "D83DDCDD.png", ":books:": "D83DDCDA.png", ":name_badge:": "D83DDCDB.png", ":blue_book:": "D83DDCD8.png", ":orange_book:": "D83DDCD9.png", ":book:": "D83DDCD6.png", ":green_book:": "D83DDCD7.png", ":notebook2:": "D83DDCD4.png", ":red_book:": "D83DDCD5.png", ":ledger:": "D83DDCD2.png", ":notebook:": "D83DDCD3.png", ":triangular_ruler:": "D83DDCD0.png", ":bookmark_tabs:": "D83DDCD1.png", ":paperclip:": "D83DDCCE.png", ":straight_ruler:": "D83DDCCF.png", ":pushpin:": "D83DDCCC.png", ":38:": "D83DDCCD.png", ":bar_chart:": "D83DDCCA.png", ":clipboard:": "D83DDCCB.png", ":chart_with_upwards_trend:": "D83DDCC8.png", ":chart_with_downwards_trend:": "D83DDCC9.png", ":calendar:": "D83DDCC6.png", ":card_index:": "D83DDCC7.png", ":page_facing_up:": "D83DDCC4.png", ":date:": "D83DDCC5.png", ":open_file_folder:": "D83DDCC2.png", ":page_with_curl:": "D83DDCC3.png", ":608:": "D83DDCC0.png", ":file_folder:": "D83DDCC1.png", ":265:": "D83DDEA1.png", ":527:": "D83DDEA0.png", ":rowboat:": "D83DDEA3.png", ":ship:": "D83DDEA2.png", ":traffic_light:": "D83DDEA5.png", ":speedboat:": "D83DDEA4.png", ":construction:": "D83DDEA7.png", ":v_traffic_light:": "D83DDEA6.png", ":triangular_flag_on_post:": "D83DDEA9.png", ":rotating_light:": "D83DDEA8.png", ":no_entry_sign:": "D83DDEAB.png", ":door:": "D83DDEAA.png", ":no_smoking:": "D83DDEAD.png", ":smoking:": "D83DDEAC.png", ":408:": "D83DDEAF.png", ":133:": "D83DDEAE.png", "◀": "25C0.png", ":potable_water:": "D83DDEB0.png", ":no_bicycles:": "D83DDEB3.png", ":bike:": "D83DDEB2.png", ":mountain_bicyclist:": "D83DDEB5.png", ":bicyclist:": "D83DDEB4.png", ":no_pedestrians:": "D83DDEB7.png", ":311:": "D83DDEB6.png", ":mens:": "D83DDEB9.png", ":children_crossing:": "D83DDEB8.png", ":restroom:": "D83DDEBB.png", ":womens:": "D83DDEBA.png", ":toilet:": "D83DDEBD.png", ":baby_symbol:": "D83DDEBC.png", ":shower:": "D83DDEBF.png", ":wc:": "D83DDEBE.png", ":helicopter:": "D83DDE81.png", ":rocket:": "D83DDE80.png", ":340:": "D83DDE83.png", ":steam_locomotive:": "D83DDE82.png", ":bullettrain_side:": "D83DDE85.png", ":391:": "D83DDE84.png", ":bullettrain_front:": "D83DDE87.png", ":train2:": "D83DDE86.png", ":station:": "D83DDE89.png", ":light_rail:": "D83DDE88.png", ":railway_car:": "D83DDE8B.png", ":tram:": "D83DDE8A.png", ":oncoming_bus:": "D83DDE8D.png", ":bus:": "D83DDE8C.png", ":busstop:": "D83DDE8F.png", ":trolleybus:": "D83DDE8E.png", ":ambulance:": "D83DDE91.png", ":minibus:": "D83DDE90.png", ":police_car:": "D83DDE93.png", ":pensive:": "D83DDE14.png", ":taxi:": "D83DDE95.png", ":oncoming_police_car:": "D83DDE94.png", ":car:": "D83DDE97.png", ":oncoming_taxi:": "D83DDE96.png", ":blue_car:": "D83DDE99.png", ":oncoming_automobile:": "D83DDE98.png", ":articulated_lorry:": "D83DDE9B.png", ":truck:": "D83DDE9A.png", ":monorail:": "D83DDE9D.png", ":tractor:": "D83DDE9C.png", ":suspension_railway:": "D83DDE9F.png", ":mountain_railway:": "D83DDE9E.png", ":538:": "D83DDCAF.png", ":571:": "D83DDE48.png", ":543:": "D83DDE49.png", ":168:": "D83DDE4A.png", ":raising_hand:": "D83DDE4B.png", ":raised_hands:": "D83DDE4C.png", ":639:": "D83DDE4D.png", ":357:": "D83DDE4E.png", ":pray:": "D83DDE4F.png", ":scream_cat:": "D83DDE40.png", ":no_good:": "D83DDE45.png", ":ok_woman:": "D83DDE46.png", ":bow:": "D83DDE47.png", ":non-potable_water:": "D83DDEB1.png", "⁉": "2049.png", ":mahjong:": "D83CDC04.png", "↪": "21AA.png", "↩": "21A9.png", "⌚": "231A.png", ":purple_heart:": "D83DDC9C.png", "↗": "2197.png", "↖": "2196.png", "↕": "2195.png", "↔": "2194.png", "↙": "2199.png", "↘": "2198.png", "⚾": "26BE.png", "⚽": "26BD.png", ":house_with_garden:": "D83CDFE1.png", "⚡": "26A1.png", "⚠": "26A0.png", "⚫": "26AB.png", ":rage:": "D83DDE21.png", "⚓": "2693.png", "0⃣": "003020E3.png", "1⃣": "003120E3.png", "2⃣": "003220E3.png", "3⃣": "003320E3.png", "4⃣": "003420E3.png", "5⃣": "003520E3.png", "6⃣": "003620E3.png", "7⃣": "003720E3.png", "8⃣": "003820E3.png", "9⃣": "003920E3.png", ":10:": "D83DDD1F.png", "⭐": "2B50.png", "⚪": "26AA.png", "⭕": "2B55.png", ":1234:": "D83DDD22.png", "Ⓜ": "24C2.png", ":european_castle:": "D83CDFF0.png", "⌛": "231B.png", "➗": "2797.png", ":((": "D83DDE29.png", ":high_heel:": "D83DDC60.png", ":swimmer:": "D83CDFCA.png", ":busts_in_silhouette:": "D83DDC65.png", "8-)": "D83DDE0D.png", "➕": "2795.png", "♈": "2648.png", ":two_women_holding_hands:": "D83DDC6D.png", ":424:": "D83DDCB9.png", ":money_with_wings:": "D83DDCB8.png", ":computer:": "D83DDCBB.png", ":348:": "D83DDCBA.png", ":minidisc:": "D83DDCBD.png", "8o": "D83DDE32.png", ":dvd:": "D83DDCBF.png", ":floppy_disc:": "D83DDCBE.png", ":59:": "D83DDCB1.png", ":moneybag:": "D83DDCB0.png", ":credit_card:": "D83DDCB3.png", ":$:": "D83DDCB2.png", ":dollar:": "D83DDCB5.png", "💴": "D83DDCB4.png", ":pound:": "D83DDCB7.png", ":yen:": "D83DDCB6.png", ":shit:": "D83DDCA9.png", ":dash:": "D83DDCA8.png", ":dizzy:": "D83DDCAB.png", ":muscle:": "D83DDCAA.png", ":thought_balloon:": "D83DDCAD.png", ":speech_balloon:": "D83DDCAC.png", "8|": "D83DDE33.png", ":28:": "D83DDCAE.png", ":bulb:": "D83DDCA1.png", ":534:": "D83DDCA0.png", ":bomb:": "D83DDCA3.png", ":297:": "D83DDCA2.png", ":boom:": "D83DDCA5.png", ":zzz:": "D83DDCA4.png", ":661:": "D83DDCA7.png", ":sweat_drops:": "D83DDCA6.png", ":blue_heart:": "D83DDC99.png", ":cupid:": "D83DDC98.png", ":yellow_heart:": "D83DDC9B.png", ":green_heart:": "D83DDC9A.png", ":56:": "D83DDC9D.png", ":32:": "D83DDC9F.png", ":revolving_hearts:": "D83DDC9E.png", ":couple_with_heart:": "D83DDC91.png", ":590:": "D83DDC90.png", ":336:": "D83DDC93.png", ":wedding:": "D83DDC92.png", ":247:": "D83DDC95.png", ":broken_heart:": "D83DDC94.png", ":heartpulse:": "D83DDC97.png", ":sparkling_heart:": "D83DDC96.png", ":syringe:": "D83DDC89.png", ":420:": "D83DDC88.png", ":kiss:": "D83DDC8B.png", ":pill:": "D83DDC8A.png", ":ring:": "D83DDC8D.png", ":383:": "D83DDC8C.png", "💏": "D83DDC8F.png", ":gem:": "D83DDC8E.png", ":information_desk_person:": "D83DDC81.png", ":skull:": "D83DDC80.png", ":dancer:": "D83DDC83.png", ":guardsman:": "D83DDC82.png", ":nail_care:": "D83DDC85.png", ":lipstick:": "D83DDC84.png", ":haircut:": "D83DDC87.png", ":massage:": "D83DDC86.png", ":bride_with_veil:": "D83DDC70.png", ":person_with_blond_hair:": "D83DDC71.png", ":man_with_gua_pi_mao:": "D83DDC72.png", ":man_with_turban:": "D83DDC73.png", ":older_man:": "D83DDC74.png", ":older_woman:": "D83DDC75.png", ":baby:": "D83DDC76.png", ":construction_worker:": "D83DDC77.png", ":princess:": "D83DDC78.png", ":japanese_ogre:": "D83DDC79.png", ":japanese_goblin:": "D83DDC7A.png", ":ghost:": "D83DDC7B.png", ":angel:": "D83DDC7C.png", ":alien:": "D83DDC7D.png", ":249:": "D83DDC7E.png", ":imp:": "D83DDC7F.png", "=(": "D83DDE20.png", ":sandal:": "D83DDC61.png", ":boot:": "D83DDC62.png", ":feet:": "D83DDC63.png", ":bust_in_silhouette:": "D83DDC64.png", ":mans_shoe:": "D83DDC5E.png", "👦": "D83DDC66.png", ":332:": "D83DDC67.png", ":man:": "D83DDC68.png", ":woman:": "D83DDC69.png", ":family:": "D83DDC6A.png", ":couple:": "D83DDC6B.png", ":two_men_holding_hands:": "D83DDC6C.png", ":point_up:": "261D.png", ":cop:": "D83DDC6E.png", ":290:": "D83DDC6F.png", ":open_hands:": "D83DDC50.png", ":crown:": "D83DDC51.png", ":womans_hat:": "D83DDC52.png", ":eyeglasses:": "D83DDC53.png", ":necktie:": "D83DDC54.png", ":shirt:": "D83DDC55.png", ":jeans:": "D83DDC56.png", ":dress:": "D83DDC57.png", ":kimono:": "D83DDC58.png", ":bikini:": "D83DDC59.png", ":womans_clothes:": "D83DDC5A.png", ":purse:": "D83DDC5B.png", ":handbag:": "D83DDC5C.png", ":pouch:": "D83DDC5D.png", "xD": "D83DDE06.png", ":shoe:": "D83DDC5F.png", ":eyes:": "D83DDC40.png", ":ear:": "D83DDC42.png", ":nose:": "D83DDC43.png", ":lips:": "D83DDC44.png", ":tongue:": "D83DDC45.png", ":point_up_2:": "D83DDC46.png", ":point_down:": "D83DDC47.png", ":point_left:": "D83DDC48.png", ":point_right:": "D83DDC49.png", ":facepunch:": "D83DDC4A.png", ":wave:": "D83DDC4B.png", ":ok:": "D83DDC4C.png", ":like:": "D83DDC4D.png", ":dislike:": "D83DDC4E.png", ":+1:": "D83DDC4D.png", ":-1:": "D83DDC4E.png", ":clap:": "D83DDC4F.png", ":^D": "D83DDE1B.png", ":3": "D83DDE19.png", ":kissing_heart:": "D83DDE18.png", ":((": "D83DDE1F.png", ":(": "D83DDE1E.png", ":stuck_out_tongue_closed_eyes:": "D83DDE1D.png", ":stuck_out_tongue_winking_eye:": "D83DDE1C.png", ":sweat:": "D83DDE13.png", ":593:": "D83DDE11.png", ":^)": "D83DDE17.png", ":confounded:": "D83DDE16.png", ":\\": "D83DDE15.png", ";-)": "D83DDE09.png", ":smiling_imp:": "D83DDE08.png", "B)": "D83DDE0E.png", "3-)": "D83DDE0C.png", ":*(": "D83DDE02.png", "=)": "D83DDE01.png", ":smile:": "D83DDE00.png", ":610:": "D83DDE07.png", ":sweat_smile:": "D83DDE05.png", ":smile2:": "D83DDE04.png", ":heart_eyes_cat:": "D83DDE3B.png", ":smile_cat:": "D83DDE3A.png", ":joy_cat:": "D83DDE39.png", ":smiley_cat:": "D83DDE38.png", ":crying_cat_face:": "D83DDE3F.png", ":pouting_cat:": "D83DDE3E.png", ":kissing_cat:": "D83DDE3D.png", ":smirk_cat:": "D83DDE3C.png", ":scream:": "D83DDE31.png", ";o": "D83DDE30.png", ":no_mouth:": "D83DDE36.png", ":dizzy_face:": "D83DDE35.png", ":sleeping:": "D83DDE34.png", ":tired_face:": "D83DDE2B.png", ":389:": "D83DDE2A.png", ":0": "D83DDE2F.png", ":O": "D83DDE2E.png", ":sob:": "D83DDE2D.png", ":p": "D83DDE2C.png", ":58:": "D83DDE23.png", ":'(": "D83DDE22.png", ":206:": "D83DDE27.png", ":622:": "D83DDE26.png", ":461:": "D83DDE25.png", ":134:": "D83DDE24.png", ":25:": "D83CDFAD.png", ":244:": "D83CDFAC.png", ":darts:": "D83CDFAF.png", ":149:": "D83CDFAE.png", ":tophat:": "D83CDFA9.png", ":art:": "D83CDFA8.png", ":113:": "D83CDFAB.png", ":circus_tent:": "D83CDFAA.png", ":movie_camera:": "D83CDFA5.png", ":microphone:": "D83CDFA4.png", ":219:": "D83CDFA7.png", ":313:": "D83CDFA6.png", ":ferris_wheel:": "D83CDFA1.png", ":carousel_horse:": "D83CDFA0.png", ":637:": "D83CDFA3.png", ":roller_coaster:": "D83CDFA2.png", ":119:": "D83CDFBD.png", ":129:": "D83CDFBC.png", ":127:": "D83CDFBF.png", ":tennis:": "D83CDFBE.png", ":musical_keyboard:": "D83CDFB9.png", ":guitar:": "D83CDFB8.png", ":violin:": "D83CDFBB.png", ":trumpet:": "D83CDFBA.png", ":647:": "D83CDFB5.png", ":43:": "D83CDFB4.png", ":saxophone:": "D83CDFB7.png", ":85:": "D83CDFB6.png", ":8ball:": "D83CDFB1.png", ":565:": "D83CDFB0.png", ":bowling:": "D83CDFB3.png", ":game_die:": "D83CDFB2.png", ":245:": "D83CDF8D.png", ":crossed_flags:": "D83CDF8C.png", ":lags:": "D83CDF8F.png", ":dolls:": "D83CDF8E.png", ":tada:": "D83CDF89.png", ":balloon:": "D83CDF88.png", ":tanabata_tree:": "D83CDF8B.png", ":confetti_ball:": "D83CDF8A.png", ":santa:": "D83CDF85.png", ":christmas_tree:": "D83CDF84.png", ":sparkler:": "D83CDF87.png", ":fireworks:": "D83CDF86.png", ":gift:": "D83CDF81.png", ":42:": "D83CDF80.png", ":jack_o_lantern:": "D83CDF83.png", ":3:": "D83CDF82.png", ":444:": "D83DDCE1.png", ":61:": "D83CDF91.png", ":386:": "D83CDF90.png", ":440:": "D83CDF93.png", ":school_satchel:": "D83CDF92.png", "⛵": "26F5.png", "⛲": "26F2.png", "⛳": "26F3.png", "⛽": "26FD.png", "⛺": "26FA.png", "⛪": "26EA.png", "⛔": "26D4.png", "⛄": "26C4.png", "⛅": "26C5.png", "⛎": "26CE.png", "✨": "2728.png", "✴": "2734.png", "✳": "2733.png", "✌": "270C.png", "✏": "270F.png", "✉": "2709.png", "✈": "2708.png", "✋": "270B.png", "✊": "270A.png", "✅": "2705.png", "✂": "2702.png", "✔": "2714.png", "✖": "2716.png", "✒": "2712.png", ":crystal_ball:": "D83DDD2E.png", ":telescope:": "D83DDD2D.png", ":microscope:": "D83DDD2C.png", ":gun:": "D83DDD2B.png", ":hocho:": "D83DDD2A.png", ":63:": "D83DDD29.png", ":504:": "D83DDD28.png", ":301:": "D83DDD27.png", ":flashlight:": "D83DDD26.png", ":fire:": "D83DDD25.png", ":abc:": "D83DDD24.png", ":274:": "D83DDD23.png", ":abcd:": "D83DDD21.png", ":ABCD:": "D83DDD20.png", ":down:": "D83DDD3D.png", ":up:": "D83DDD3C.png", ":144:": "D83DDD3B.png", ":371:": "D83DDD3A.png", ":46:": "D83DDD39.png", ":240:": "D83DDD38.png", ":48:": "D83DDD37.png", ":298:": "D83DDD36.png", ":29:": "D83DDD35.png", ":250:": "D83DDD34.png", ":white_square_button:": "D83DDD33.png", ":black_square_button:": "D83DDD32.png", ":trident:": "D83DDD31.png", ":280:": "D83DDD0F.png", ":585:": "D83DDD0E.png", ":204:": "D83DDD0D.png", ":electric_plug:": "D83DDD0C.png", ":battery:": "D83DDD0B.png", ":speaker:": "D83DDD09.png", ":nosound:": "D83DDD07.png", ":low_brightness:": "D83DDD06.png", ":397:": "D83DDD05.png", ":268:": "D83DDD04.png", ":597:": "D83DDD03.png", ":584:": "D83DDD02.png", ":517:": "D83DDD01.png", ":374:": "D83DDD00.png", ":underage:": "D83DDD1E.png", ":173:": "D83DDD1D.png", ":soon:": "D83DDD1C.png", ":on:": "D83DDD1B.png", ":end:": "D83DDD1A.png", ":back:": "D83DDD19.png", ":583:": "D83DDD18.png", ":230:": "D83DDD17.png", ":629:": "D83DDD16.png", ":163:": "D83DDD15.png", ":426:": "D83DDD14.png", ":lock:": "D83DDD13.png", ":232:": "D83DDD12.png", ":key:": "D83DDD11.png", ":328:": "D83DDD10.png", ":black_joker:": "D83CDCCF.png", "©": "00A9.png", ":174:": "D83CDF64.png", ":235:": "D83CDF65.png", ":132:": "D83CDF66.png", ":417:": "D83CDF67.png", ":524:": "D83CDF60.png", ":105:": "D83CDF61.png", ":229:": "D83CDF62.png", ":50:": "D83CDF63.png", ":499:": "D83CDF6C.png", ":415:": "D83CDF6D.png", ":467:": "D83CDF6E.png", ":546:": "D83CDF6F.png", ":74:": "D83CDF68.png", ":306:": "D83CDF69.png", ":363:": "D83CDF6A.png", ":148:": "D83CDF6B.png", ":654:": "D83CDF74.png", ":251:": "D83CDF75.png", ":641:": "D83CDF76.png", ":439:": "D83CDF77.png", ":320:": "D83CDF70.png", ":44:": "D83CDF71.png", ":291:": "D83CDF72.png", ":495:": "D83CDF73.png", ":554:": "D83CDF7C.png", ":513:": "D83CDF78.png", ":384:": "D83CDF79.png", ":9:": "D83CDF7A.png", ":90:": "D83CDF7B.png", ":522:": "D83CDF44.png", ":498:": "D83CDF45.png", ":266:": "D83CDF46.png", ":16:": "D83CDF47.png", ":152:": "D83CDF40.png", ":560:": "D83CDF41.png", ":454:": "D83CDF42.png", ":302:": "D83CDF43.png", ":483:": "D83CDF4C.png", ":664:": "D83CDF4D.png", ":96:": "D83CDF4E.png", ":663:": "D83CDF4F.png", ":493:": "D83CDF48.png", ":539:": "D83CDF49.png", ":270:": "D83CDF4A.png", ":643:": "D83CDF4B.png", ":17:": "D83CDF54.png", ":343:": "D83CDF55.png", ":399:": "D83CDF56.png", ":450:": "D83CDF57.png", ":293:": "D83CDF50.png", ":184:": "D83CDF51.png", ":47:": "D83CDF52.png", ":403:": "D83CDF53.png", ":400:": "D83CDF5C.png", ":410:": "D83CDF5D.png", ":322:": "D83CDF5E.png", ":482:": "D83CDF5F.png", ":143:": "D83CDF58.png", ":542:": "D83CDF59.png", ":588:": "D83CDF5A.png", ":529:": "D83CDF5B.png", "☺": "263A.png", ":111:": "D83CDF08.png", "☑": "2611.png", "☕": "2615.png", ":22:": "D83CDF15.png", "☎": "260E.png", "☁": "2601.png", "☀": "2600.png", ":14:": "D83DDC33.png", ":409:": "D83DDC32.png", ":382:": "D83DDC31.png", ":460:": "D83DDC30.png", ":484:": "D83DDC37.png", ":288:": "D83DDC36.png", ":594:": "D83DDC35.png", ":591:": "D83DDC34.png", ":372:": "D83DDC3B.png", ":563:": "D83DDC3A.png", ":596:": "D83DDC39.png", ":595:": "D83DDC38.png", ":367:": "D83DDC3E.png", ":376:": "D83DDC3D.png", ":305:": "D83DDC3C.png", ":281:": "D83DDC23.png", ":452:": "D83DDC22.png", ":388:": "D83DDC21.png", ":39:": "D83DDC20.png", ":36:": "D83DDC27.png", ":189:": "D83DDC26.png", ":500:": "D83DDC25.png", ":277:": "D83DDC24.png", ":459:": "D83DDC2B.png", ":486:": "D83DDC2A.png", ":662:": "D83DDC28.png", ":509:": "D83DDC2F.png", ":365:": "D83DDC2E.png", ":34:": "D83DDC2D.png", ":319:": "D83DDC2C.png", ":118:": "D83DDC13.png", ":463:": "D83DDC12.png", ":220:": "D83DDC11.png", ":653:": "D83DDC10.png", ":599:": "D83DDC17.png", ":603:": "D83DDC16.png", ":489:": "D83DDC15.png", ":552:": "D83DDC14.png", ":181:": "D83DDC1B.png", ":208:": "D83DDC1A.png", ":19:": "D83DDC19.png", ":580:": "D83DDC18.png", ":398:": "D83DDC1F.png", ":310:": "D83DDC1E.png", ":4:": "D83DDC1D.png", ":564:": "D83DDC1C.png", ":135:": "D83DDC03.png", ":566:": "D83DDC02.png", ":211:": "D83DDC01.png", ":631:": "D83DDC00.png", ":496:": "D83DDC07.png", ":263:": "D83DDC06.png", ":447:": "D83DDC05.png", ":283:": "D83DDC04.png", ":607:": "D83DDC0B.png", ":23:": "D83DDC0A.png", ":634:": "D83DDC09.png", ":435:": "D83DDC08.png", ":349:": "D83DDC0F.png", ":53:": "D83DDC0E.png", ":101:": "D83DDC0D.png", ":567:": "D83DDC0C.png", "◻": "25FB.png", "❤": "2764.png", "◼": "25FC.png", "◽": "25FD.png", "◾": "25FE.png", "➿": "27BF.png", "⬅": "2B05.png", ":531:": "D83DDE92.png", ":116:": "D83CDD8E.png", ":466:": "D83CDD95.png", ":545:": "D83CDD94.png", ":202:": "D83CDD97.png", ":100:": "D83CDD96.png", ":CL:": "D83CDD91.png", ":free:": "D83CDD93.png", ":cool:": "D83CDD92.png", ":UP!:": "D83CDD99.png", ":SOS:": "D83CDD98.png", ":VS:": "D83CDD9A.png", ":525:": "D83CDF20.png", ":614:": "D83CDF37.png", ":13:": "D83CDF35.png", ":108:": "D83CDF34.png", ":289:": "D83CDF33.png", ":390:": "D83CDF32.png", ":222:": "D83CDF31.png", ":92:": "D83CDF30.png", ":205:": "D83CDF3F.png", ":7:": "D83CDF3E.png", ":142:": "D83CDF3D.png", ":536:": "D83CDF3C.png", ":153:": "D83CDF3B.png", ":307:": "D83CDF3A.png", ":304:": "D83CDF39.png", ":18:": "D83CDF38.png", ":558:": "D83CDF07.png", ":71:": "D83CDF06.png", ":421:": "D83CDF05.png", ":485:": "D83CDF04.png", ":223:": "D83CDF03.png", ":269:": "D83CDF02.png", ":574:": "D83CDF01.png", ":124:": "D83CDF00.png", ":65:": "D83CDF0F.png", ":15:": "D83CDF0E.png", ":411:": "D83CDF0D.png", ":272:": "D83CDF0C.png", ":335:": "D83CDF0B.png", ":379:": "D83CDF0A.png", ":192:": "D83CDF09.png", ":359:": "D83CDF17.png", ":436:": "D83CDF16.png", ":479:": "D83CDF14.png", ":231:": "D83CDF13.png", ":528:": "D83CDF12.png", ":341:": "D83CDF11.png", ":491:": "D83CDF10.png", ":520:": "D83CDF1F.png", ":185:": "D83CDF1E.png", ":568:": "D83CDF1D.png", ":40:": "D83CDF1C.png", ":393:": "D83CDF1B.png", ":275:": "D83CDF1A.png", ":72:": "D83CDF19.png", ":246:": "D83CDF18.png", "❓": "2753.png", "❗": "2757.png", "❔": "2754.png", "❕": "2755.png", "➰": "27B0.png", "❄": "2744.png", "❎": "274E.png", "❌": "274C.png", "⏰": "23F0.png", "⏳": "23F3.png", "⏩": "23E9.png", "⏪": "23EA.png", "⏫": "23EB.png", "⏬": "23EC.png", ":106:": "D83DDEC4.png", ":465:": "D83DDEC2.png", "▶": "25B6.png", ":236:": "D83DDEC0.png", ":271:": "D83DDEC1.png", "⤵": "2935.png", "⤴": "2934.png", "▫": "25AB.png", "▪": "25AA.png", "❇": "2747.png", ":510:": "D83CDD7E.png", ":480:": "D83CDD7F.png", "〽": "303D.png", "™": "2122.png", "〰": "3030.png", ":351:": "D83CDD70.png", ":237:": "D83CDD71.png", "ℹ": "2139.png", "☔": "2614.png", "⬇": "2B07.png", "➖": "2796.png", ":358:": "D83CDE01.png", "⬆": "2B06.png", "♿": "267F.png", "♻": "267B.png", "♨": "2668.png", "♦": "2666.png", "♥": "2665.png", "♣": "2663.png", "♠": "2660.png", "®": "00AE.png", "♒": "2652.png", "♓": "2653.png", "♐": "2650.png", "♑": "2651.png", "♎": "264E.png", "♏": "264F.png", "♌": "264C.png", "♍": "264D.png", "♊": "264A.png", "♋": "264B.png", ":592:": "D83DDCF4.png", "♉": "2649.png", ":472:": "D83DDDFB.png", ":84:": "D83DDDFE.png", ":254:": "D83DDDFF.png", ":316:": "D83DDDFC.png", ":226:": "D83DDDFD.png", "‼": "203C.png", ":621:": "D83DDEC5.png", ":140:": "D83DDEC3.png", ":tox:": "tox.png", ":wtox:": "wtox.png", ":td:": "td.png", ":cr:": "cr.png", ":gd:": "gd.png", ":si:": "si.png", ":ni:": "ni.png", ":gs:": "gs.png", ":er:": "er.png", ":pg:": "pg.png", ":jp:": "jp.png", ":hu:": "hu.png", ":is:": "is.png", ":ro:": "ro.png", ":europeanunion:": "europeanunion.png", ":bj:": "bj.png", ":mp:": "mp.png", ":tl:": "tl.png", ":gl:": "gl.png", ":sa:": "sa.png", ":na:": "na.png", ":as:": "as.png", ":lt:": "lt.png", ":za:": "za.png", ":bb:": "bb.png", ":mx:": "mx.png", ":np:": "np.png", ":ru:": "ru.png", ":vg:": "vg.png", ":sy:": "sy.png", ":fam:": "fam.png", ":bo:": "bo.png", ":um:": "um.png", ":fr:": "fr.png", ":sh:": "sh.png", ":gr:": "gr.png", ":nu:": "nu.png", ":mo:": "mo.png", ":ir:": "ir.png", ":de:": "de.png", ":tt:": "tt.png", ":lb:": "lb.png", ":ci:": "ci.png", ":tm:": "tm.png", ":pl:": "pl.png", ":vi:": "vi.png", ":hr:": "hr.png", ":ar:": "ar.png", ":bz:": "bz.png", ":ae:": "ae.png", ":lu:": "lu.png", ":cz:": "cz.png", ":dm:": "dm.png", ":ca:": "ca.png", ":yt:": "yt.png", ":va:": "va.png", ":ee:": "ee.png", ":pt:": "pt.png", ":az:": "az.png", ":br:": "br.png", ":dk:": "dk.png", ":am:": "am.png", ":tw:": "tw.png", ":gq:": "gq.png", ":et:": "et.png", ":pe:": "pe.png", ":pr:": "pr.png", ":kr:": "kr.png", ":st:": "st.png", ":ki:": "ki.png", ":bt:": "bt.png", ":mr:": "mr.png", ":nz:": "nz.png", ":lc:": "lc.png", ":mm:": "mm.png", ":ch:": "ch.png", ":gb:": "gb.png", ":so:": "so.png", ":nc:": "nc.png", ":gy:": "gy.png", ":scotland:": "scotland.png", ":pm:": "pm.png", ":ug:": "ug.png", ":sv:": "sv.png", ":gf:": "gf.png", ":kz:": "kz.png", ":lr:": "lr.png", ":cy:": "cy.png", ":ad:": "ad.png", ":mz:": "mz.png", ":fj:": "fj.png", ":sg:": "sg.png", ":au:": "au.png", ":bs:": "bs.png", ":england:": "england.png", ":al:": "al.png", ":bd:": "bd.png", ":fi:": "fi.png", ":md:": "md.png", ":rs:": "rs.png", ":ps:": "ps.png", ":uy:": "uy.png", ":tr:": "tr.png", ":kh:": "kh.png", ":zm:": "zm.png", ":ga:": "ga.png", ":ml:": "ml.png", ":hk:": "hk.png", ":ky:": "ky.png", ":it:": "it.png", ":cn:": "cn.png", ":ag:": "ag.png", ":ls:": "ls.png", ":tz:": "tz.png", ":wales:": "wales.png", ":bm:": "bm.png", ":co:": "co.png", ":tk:": "tk.png", ":gi:": "gi.png", ":eg:": "eg.png", ":ie:": "ie.png", ":at:": "at.png", ":mc:": "mc.png", ":by:": "by.png", ":ao:": "ao.png", ":lk:": "lk.png", ":me:": "me.png", ":be:": "be.png", ":cg:": "cg.png", ":sn:": "sn.png", ":kp:": "kp.png", ":mq:": "mq.png", ":kg:": "kg.png", ":bv:": "bv.png", ":mt:": "mt.png", ":qa:": "qa.png", ":la:": "la.png", ":th:": "th.png", ":cv:": "cv.png", ":id:": "id.png", ":sm:": "sm.png", ":ne:": "ne.png", ":il:": "il.png", ":bi:": "bi.png", ":jm:": "jm.png", ":af:": "af.png", ":bn:": "bn.png", ":ma:": "ma.png", ":gh:": "gh.png", ":se:": "se.png", ":pk:": "pk.png", ":ua:": "ua.png", ":to:": "to.png", ":aw:": "aw.png", ":gt:": "gt.png", ":mk:": "mk.png", ":an:": "an.png", ":bf:": "bf.png", ":tc:": "tc.png", ":nl:": "nl.png", ":cf:": "cf.png", ":gp:": "gp.png", ":ly:": "ly.png", ":mw:": "mw.png", ":bw:": "bw.png", ":cu:": "cu.png", ":mn:": "mn.png", ":nf:": "nf.png", ":sl:": "sl.png", ":io:": "io.png", ":cx:": "cx.png", ":kw:": "kw.png", ":py:": "py.png", ":us:": "us.png", ":pa:": "pa.png", ":kn:": "kn.png", ":zw:": "zw.png", ":cm:": "cm.png", ":fm:": "fm.png", ":sd:": "sd.png", ":ph:": "ph.png", ":fk:": "fk.png", ":tj:": "tj.png", ":ai:": "ai.png", ":li:": "li.png", ":mg:": "mg.png", ":bg:": "bg.png", ":ms:": "ms.png", ":gw:": "gw.png", ":vc:": "vc.png", ":hn:": "hn.png", ":ke:": "ke.png", ":ax:": "ax.png", ":mv:": "mv.png", ":tf:": "tf.png", ":vu:": "vu.png", ":sk:": "sk.png", ":ng:": "ng.png", ":vn:": "vn.png", ":in:": "in.png", ":sr:": "sr.png", ":iq:": "iq.png", ":hm:": "hm.png", ":pw:": "pw.png", ":ws:": "ws.png", ":jo:": "jo.png", ":km:": "km.png", ":bh:": "bh.png", ":tn:": "tn.png", ":cl:": "cl.png", ":gn:": "gn.png", ":sc:": "sc.png", ":eh:": "eh.png", ":ba:": "ba.png", ":re:": "re.png", ":lv:": "lv.png", ":cd:": "cd.png", ":ve:": "ve.png", ":om:": "om.png", ":cs:": "cs.png", ":ge:": "ge.png", ":tg:": "tg.png", ":es:": "es.png", ":sj:": "sj.png", ":pf:": "pf.png", ":ht:": "ht.png", ":dj:": "dj.png", ":ec:": "ec.png", ":tv:": "tv.png", ":wf:": "wf.png", ":catalonia:": "catalonia.png", ":ck:": "ck.png", ":fo:": "fo.png", ":gm:": "gm.png", ":mh:": "mh.png", ":ye:": "ye.png", ":sb:": "sb.png", ":pn:": "pn.png", ":mu:": "mu.png", ":do:": "do.png", ":my:": "my.png", ":nr:": "nr.png", ":cc:": "cc.png", ":no:": "no.png", ":gu:": "gu.png", ":rw:": "rw.png", ":dz:": "dz.png", ":sz:": "sz.png", ":uz:": "uz.png" } 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 new file mode 100644 index 0000000..2659bf2 Binary files /dev/null and b/toxygen/smileys/ksk/angry.png differ diff --git a/toxygen/smileys/ksk/angry2.png b/toxygen/smileys/ksk/angry2.png new file mode 100644 index 0000000..6ecbb1e Binary files /dev/null and b/toxygen/smileys/ksk/angry2.png differ diff --git a/toxygen/smileys/ksk/angry3.png b/toxygen/smileys/ksk/angry3.png new file mode 100644 index 0000000..9b9ebc0 Binary files /dev/null and b/toxygen/smileys/ksk/angry3.png differ diff --git a/toxygen/smileys/ksk/blink.png b/toxygen/smileys/ksk/blink.png new file mode 100644 index 0000000..b7fe238 Binary files /dev/null and b/toxygen/smileys/ksk/blink.png differ diff --git a/toxygen/smileys/ksk/bluestar.png b/toxygen/smileys/ksk/bluestar.png new file mode 100644 index 0000000..21f37ca Binary files /dev/null and b/toxygen/smileys/ksk/bluestar.png differ diff --git a/toxygen/smileys/ksk/calm.png b/toxygen/smileys/ksk/calm.png new file mode 100644 index 0000000..da19990 Binary files /dev/null and b/toxygen/smileys/ksk/calm.png differ diff --git a/toxygen/smileys/ksk/config.json b/toxygen/smileys/ksk/config.json new file mode 100644 index 0000000..0cc910e --- /dev/null +++ b/toxygen/smileys/ksk/config.json @@ -0,0 +1 @@ +{"BD": "cool2.png", "v_v": "calm.png", ":/": "getlost.png", ":(": "sad.png", ":)": "smile.png", ":*": "kiss.png", ":animal:": "pawn.png", "=|": "none.png", "=*": "kiss.png", ":heart:": "heart.png", "B]": "cool.png", "=o": "shocked.png", ":0": "shocked.png", "=S": "none2.png", "=]": "smile2.png", "=\\": "getlost.png", "B-)": "cool.png", ":pawn:": "pawn.png", "=O": "shocked.png", ">:\\": "angry2.png", ":redstar:": "redstar.png", ":o": "shocked.png", "=0": "shocked.png", "B-D": "cool2.png", ":|": "none.png", ":''(": "cry.png", "=/": "getlost.png", "=)": "smile.png", "=(": "sad.png", "B-]": "cool.png", ":O": "shocked.png", ":D": "grin.png", "B)": "cool.png", ":'(": "cry.png", ":]": "smile2.png", ":music:": "notes.png", ":P": "tongue.png", ":S": "none2.png", ":evil:": "evil.png", ":-O": "shocked.png", ":zzzzz:": "zzz.png", ">:[]": "angry.png", ";|": "none.png", ":-\\": "getlost.png", ":-]": "smile2.png", ":-S": "none2.png", ":-P": "tongue.png", ";o": "shocked.png", ";S": "none2.png", ":\\": "getlost.png", ";P": "tongue.png", ":pet:": "pawn.png", ":-o": "shocked.png", ";]": "blink.png", ";\\": "getlost.png", ":oops:": "oops.png", ":-|": "none.png", ";D": "grin.png", ";O": "shocked.png", "@->-": "flower.png", ";0": "shocked.png", ":zzz:": "zzz.png", ":cool2:": "cool2.png", "^_^": "pleased.png", ":)))": "grin.png", ";)": "blink.png", ";/": "getlost.png", ":-*": "kiss.png", ":-(": "sad.png", ":-)": "smile.png", "8-[]": "scared.png", ":cool:": "cool.png", ":kiss:": "kiss.png", ":notes:": "notes.png", ":calm:": "calm.png", ":-0": "shocked.png", ":greenstar:": "greenstar.png", ">:][": "angry.png", ">:]]": "evil2.png", "B))": "cool2.png", ">:)": "evil.png", ">:(": "angry3.png", ">:/": "angry2.png", ":lol:": "lol.png", ":scared:": "scared.png", ">:>": "evil.png", ">:<": "angry3.png", ">:D": "evil2.png", "B]]": "cool2.png", ">:((": "angry3.png", ">:[": "angry3.png", ":sick:": "unwell.png", ":-/": "getlost.png", ":cry:": "cry.png", "<3": "heart.png", ":leaf:": "leaf.png", ">:))": "evil2.png", ":bluestar:": "bluestar.png", ";-0": "shocked.png", ":weed:": "leaf.png", ":zzzz:": "zzz.png", ":sing:": "notes.png", ":yellowstar:": "yellowstar.png", ";-/": "getlost.png", ";-)": "blink.png", ":dead:": "dead.png", ";-S": "none2.png", "^^": "pleased.png", ";-P": "tongue.png", ";-]": "blink.png", ";-\\": "getlost.png", ":flower:": "flower.png", ":puke:": "unwell.png", ";-O": "shocked.png", ":love:": "heart.png", ";-o": "shocked.png", ":))))": "grin.png", ":))": "grin.png"} diff --git a/toxygen/smileys/ksk/cool.png b/toxygen/smileys/ksk/cool.png new file mode 100644 index 0000000..891ed33 Binary files /dev/null and b/toxygen/smileys/ksk/cool.png differ diff --git a/toxygen/smileys/ksk/cool2.png b/toxygen/smileys/ksk/cool2.png new file mode 100644 index 0000000..3dea030 Binary files /dev/null and b/toxygen/smileys/ksk/cool2.png differ diff --git a/toxygen/smileys/ksk/cry.png b/toxygen/smileys/ksk/cry.png new file mode 100644 index 0000000..fea2481 Binary files /dev/null and b/toxygen/smileys/ksk/cry.png differ diff --git a/toxygen/smileys/ksk/dead.png b/toxygen/smileys/ksk/dead.png new file mode 100644 index 0000000..7b22495 Binary files /dev/null and b/toxygen/smileys/ksk/dead.png differ diff --git a/toxygen/smileys/ksk/evil.png b/toxygen/smileys/ksk/evil.png new file mode 100644 index 0000000..140a259 Binary files /dev/null and b/toxygen/smileys/ksk/evil.png differ diff --git a/toxygen/smileys/ksk/evil2.png b/toxygen/smileys/ksk/evil2.png new file mode 100644 index 0000000..c01efdd Binary files /dev/null and b/toxygen/smileys/ksk/evil2.png differ diff --git a/toxygen/smileys/ksk/flower.png b/toxygen/smileys/ksk/flower.png new file mode 100644 index 0000000..5463fda Binary files /dev/null and b/toxygen/smileys/ksk/flower.png differ diff --git a/toxygen/smileys/ksk/getlost.png b/toxygen/smileys/ksk/getlost.png new file mode 100644 index 0000000..2c75727 Binary files /dev/null and b/toxygen/smileys/ksk/getlost.png differ diff --git a/toxygen/smileys/ksk/greenstar.png b/toxygen/smileys/ksk/greenstar.png new file mode 100644 index 0000000..b557c50 Binary files /dev/null and b/toxygen/smileys/ksk/greenstar.png differ diff --git a/toxygen/smileys/ksk/grin.png b/toxygen/smileys/ksk/grin.png new file mode 100644 index 0000000..b35bf24 Binary files /dev/null and b/toxygen/smileys/ksk/grin.png differ diff --git a/toxygen/smileys/ksk/heart.png b/toxygen/smileys/ksk/heart.png new file mode 100644 index 0000000..25d3d7f Binary files /dev/null and b/toxygen/smileys/ksk/heart.png differ diff --git a/toxygen/smileys/ksk/kiss.png b/toxygen/smileys/ksk/kiss.png new file mode 100644 index 0000000..4764d69 Binary files /dev/null and b/toxygen/smileys/ksk/kiss.png differ diff --git a/toxygen/smileys/ksk/leaf.png b/toxygen/smileys/ksk/leaf.png new file mode 100644 index 0000000..7598896 Binary files /dev/null and b/toxygen/smileys/ksk/leaf.png differ diff --git a/toxygen/smileys/ksk/lol.png b/toxygen/smileys/ksk/lol.png new file mode 100644 index 0000000..9d42add Binary files /dev/null and b/toxygen/smileys/ksk/lol.png differ diff --git a/toxygen/smileys/ksk/none.png b/toxygen/smileys/ksk/none.png new file mode 100644 index 0000000..03d421f Binary files /dev/null and b/toxygen/smileys/ksk/none.png differ diff --git a/toxygen/smileys/ksk/none2.png b/toxygen/smileys/ksk/none2.png new file mode 100644 index 0000000..0fc9cf1 Binary files /dev/null and b/toxygen/smileys/ksk/none2.png differ diff --git a/toxygen/smileys/ksk/notes.png b/toxygen/smileys/ksk/notes.png new file mode 100644 index 0000000..6c07260 Binary files /dev/null and b/toxygen/smileys/ksk/notes.png differ diff --git a/toxygen/smileys/ksk/oops.png b/toxygen/smileys/ksk/oops.png new file mode 100644 index 0000000..744a2a0 Binary files /dev/null and b/toxygen/smileys/ksk/oops.png differ diff --git a/toxygen/smileys/ksk/pawn.png b/toxygen/smileys/ksk/pawn.png new file mode 100644 index 0000000..cce0cad Binary files /dev/null and b/toxygen/smileys/ksk/pawn.png differ diff --git a/toxygen/smileys/ksk/pleased.png b/toxygen/smileys/ksk/pleased.png new file mode 100644 index 0000000..2c7e60d Binary files /dev/null and b/toxygen/smileys/ksk/pleased.png differ diff --git a/toxygen/smileys/ksk/redstar.png b/toxygen/smileys/ksk/redstar.png new file mode 100644 index 0000000..33bcdf1 Binary files /dev/null and b/toxygen/smileys/ksk/redstar.png differ diff --git a/toxygen/smileys/ksk/sad.png b/toxygen/smileys/ksk/sad.png new file mode 100644 index 0000000..0a33174 Binary files /dev/null and b/toxygen/smileys/ksk/sad.png differ diff --git a/toxygen/smileys/ksk/scared.png b/toxygen/smileys/ksk/scared.png new file mode 100644 index 0000000..1b5c55c Binary files /dev/null and b/toxygen/smileys/ksk/scared.png differ diff --git a/toxygen/smileys/ksk/shocked.png b/toxygen/smileys/ksk/shocked.png new file mode 100644 index 0000000..83e0850 Binary files /dev/null and b/toxygen/smileys/ksk/shocked.png differ diff --git a/toxygen/smileys/ksk/smile.png b/toxygen/smileys/ksk/smile.png new file mode 100644 index 0000000..a431ca7 Binary files /dev/null and b/toxygen/smileys/ksk/smile.png differ diff --git a/toxygen/smileys/ksk/smile2.png b/toxygen/smileys/ksk/smile2.png new file mode 100644 index 0000000..4d003ae Binary files /dev/null and b/toxygen/smileys/ksk/smile2.png differ diff --git a/toxygen/smileys/ksk/tongue.png b/toxygen/smileys/ksk/tongue.png new file mode 100644 index 0000000..bf6c37e Binary files /dev/null and b/toxygen/smileys/ksk/tongue.png differ diff --git a/toxygen/smileys/ksk/unwell.png b/toxygen/smileys/ksk/unwell.png new file mode 100644 index 0000000..5bca721 Binary files /dev/null and b/toxygen/smileys/ksk/unwell.png differ diff --git a/toxygen/smileys/ksk/yellowstar.png b/toxygen/smileys/ksk/yellowstar.png new file mode 100644 index 0000000..5e00805 Binary files /dev/null and b/toxygen/smileys/ksk/yellowstar.png differ diff --git a/toxygen/smileys/ksk/zzz.png b/toxygen/smileys/ksk/zzz.png new file mode 100644 index 0000000..0d17073 Binary files /dev/null and b/toxygen/smileys/ksk/zzz.png differ diff --git a/toxygen/smileys.py b/toxygen/smileys/smileys.py similarity index 70% rename from toxygen/smileys.py rename to toxygen/smileys/smileys.py index 9143a0b..604e681 100644 --- a/toxygen/smileys.py +++ b/toxygen/smileys/smileys.py @@ -1,14 +1,20 @@ -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 -try: - from PySide import QtCore -except ImportError: - from PyQt4 import QtCore +from qtpy import QtCore -class SmileyLoader(util.Singleton): +from utils import util + +# LOG=util.log +global LOG +LOG = logging.getLogger('app.'+__name__) +log = lambda x: LOG.info(x) + +class SmileyLoader: """ Class which loads smileys packs and insert smileys into messages """ @@ -28,16 +34,16 @@ class SmileyLoader(util.Singleton): pack_name = self._settings['smiley_pack'] if self._settings['smileys'] and self._curr_pack != pack_name: self._curr_pack = pack_name - path = self.get_smileys_path() + 'config.json' + path = util.join_path(self.get_smileys_path(), 'config.json') try: with open(path, encoding='utf8') as fl: self._smileys = json.loads(fl.read()) 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 = self.get_smileys_path() + value + value = util.join_path(self.get_smileys_path(), value) if value not in values: keys.append(key) values.append(value) @@ -45,13 +51,14 @@ class SmileyLoader(util.Singleton): 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.curr_directory() + '/smileys/' + self._curr_pack + '/' + return util.join_path(util.get_smileys_directory(), self._curr_pack) if self._curr_pack is not None else None - def get_packs_list(self): - d = util.curr_directory() + '/smileys/' + @staticmethod + def get_packs_list(): + d = util.get_smileys_directory() return [x[1] for x in os.walk(d)][0] def get_smileys(self): @@ -74,18 +81,3 @@ class SmileyLoader(util.Singleton): if file_name.endswith('.gif'): # animated smiley edit.addAnimation(QtCore.QUrl(file_name), self.get_smileys_path() + file_name) return ' '.join(arr) - - -def sticker_loader(): - """ - :return list of stickers - """ - result = [] - d = util.curr_directory() + '/stickers/' - keys = [x[1] for x in os.walk(d)][0] - for key in keys: - path = d + key + '/' - files = filter(lambda f: f.endswith('.png'), os.listdir(path)) - files = map(lambda f: str(path + f), files) - result.extend(files) - return result diff --git a/toxygen/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/__init__.py b/toxygen/stickers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/stickers/stickers.py b/toxygen/stickers/stickers.py new file mode 100644 index 0000000..56a0a29 --- /dev/null +++ b/toxygen/stickers/stickers.py @@ -0,0 +1,19 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os +import utils.util as util + +def load_stickers(): + """ + :return list of stickers + """ + result = [] + d = util.get_stickers_directory() + keys = [x[1] for x in os.walk(d)][0] + for key in keys: + path = util.join_path(d, key) + files = filter(lambda f: f.endswith('.png'), os.listdir(path)) + files = map(lambda f: util.join_path(path, f), files) + result.extend(files) + + return result diff --git a/toxygen/stickers/tox/black.png b/toxygen/stickers/tox/black.png old mode 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 new file mode 100644 index 0000000..afb2d2d Binary files /dev/null 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 new file mode 100644 index 0000000..038d833 Binary files /dev/null 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/dark_style.qss b/toxygen/styles/dark_style.qss new file mode 100644 index 0000000..ece5ec3 --- /dev/null +++ b/toxygen/styles/dark_style.qss @@ -0,0 +1,1335 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) <2013-2014> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +QToolTip +{ + border: 1px solid #3A3939; + background-color: rgb(90, 102, 117);; + color: white; + padding: 1px; + opacity: 200; +} + +QWidget +{ + color: silver; + background-color: #302F2F; + selection-background-color: #A9A9A9; + selection-color: black; + background-clip: border; + border-image: none; + outline: 0; +} + +QWidget:item:hover +{ + background-color: #78879b; + color: black; +} + +QWidget:item:selected +{ + background-color: #A9A9A9; +} + +QProgressBar:horizontal { + border: 1px solid #3A3939; + text-align: center; + padding: 1px; + background: #201F1F; +} +QProgressBar::chunk:horizontal { + background-color: qlineargradient(spread:reflect, x1:1, y1:0.545, x2:1, y2:0, stop:0 rgba(28, 66, 111, 255), stop:1 rgba(37, 87, 146, 255)); +} + +QCheckBox:disabled +{ + color: #777777; +} +QCheckBox::indicator, +QGroupBox::indicator +{ + width: 18px; + height: 18px; +} +QGroupBox::indicator +{ + margin-left: 2px; +} + +QCheckBox::indicator:unchecked, +QCheckBox::indicator:unchecked:hover, +QGroupBox::indicator:unchecked, +QGroupBox::indicator:unchecked:hover +{ + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:focus, +QCheckBox::indicator:unchecked:pressed, +QGroupBox::indicator:unchecked:focus, +QGroupBox::indicator:unchecked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); +} + +QCheckBox::indicator:checked, +QCheckBox::indicator:checked:hover, +QGroupBox::indicator:checked, +QGroupBox::indicator:checked:hover +{ + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QCheckBox::indicator:checked:focus, +QCheckBox::indicator:checked:pressed, +QGroupBox::indicator:checked:focus, +QGroupBox::indicator:checked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_checked_focus.png); +} + +QCheckBox::indicator:indeterminate, +QCheckBox::indicator:indeterminate:hover, +QCheckBox::indicator:indeterminate:pressed +QGroupBox::indicator:indeterminate, +QGroupBox::indicator:indeterminate:hover, +QGroupBox::indicator:indeterminate:pressed +{ + image: url(:/qss_icons/rc/checkbox_indeterminate.png); +} + +QCheckBox::indicator:indeterminate:focus, +QGroupBox::indicator:indeterminate:focus +{ + image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); +} + +QCheckBox::indicator:checked:disabled, +QGroupBox::indicator:checked:disabled +{ + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +QCheckBox::indicator:unchecked:disabled, +QGroupBox::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QRadioButton +{ + spacing: 5px; + outline: none; + color: #bbb; + margin-bottom: 2px; +} + +QRadioButton:disabled +{ + color: #777777; +} +QRadioButton::indicator +{ + width: 21px; + height: 21px; +} + +QRadioButton::indicator:unchecked, +QRadioButton::indicator:unchecked:hover +{ + image: url(:/qss_icons/rc/radio_unchecked.png); +} + +QRadioButton::indicator:unchecked:focus, +QRadioButton::indicator:unchecked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_unchecked_focus.png); +} + +QRadioButton::indicator:checked, +QRadioButton::indicator:checked:hover +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked.png); +} + +QRadioButton::indicator:checked:focus, +QRadioButton::indicato::menu-arrowr:checked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked_focus.png); +} + +QRadioButton::indicator:indeterminate, +QRadioButton::indicator:indeterminate:hover, +QRadioButton::indicator:indeterminate:pressed +{ + image: url(:/qss_icons/rc/radio_indeterminate.png); +} + +QRadioButton::indicator:checked:disabled +{ + outline: none; + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QRadioButton::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + + +QMenuBar +{ + background-color: #302F2F; + color: silver; +} + +QMenuBar::item +{ + background: transparent; +} + +QMenuBar::item:selected +{ + background: transparent; + border: 1px solid #A9A9A9; +} + +QMenuBar::item:pressed +{ + border: 1px solid #3A3939; + background-color: #A9A9A9; + color: black; + margin-bottom:-1px; + padding-bottom:1px; +} + +QMenu +{ + border: 1px solid #3A3939; + color: silver; + margin: 2px; +} + +QMenu::icon +{ + margin: 5px; +} + +QMenu::item +{ + padding: 5px 30px 5px 30px; + margin-left: 5px; + border: 1px solid transparent; /* reserve space for selection border */ +} + +QMenu::item:selected +{ + color: black; +} + +QMenu::separator { + height: 2px; + background: lightblue; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::indicator { + width: 18px; + height: 18px; +} + +/* non-exclusive indicator = check box style indicator + (see QActionGroup::setExclusive) */ +QMenu::indicator:non-exclusive:unchecked { + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QMenu::indicator:non-exclusive:unchecked:selected { + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QMenu::indicator:non-exclusive:checked { + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QMenu::indicator:non-exclusive:checked:selected { + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ +QMenu::indicator:exclusive:unchecked { + image: url(:/qss_icons/rc/radio_unchecked.png); +} + +QMenu::indicator:exclusive:unchecked:selected { + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + +QMenu::indicator:exclusive:checked { + image: url(:/qss_icons/rc/radio_checked.png); +} + +QMenu::indicator:exclusive:checked:selected { + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QMenu::right-arrow { + margin: 5px; + image: url(:/qss_icons/rc/right_arrow.png) +} + + +QWidget:disabled +{ + color: #404040; + background-color: #302F2F; +} + +QAbstractItemView +{ + alternate-background-color: #3A3939; + color: silver; + border: 1px solid 3A3939; + border-radius: 2px; + padding: 1px; +} + +QWidget:focus, QMenuBar:focus +{ + border: 1px solid #78879b; +} + +QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus +{ + border: none; +} + +QLineEdit +{ + background-color: #201F1F; + padding: 2px; + border-style: solid; + border: 1px solid #3A3939; + border-radius: 2px; + color: silver; +} + +QGroupBox { + border:1px solid #3A3939; + border-radius: 2px; + margin-top: 20px; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top center; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; +} + +QAbstractScrollArea +{ + border-radius: 2px; + border: 1px solid #3A3939; + background-color: transparent; +} + +QScrollBar:horizontal +{ + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent #2A2929; + border-radius: 4px; + background-color: #2A2929; +} + +QScrollBar::handle:horizontal +{ + background-color: #605F5F; + min-width: 5px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/right_arrow_disabled.png); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/left_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/right_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/left_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal +{ + background: none; +} + + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + background-color: #2A2929; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent #2A2929; + border-radius: 4px; +} + +QScrollBar::handle:vertical +{ + background-color: #605F5F; + min-height: 5px; + border-radius: 4px; +} + +QScrollBar::sub-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/up_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/down_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on +{ + + border-image: url(:/qss_icons/rc/up_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on +{ + border-image: url(:/qss_icons/rc/down_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical +{ + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #201F1F; + color: silver; + border: 1px solid #3A3939; +} + +QPlainTextEdit +{ + background-color: #201F1F;; + color: silver; + border-radius: 2px; + border: 1px solid #3A3939; +} + +QHeaderView::section +{ + background-color: #3A3939; + color: silver; + padding-left: 4px; + border: 1px solid #6c6c6c; +} + +QSizeGrip { + image: url(:/qss_icons/rc/sizegrip.png); + width: 12px; + height: 12px; +} + + +QMainWindow::separator +{ + background-color: #302F2F; + color: white; + padding-left: 4px; + spacing: 2px; + border: 1px dashed #3A3939; +} + +QMainWindow::separator:hover +{ + + background-color: #787876; + color: white; + padding-left: 4px; + border: 1px solid #3A3939; + spacing: 2px; +} + + +QMenu::separator +{ + height: 1px; + background-color: #3A3939; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + + +QFrame +{ + border-radius: 2px; + border: 1px solid #444; +} + +QFrame[frameShape="0"] +{ + border-radius: 2px; + border: 1px transparent #444; +} + +QStackedWidget +{ + border: 1px transparent black; +} + +QToolBar { + border: 1px transparent #393838; + background: 1px solid #302F2F; + font-weight: bold; +} + +QToolBar::handle:horizontal { + image: url(:/qss_icons/rc/Hmovetoolbar.png); +} +QToolBar::handle:vertical { + image: url(:/qss_icons/rc/Vmovetoolbar.png); +} +QToolBar::separator:horizontal { + image: url(:/qss_icons/rc/Hsepartoolbar.png); +} +QToolBar::separator:vertical { + image: url(:/qss_icons/rc/Vsepartoolbars.png); +} + +QPushButton +{ + color: silver; + background-color: #302F2F; + border-width: 1px; + border-color: #4A4949; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + border-radius: 2px; + outline: none; +} + +QPushButton:focus +{ + border-width: 1px; + border-color: #4A4949; + border-style: solid; +} + +QPushButton:disabled +{ + background-color: #302F2F; + border-width: 1px; + border-color: #3A3939; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + /*border-radius: 2px;*/ + color: #454545; +} + +QComboBox +{ + selection-background-color: #A9A9A9; + background-color: #201F1F; + border-style: solid; + border: 1px solid #3A3939; + border-radius: 2px; + padding: 2px; + min-width: 75px; +} + +QPushButton:hover +{ + background-color: #3d8ec9; + color: white; +} + +QComboBox:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:hover,QTreeView:hover +{ + border: 1px solid #78879b; + color: silver; +} + +QComboBox:on +{ + background-color: #626873; + padding-top: 3px; + padding-left: 4px; + selection-background-color: #4a4a4a; +} + +QComboBox QAbstractItemView +{ + background-color: #201F1F; + border-radius: 2px; + border: 1px solid #444; + selection-background-color: #A9A9A9; +} + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); +} + +QComboBox::down-arrow:on, QComboBox::down-arrow:hover, +QComboBox::down-arrow:focus +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + +QAbstractSpinBox { + padding-top: 2px; + padding-bottom: 2px; + border: 1px solid #3A3939; + background-color: #201F1F; + color: silver; + border-radius: 2px; + min-width: 75px; +} + +QAbstractSpinBox:up-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; +} + +QAbstractSpinBox:down-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; +} + +QAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off { + image: url(:/qss_icons/rc/up_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::up-arrow:hover +{ + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::down-arrow:hover +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + + +QLabel +{ + border: 0px solid black; + background-color: transparent; +} + +QTabWidget{ + border: 1px transparent black; +} + +QTabWidget::pane { + border: 1px solid #444; + border-radius: 3px; + padding: 3px; +} + +QTabBar +{ + qproperty-drawBase: 0; + left: 5px; /* move to the right by 5px */ +} + +QTabBar:focus +{ + border: 0px transparent black; +} + +QTabBar::close-button { + image: url(:/qss_icons/rc/close.png); + background: transparent; +} + +QTabBar::close-button:hover +{ + image: url(:/qss_icons/rc/close-hover.png); + background: transparent; +} + +QTabBar::close-button:pressed { + image: url(:/qss_icons/rc/close-pressed.png); + background: transparent; +} + +/* TOP TABS */ +QTabBar::tab:top { + color: #b1b1b1; + border: 1px solid #4A4949; + border-bottom: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-bottom: 1px transparent #4A4949; + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +QTabBar::tab:top:!selected:hover { + background-color: #48576b; +} + +/* BOTTOM TABS */ +QTabBar::tab:bottom { + color: #b1b1b1; + border: 1px solid #4A4949; + border-top: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-top: 1px transparent #4A4949; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +QTabBar::tab:bottom:!selected:hover { + background-color: #78879b; +} + +/* LEFT TABS */ +QTabBar::tab:left { + color: #b1b1b1; + border: 1px solid #4A4949; + border-left: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-right: 1px transparent #4A4949; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +QTabBar::tab:left:!selected:hover { + background-color: #48576b; +} + + +/* RIGHT TABS */ +QTabBar::tab:right { + color: #b1b1b1; + border: 1px solid #4A4949; + border-right: 1px transparent black; + background-color: #302F2F; + padding: 5px; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:!selected +{ + color: #b1b1b1; + background-color: #201F1F; + border: 1px transparent #4A4949; + border-right: 1px transparent #4A4949; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +QTabBar::tab:right:!selected:hover { + background-color: #48576b; +} + +QTabBar QToolButton::right-arrow:enabled { + image: url(:/qss_icons/rc/right_arrow.png); + } + + QTabBar QToolButton::left-arrow:enabled { + image: url(:/qss_icons/rc/left_arrow.png); + } + +QTabBar QToolButton::right-arrow:disabled { + image: url(:/qss_icons/rc/right_arrow_disabled.png); + } + + QTabBar QToolButton::left-arrow:disabled { + image: url(:/qss_icons/rc/left_arrow_disabled.png); + } + + +QDockWidget { + border: 1px solid #403F3F; + titlebar-close-icon: url(:/qss_icons/rc/close.png); + titlebar-normal-icon: url(:/qss_icons/rc/undock.png); +} + +QDockWidget::close-button, QDockWidget::float-button { + border: 1px solid transparent; + border-radius: 2px; + background: transparent; +} + +QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background: rgba(255, 255, 255, 10); +} + +QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + padding: 1px -1px -1px 1px; + background: rgba(255, 255, 255, 10); +} + +QTreeView, QListView +{ + border: 1px solid #444; + background-color: #201F1F; +} + +QTreeView:branch:selected, QTreeView:branch:hover +{ + background: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:!adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_closed.png); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_open.png); +} + +QTreeView::branch:has-children:!has-siblings:closed:hover, +QTreeView::branch:closed:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_closed-on.png); + } + +QTreeView::branch:open:has-children:!has-siblings:hover, +QTreeView::branch:open:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_open-on.png); + } + +QListView::item:!selected:hover, QListView::item:!selected:hover, QTreeView::item:!selected:hover { + background: rgba(0, 0, 0, 0); + outline: 0; + color: #FFFFFF +} + +QListView::item:selected:hover, QListView::item:selected:hover, QTreeView::item:selected:hover { + background: #3d8ec9; + color: #FFFFFF; +} + +QSlider::groove:horizontal { + border: 1px solid #3A3939; + height: 8px; + background: #201F1F; + margin: 2px 0; + border-radius: 2px; +} + +QSlider::handle:horizontal { + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0.0 silver, stop: 0.2 #a8a8a8, stop: 1 #727272); + border: 1px solid #3A3939; + width: 14px; + height: 14px; + margin: -4px 0; + border-radius: 2px; +} + +QSlider::groove:vertical { + border: 1px solid #3A3939; + width: 8px; + background: #201F1F; + margin: 0 0px; + border-radius: 2px; +} + +QSlider::handle:vertical { + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 silver, + stop: 0.2 #a8a8a8, stop: 1 #727272); + border: 1px solid #3A3939; + width: 14px; + height: 14px; + margin: 0 -4px; + border-radius: 2px; +} + +QToolButton { + background-color: transparent; + border: 1px transparent #4A4949; + border-radius: 2px; + margin: 3px; + padding: 3px; +} + +QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ + padding-right: 20px; /* make way for the popup button */ + border: 1px transparent #4A4949; + border-radius: 5px; +} + +QToolButton[popupMode="2"] { /* only for InstantPopup */ + padding-right: 10px; /* make way for the popup button */ + border: 1px transparent #4A4949; +} + + +QToolButton:hover, QToolButton::menu-button:hover { + background-color: transparent; + border: 1px solid #78879b; +} + +QToolButton:checked, QToolButton:pressed, + QToolButton::menu-button:pressed { + background-color: #4A4949; + border: 1px solid #78879b; +} + +/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ +QToolButton::menu-indicator { + image: url(:/qss_icons/rc/down_arrow.png); + top: -7px; left: -2px; /* shift it a bit */ +} + +/* the subcontrols below are used only in the MenuButtonPopup mode */ +QToolButton::menu-button { + border: 1px transparent #4A4949; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border = 20px allocated above */ + width: 16px; + outline: none; +} + +QToolButton::menu-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QToolButton::menu-arrow:open { + top: 1px; left: 1px; /* shift it a bit */ + border: 1px solid #3A3939; +} + +QTableView +{ + border: 1px solid #444; + gridline-color: #6c6c6c; + background-color: #201F1F; +} + + +QTableView, QHeaderView +{ + border-radius: 0px; +} + +QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { + background: #78879b; + color: #FFFFFF; +} + +QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { + background: #3d8ec9; + color: #FFFFFF; +} + + +QHeaderView +{ + border: 1px transparent; + border-radius: 2px; + margin: 0px; + padding: 0px; +} + +QHeaderView::section { + background-color: #3A3939; + color: silver; + padding: 4px; + border: 1px solid #6c6c6c; + border-radius: 0px; + text-align: center; +} + +QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one +{ + border-top: 1px solid #6c6c6c; +} + +QHeaderView::section::vertical +{ + border-top: transparent; +} + +QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one +{ + border-left: 1px solid #6c6c6c; +} + +QHeaderView::section::horizontal +{ + border-left: transparent; +} + + +QHeaderView::section:checked + { + color: white; + background-color: #5A5959; + } + + /* style the sort indicator */ +QHeaderView::down-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QHeaderView::up-arrow { + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QTableCornerButton::section { + background-color: #3A3939; + border: 1px solid #3A3939; + border-radius: 2px; +} + +QToolBox { + padding: 3px; + border: 1px transparent black; +} + +QToolBox::tab { + color: #b1b1b1; + background-color: #302F2F; + border: 1px solid #4A4949; + border-bottom: 1px transparent #302F2F; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + + QToolBox::tab:selected { /* italicize selected tabs */ + font: italic; + background-color: #302F2F; + border-color: #3d8ec9; + } + +QStatusBar::item { + border: 1px solid #3A3939; + border-radius: 2px; + } + + +QFrame[height="3"], QFrame[width="3"] { + background-color: #444; +} + + +QSplitter::handle { + border: 1px dashed #3A3939; +} + +QSplitter::handle:hover { + background-color: #787876; + border: 1px solid #3A3939; +} + +QSplitter::handle:horizontal { + width: 1px; +} + +QSplitter::handle:vertical { + height: 1px; +} + +MessageItem +{ + border: none; +} + +MessageBrowser +{ + border: none; +} + +MessageBrowser::focus +{ + border: none; +} + +MessageItem::focus +{ + border: none; +} + +MessageBrowser:hover +{ + border: none; +} + +QListWidget QPushButton +{ + background-color: transparent; + border: none; +} + +QPushButton:hover +{ + background-color: #4A4949; +} + +#messages:item:selected +{ + background-color: #1E90FF; +} + +MessageBrowser +{ + background-color: transparent; +} + +#messages:item:selected QListWidgetItem +{ + background-color: #1E90FF; +} + +#friendsListWidget:item:selected +{ + background-color: #333333; +} + +#toxygen +{ + color: #A9A9A9; +} + +QCheckBox +{ + spacing: 5px; + outline: none; + color: #bbb; + margin-bottom: 2px; + text-align: center; +} + +QListWidget > QLabel +{ + color: #A9A9A9; +} + +#searchLineEdit +{ + padding-left: 22px; +} + +#mainmenubutton +{ + border: 1px solid #3A3939; + color: silver; + margin: 0px; + text-align: center; +} + +#mainmenubutton:hover +{ + background: transparent; + border: 1px solid #A9A9A9; + background-color: #302F2F; +} + +#mainmenubutton:pressed +{ + background: transparent; + border: 1px solid #A9A9A9; + background-color: #302F2F; +} + +#mainmenubutton::menu-indicator +{ + image: none; + width: 0px; + height: 0px; +} + +ClickableLabel:focus +{ + border-width: 1px; + border-color: #4A4949; + border-style: solid; +} + +ClickableLabel:hover +{ + background-color: #4A4949; +} + +#warningLabel +{ + color: #BC1C1C; +} + +#groupInvitesPushButton +{ + background-color: #009c00; +} + diff --git a/toxygen/styles/rc/Hmovetoolbar.png b/toxygen/styles/rc/Hmovetoolbar.png old mode 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 50ddfee..dca54d8 100644 --- a/toxygen/styles/style.py +++ b/toxygen/styles/style.py @@ -1,21 +1,14 @@ -# -*- coding: utf-8 -*- +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -# Resource object code -# -# Created: вт июн 21 13:50:04 2016 -# by: The Resource Compiler for PySide (Qt v4.8.4) -# -# WARNING! All changes made in this file will be lost! - -from PySide 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/styles/style.qrc b/toxygen/styles/style.qrc index ac14bc5..7ceed90 100644 --- a/toxygen/styles/style.qrc +++ b/toxygen/styles/style.qrc @@ -41,6 +41,9 @@ rc/radio_unchecked.png + dark_style.qss + + style.qss diff --git a/toxygen/styles/style.qss b/toxygen/styles/style.qss index 0eddfde..ff9f614 100644 --- a/toxygen/styles/style.qss +++ b/toxygen/styles/style.qss @@ -1,1216 +1,6 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) <2013-2014> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -QToolTip +#searchLineEdit { - border: 1px solid #3A3939; - background-color: rgb(90, 102, 117);; - color: white; - padding: 1px; - opacity: 200; -} - -QWidget -{ - color: silver; - background-color: #302F2F; - selection-background-color: #A9A9A9; - selection-color: black; - background-clip: border; - border-image: none; - outline: 0; -} - -QWidget:item:hover -{ - background-color: #78879b; - color: black; -} - -QWidget:item:selected -{ - background-color: #A9A9A9; -} - -QProgressBar:horizontal { - border: 1px solid #3A3939; - text-align: center; - padding: 1px; - background: #201F1F; -} -QProgressBar::chunk:horizontal { - background-color: qlineargradient(spread:reflect, x1:1, y1:0.545, x2:1, y2:0, stop:0 rgba(28, 66, 111, 255), stop:1 rgba(37, 87, 146, 255)); -} - -QCheckBox:disabled -{ - color: #777777; -} -QCheckBox::indicator, -QGroupBox::indicator -{ - width: 18px; - height: 18px; -} -QGroupBox::indicator -{ - margin-left: 2px; -} - -QCheckBox::indicator:unchecked, -QCheckBox::indicator:unchecked:hover, -QGroupBox::indicator:unchecked, -QGroupBox::indicator:unchecked:hover -{ - image: url(:/qss_icons/rc/checkbox_unchecked.png); -} - -QCheckBox::indicator:unchecked:focus, -QCheckBox::indicator:unchecked:pressed, -QGroupBox::indicator:unchecked:focus, -QGroupBox::indicator:unchecked:pressed -{ - border: none; - image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); -} - -QCheckBox::indicator:checked, -QCheckBox::indicator:checked:hover, -QGroupBox::indicator:checked, -QGroupBox::indicator:checked:hover -{ - image: url(:/qss_icons/rc/checkbox_checked.png); -} - -QCheckBox::indicator:checked:focus, -QCheckBox::indicator:checked:pressed, -QGroupBox::indicator:checked:focus, -QGroupBox::indicator:checked:pressed -{ - border: none; - image: url(:/qss_icons/rc/checkbox_checked_focus.png); -} - -QCheckBox::indicator:indeterminate, -QCheckBox::indicator:indeterminate:hover, -QCheckBox::indicator:indeterminate:pressed -QGroupBox::indicator:indeterminate, -QGroupBox::indicator:indeterminate:hover, -QGroupBox::indicator:indeterminate:pressed -{ - image: url(:/qss_icons/rc/checkbox_indeterminate.png); -} - -QCheckBox::indicator:indeterminate:focus, -QGroupBox::indicator:indeterminate:focus -{ - image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); -} - -QCheckBox::indicator:checked:disabled, -QGroupBox::indicator:checked:disabled -{ - image: url(:/qss_icons/rc/checkbox_checked_disabled.png); -} - -QCheckBox::indicator:unchecked:disabled, -QGroupBox::indicator:unchecked:disabled -{ - image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); -} - -QRadioButton -{ - spacing: 5px; - outline: none; - color: #bbb; - margin-bottom: 2px; -} - -QRadioButton:disabled -{ - color: #777777; -} -QRadioButton::indicator -{ - width: 21px; - height: 21px; -} - -QRadioButton::indicator:unchecked, -QRadioButton::indicator:unchecked:hover -{ - image: url(:/qss_icons/rc/radio_unchecked.png); -} - -QRadioButton::indicator:unchecked:focus, -QRadioButton::indicator:unchecked:pressed -{ - border: none; - outline: none; - image: url(:/qss_icons/rc/radio_unchecked_focus.png); -} - -QRadioButton::indicator:checked, -QRadioButton::indicator:checked:hover -{ - border: none; - outline: none; - image: url(:/qss_icons/rc/radio_checked.png); -} - -QRadioButton::indicator:checked:focus, -QRadioButton::indicato::menu-arrowr:checked:pressed -{ - border: none; - outline: none; - image: url(:/qss_icons/rc/radio_checked_focus.png); -} - -QRadioButton::indicator:indeterminate, -QRadioButton::indicator:indeterminate:hover, -QRadioButton::indicator:indeterminate:pressed -{ - image: url(:/qss_icons/rc/radio_indeterminate.png); -} - -QRadioButton::indicator:checked:disabled -{ - outline: none; - image: url(:/qss_icons/rc/radio_checked_disabled.png); -} - -QRadioButton::indicator:unchecked:disabled -{ - image: url(:/qss_icons/rc/radio_unchecked_disabled.png); -} - - -QMenuBar -{ - background-color: #302F2F; - color: silver; -} - -QMenuBar::item -{ - background: transparent; -} - -QMenuBar::item:selected -{ - background: transparent; - border: 1px solid #A9A9A9; -} - -QMenuBar::item:pressed -{ - border: 1px solid #3A3939; - background-color: #A9A9A9; - color: black; - margin-bottom:-1px; - padding-bottom:1px; -} - -QMenu -{ - border: 1px solid #3A3939; - color: silver; - margin: 2px; -} - -QMenu::icon -{ - margin: 5px; -} - -QMenu::item -{ - padding: 5px 30px 5px 30px; - margin-left: 5px; - border: 1px solid transparent; /* reserve space for selection border */ -} - -QMenu::item:selected -{ - color: black; -} - -QMenu::separator { - height: 2px; - background: lightblue; - margin-left: 10px; - margin-right: 5px; -} - -QMenu::indicator { - width: 18px; - height: 18px; -} - -/* non-exclusive indicator = check box style indicator - (see QActionGroup::setExclusive) */ -QMenu::indicator:non-exclusive:unchecked { - image: url(:/qss_icons/rc/checkbox_unchecked.png); -} - -QMenu::indicator:non-exclusive:unchecked:selected { - image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); -} - -QMenu::indicator:non-exclusive:checked { - image: url(:/qss_icons/rc/checkbox_checked.png); -} - -QMenu::indicator:non-exclusive:checked:selected { - image: url(:/qss_icons/rc/checkbox_checked_disabled.png); -} - -/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ -QMenu::indicator:exclusive:unchecked { - image: url(:/qss_icons/rc/radio_unchecked.png); -} - -QMenu::indicator:exclusive:unchecked:selected { - image: url(:/qss_icons/rc/radio_unchecked_disabled.png); -} - -QMenu::indicator:exclusive:checked { - image: url(:/qss_icons/rc/radio_checked.png); -} - -QMenu::indicator:exclusive:checked:selected { - image: url(:/qss_icons/rc/radio_checked_disabled.png); -} - -QMenu::right-arrow { - margin: 5px; - image: url(:/qss_icons/rc/right_arrow.png) -} - - -QWidget:disabled -{ - color: #404040; - background-color: #302F2F; -} - -QAbstractItemView -{ - alternate-background-color: #3A3939; - color: silver; - border: 1px solid 3A3939; - border-radius: 2px; - padding: 1px; -} - -QWidget:focus, QMenuBar:focus -{ - border: 1px solid #78879b; -} - -QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus -{ - border: none; -} - -QLineEdit -{ - background-color: #201F1F; - padding: 2px; - border-style: solid; - border: 1px solid #3A3939; - border-radius: 2px; - color: silver; -} - -QGroupBox { - border:1px solid #3A3939; - border-radius: 2px; - margin-top: 20px; -} - -QGroupBox::title { - subcontrol-origin: margin; - subcontrol-position: top center; - padding-left: 10px; - padding-right: 10px; - padding-top: 10px; -} - -QAbstractScrollArea -{ - border-radius: 2px; - border: 1px solid #3A3939; - background-color: transparent; -} - -QScrollBar:horizontal -{ - height: 15px; - margin: 3px 15px 3px 15px; - border: 1px transparent #2A2929; - border-radius: 4px; - background-color: #2A2929; -} - -QScrollBar::handle:horizontal -{ - background-color: #605F5F; - min-width: 5px; - border-radius: 4px; -} - -QScrollBar::add-line:horizontal -{ - margin: 0px 3px 0px 3px; - border-image: url(:/qss_icons/rc/right_arrow_disabled.png); - width: 10px; - height: 10px; - subcontrol-position: right; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:horizontal -{ - margin: 0px 3px 0px 3px; - border-image: url(:/qss_icons/rc/left_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on -{ - border-image: url(:/qss_icons/rc/right_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: right; - subcontrol-origin: margin; -} - - -QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on -{ - border-image: url(:/qss_icons/rc/left_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal -{ - background: none; -} - - -QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal -{ - background: none; -} - -QScrollBar:vertical -{ - background-color: #2A2929; - width: 15px; - margin: 15px 3px 15px 3px; - border: 1px transparent #2A2929; - border-radius: 4px; -} - -QScrollBar::handle:vertical -{ - background-color: #605F5F; - min-height: 5px; - border-radius: 4px; -} - -QScrollBar::sub-line:vertical -{ - margin: 3px 0px 3px 0px; - border-image: url(:/qss_icons/rc/up_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: top; - subcontrol-origin: margin; -} - -QScrollBar::add-line:vertical -{ - margin: 3px 0px 3px 0px; - border-image: url(:/qss_icons/rc/down_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on -{ - - border-image: url(:/qss_icons/rc/up_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: top; - subcontrol-origin: margin; -} - - -QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on -{ - border-image: url(:/qss_icons/rc/down_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical -{ - background: none; -} - - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical -{ - background: none; -} - -QTextEdit -{ - background-color: #201F1F; - color: silver; - border: 1px solid #3A3939; -} - -QPlainTextEdit -{ - background-color: #201F1F;; - color: silver; - border-radius: 2px; - border: 1px solid #3A3939; -} - -QHeaderView::section -{ - background-color: #3A3939; - color: silver; - padding-left: 4px; - border: 1px solid #6c6c6c; -} - -QSizeGrip { - image: url(:/qss_icons/rc/sizegrip.png); - width: 12px; - height: 12px; -} - - -QMainWindow::separator -{ - background-color: #302F2F; - color: white; - padding-left: 4px; - spacing: 2px; - border: 1px dashed #3A3939; -} - -QMainWindow::separator:hover -{ - - background-color: #787876; - color: white; - padding-left: 4px; - border: 1px solid #3A3939; - spacing: 2px; -} - - -QMenu::separator -{ - height: 1px; - background-color: #3A3939; - color: white; - padding-left: 4px; - margin-left: 10px; - margin-right: 5px; -} - - -QFrame -{ - border-radius: 2px; - border: 1px solid #444; -} - -QFrame[frameShape="0"] -{ - border-radius: 2px; - border: 1px transparent #444; -} - -QStackedWidget -{ - border: 1px transparent black; -} - -QToolBar { - border: 1px transparent #393838; - background: 1px solid #302F2F; - font-weight: bold; -} - -QToolBar::handle:horizontal { - image: url(:/qss_icons/rc/Hmovetoolbar.png); -} -QToolBar::handle:vertical { - image: url(:/qss_icons/rc/Vmovetoolbar.png); -} -QToolBar::separator:horizontal { - image: url(:/qss_icons/rc/Hsepartoolbar.png); -} -QToolBar::separator:vertical { - image: url(:/qss_icons/rc/Vsepartoolbars.png); -} - -QPushButton -{ - color: silver; - background-color: #302F2F; - border-width: 1px; - border-color: #4A4949; - border-style: solid; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 5px; - padding-right: 5px; - border-radius: 2px; - outline: none; -} - -QPushButton:focus -{ - border-width: 1px; - border-color: #4A4949; - border-style: solid; -} - -QPushButton:disabled -{ - background-color: #302F2F; - border-width: 1px; - border-color: #3A3939; - border-style: solid; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 10px; - padding-right: 10px; - /*border-radius: 2px;*/ - color: #454545; -} - -QComboBox -{ - selection-background-color: #A9A9A9; - background-color: #201F1F; - border-style: solid; - border: 1px solid #3A3939; - border-radius: 2px; - padding: 2px; - min-width: 75px; -} - -QPushButton:hover -{ - background-color: #3d8ec9; - color: white; -} - -QComboBox:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:hover,QTreeView:hover -{ - border: 1px solid #78879b; - color: silver; -} - -QComboBox:on -{ - background-color: #626873; - padding-top: 3px; - padding-left: 4px; - selection-background-color: #4a4a4a; -} - -QComboBox QAbstractItemView -{ - background-color: #201F1F; - border-radius: 2px; - border: 1px solid #444; - selection-background-color: #A9A9A9; -} - -QComboBox::drop-down -{ - subcontrol-origin: padding; - subcontrol-position: top right; - width: 15px; - - border-left-width: 0px; - border-left-color: darkgray; - border-left-style: solid; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} - -QComboBox::down-arrow -{ - image: url(:/qss_icons/rc/down_arrow_disabled.png); -} - -QComboBox::down-arrow:on, QComboBox::down-arrow:hover, -QComboBox::down-arrow:focus -{ - image: url(:/qss_icons/rc/down_arrow.png); -} - -QAbstractSpinBox { - padding-top: 2px; - padding-bottom: 2px; - border: 1px solid #3A3939; - background-color: #201F1F; - color: silver; - border-radius: 2px; - min-width: 75px; -} - -QAbstractSpinBox:up-button -{ - background-color: transparent; - subcontrol-origin: border; - subcontrol-position: center right; -} - -QAbstractSpinBox:down-button -{ - background-color: transparent; - subcontrol-origin: border; - subcontrol-position: center left; -} - -QAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off { - image: url(:/qss_icons/rc/up_arrow_disabled.png); - width: 10px; - height: 10px; -} -QAbstractSpinBox::up-arrow:hover -{ - image: url(:/qss_icons/rc/up_arrow.png); -} - - -QAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off -{ - image: url(:/qss_icons/rc/down_arrow_disabled.png); - width: 10px; - height: 10px; -} -QAbstractSpinBox::down-arrow:hover -{ - image: url(:/qss_icons/rc/down_arrow.png); -} - - -QLabel -{ - border: 0px solid black; - background-color: transparent; -} - -QTabWidget{ - border: 1px transparent black; -} - -QTabWidget::pane { - border: 1px solid #444; - border-radius: 3px; - padding: 3px; -} - -QTabBar -{ - qproperty-drawBase: 0; - left: 5px; /* move to the right by 5px */ -} - -QTabBar:focus -{ - border: 0px transparent black; -} - -QTabBar::close-button { - image: url(:/qss_icons/rc/close.png); - background: transparent; -} - -QTabBar::close-button:hover -{ - image: url(:/qss_icons/rc/close-hover.png); - background: transparent; -} - -QTabBar::close-button:pressed { - image: url(:/qss_icons/rc/close-pressed.png); - background: transparent; -} - -/* TOP TABS */ -QTabBar::tab:top { - color: #b1b1b1; - border: 1px solid #4A4949; - border-bottom: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-top-left-radius: 2px; - border-top-right-radius: 2px; -} - -QTabBar::tab:top:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-bottom: 1px transparent #4A4949; - border-top-left-radius: 0px; - border-top-right-radius: 0px; -} - -QTabBar::tab:top:!selected:hover { - background-color: #48576b; -} - -/* BOTTOM TABS */ -QTabBar::tab:bottom { - color: #b1b1b1; - border: 1px solid #4A4949; - border-top: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; -} - -QTabBar::tab:bottom:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-top: 1px transparent #4A4949; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; -} - -QTabBar::tab:bottom:!selected:hover { - background-color: #78879b; -} - -/* LEFT TABS */ -QTabBar::tab:left { - color: #b1b1b1; - border: 1px solid #4A4949; - border-left: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; -} - -QTabBar::tab:left:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-right: 1px transparent #4A4949; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; -} - -QTabBar::tab:left:!selected:hover { - background-color: #48576b; -} - - -/* RIGHT TABS */ -QTabBar::tab:right { - color: #b1b1b1; - border: 1px solid #4A4949; - border-right: 1px transparent black; - background-color: #302F2F; - padding: 5px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} - -QTabBar::tab:right:!selected -{ - color: #b1b1b1; - background-color: #201F1F; - border: 1px transparent #4A4949; - border-right: 1px transparent #4A4949; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; -} - -QTabBar::tab:right:!selected:hover { - background-color: #48576b; -} - -QTabBar QToolButton::right-arrow:enabled { - image: url(:/qss_icons/rc/right_arrow.png); - } - - QTabBar QToolButton::left-arrow:enabled { - image: url(:/qss_icons/rc/left_arrow.png); - } - -QTabBar QToolButton::right-arrow:disabled { - image: url(:/qss_icons/rc/right_arrow_disabled.png); - } - - QTabBar QToolButton::left-arrow:disabled { - image: url(:/qss_icons/rc/left_arrow_disabled.png); - } - - -QDockWidget { - border: 1px solid #403F3F; - titlebar-close-icon: url(:/qss_icons/rc/close.png); - titlebar-normal-icon: url(:/qss_icons/rc/undock.png); -} - -QDockWidget::close-button, QDockWidget::float-button { - border: 1px solid transparent; - border-radius: 2px; - background: transparent; -} - -QDockWidget::close-button:hover, QDockWidget::float-button:hover { - background: rgba(255, 255, 255, 10); -} - -QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { - padding: 1px -1px -1px 1px; - background: rgba(255, 255, 255, 10); -} - -QTreeView, QListView -{ - border: 1px solid #444; - background-color: #201F1F; -} - -QTreeView:branch:selected, QTreeView:branch:hover -{ - background: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:has-siblings:!adjoins-item { - border-image: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:has-siblings:adjoins-item { - border-image: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:!has-children:!has-siblings:adjoins-item { - border-image: url(:/qss_icons/rc/transparent.png); -} - -QTreeView::branch:has-children:!has-siblings:closed, -QTreeView::branch:closed:has-children:has-siblings { - image: url(:/qss_icons/rc/branch_closed.png); -} - -QTreeView::branch:open:has-children:!has-siblings, -QTreeView::branch:open:has-children:has-siblings { - image: url(:/qss_icons/rc/branch_open.png); -} - -QTreeView::branch:has-children:!has-siblings:closed:hover, -QTreeView::branch:closed:has-children:has-siblings:hover { - image: url(:/qss_icons/rc/branch_closed-on.png); - } - -QTreeView::branch:open:has-children:!has-siblings:hover, -QTreeView::branch:open:has-children:has-siblings:hover { - image: url(:/qss_icons/rc/branch_open-on.png); - } - -QListView::item:!selected:hover, QListView::item:!selected:hover, QTreeView::item:!selected:hover { - background: rgba(0, 0, 0, 0); - outline: 0; - color: #FFFFFF -} - -QListView::item:selected:hover, QListView::item:selected:hover, QTreeView::item:selected:hover { - background: #3d8ec9; - color: #FFFFFF; -} - -QSlider::groove:horizontal { - border: 1px solid #3A3939; - height: 8px; - background: #201F1F; - margin: 2px 0; - border-radius: 2px; -} - -QSlider::handle:horizontal { - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0.0 silver, stop: 0.2 #a8a8a8, stop: 1 #727272); - border: 1px solid #3A3939; - width: 14px; - height: 14px; - margin: -4px 0; - border-radius: 2px; -} - -QSlider::groove:vertical { - border: 1px solid #3A3939; - width: 8px; - background: #201F1F; - margin: 0 0px; - border-radius: 2px; -} - -QSlider::handle:vertical { - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 silver, - stop: 0.2 #a8a8a8, stop: 1 #727272); - border: 1px solid #3A3939; - width: 14px; - height: 14px; - margin: 0 -4px; - border-radius: 2px; -} - -QToolButton { - background-color: transparent; - border: 1px transparent #4A4949; - border-radius: 2px; - margin: 3px; - padding: 3px; -} - -QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ - padding-right: 20px; /* make way for the popup button */ - border: 1px transparent #4A4949; - border-radius: 5px; -} - -QToolButton[popupMode="2"] { /* only for InstantPopup */ - padding-right: 10px; /* make way for the popup button */ - border: 1px transparent #4A4949; -} - - -QToolButton:hover, QToolButton::menu-button:hover { - background-color: transparent; - border: 1px solid #78879b; -} - -QToolButton:checked, QToolButton:pressed, - QToolButton::menu-button:pressed { - background-color: #4A4949; - border: 1px solid #78879b; -} - -/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ -QToolButton::menu-indicator { - image: url(:/qss_icons/rc/down_arrow.png); - top: -7px; left: -2px; /* shift it a bit */ -} - -/* the subcontrols below are used only in the MenuButtonPopup mode */ -QToolButton::menu-button { - border: 1px transparent #4A4949; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - /* 16px width + 4px for border = 20px allocated above */ - width: 16px; - outline: none; -} - -QToolButton::menu-arrow { - image: url(:/qss_icons/rc/down_arrow.png); -} - -QToolButton::menu-arrow:open { - top: 1px; left: 1px; /* shift it a bit */ - border: 1px solid #3A3939; -} - -QPushButton::menu-indicator { - subcontrol-origin: padding; - subcontrol-position: bottom right; - left: 8px; -} - -QTableView -{ - border: 1px solid #444; - gridline-color: #6c6c6c; - background-color: #201F1F; -} - - -QTableView, QHeaderView -{ - border-radius: 0px; -} - -QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { - background: #78879b; - color: #FFFFFF; -} - -QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { - background: #3d8ec9; - color: #FFFFFF; -} - - -QHeaderView -{ - border: 1px transparent; - border-radius: 2px; - margin: 0px; - padding: 0px; -} - -QHeaderView::section { - background-color: #3A3939; - color: silver; - padding: 4px; - border: 1px solid #6c6c6c; - border-radius: 0px; - text-align: center; -} - -QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one -{ - border-top: 1px solid #6c6c6c; -} - -QHeaderView::section::vertical -{ - border-top: transparent; -} - -QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one -{ - border-left: 1px solid #6c6c6c; -} - -QHeaderView::section::horizontal -{ - border-left: transparent; -} - - -QHeaderView::section:checked - { - color: white; - background-color: #5A5959; - } - - /* style the sort indicator */ -QHeaderView::down-arrow { - image: url(:/qss_icons/rc/down_arrow.png); -} - -QHeaderView::up-arrow { - image: url(:/qss_icons/rc/up_arrow.png); -} - - -QTableCornerButton::section { - background-color: #3A3939; - border: 1px solid #3A3939; - border-radius: 2px; -} - -QToolBox { - padding: 3px; - border: 1px transparent black; -} - -QToolBox::tab { - color: #b1b1b1; - background-color: #302F2F; - border: 1px solid #4A4949; - border-bottom: 1px transparent #302F2F; - border-top-left-radius: 5px; - border-top-right-radius: 5px; -} - - QToolBox::tab:selected { /* italicize selected tabs */ - font: italic; - background-color: #302F2F; - border-color: #3d8ec9; - } - -QStatusBar::item { - border: 1px solid #3A3939; - border-radius: 2px; - } - - -QFrame[height="3"], QFrame[width="3"] { - background-color: #444; -} - - -QSplitter::handle { - border: 1px dashed #3A3939; -} - -QSplitter::handle:hover { - background-color: #787876; - border: 1px solid #3A3939; -} - -QSplitter::handle:horizontal { - width: 1px; -} - -QSplitter::handle:vertical { - height: 1px; -} - -MessageItem -{ - border: none; + padding-left: 22px; } MessageEdit @@ -1233,47 +23,18 @@ MessageEdit:hover border: none; } -QListWidget QPushButton -{ - background-color: transparent; - border: none; -} - -QPushButton:hover -{ - background-color: #4A4949; -} - -#messages:item:selected +MessageEdit { background-color: transparent; } -#friends_list:item:selected +#warningLabel { - background-color: #333333; + color: #BC1C1C; } -#toxygen +#groupInvitesPushButton { - color: #A9A9A9; + background-color: #009c00; } -QCheckBox -{ - spacing: 5px; - outline: none; - color: #bbb; - margin-bottom: 2px; - text-align: center; -} - -QListWidget > QLabel -{ - color: #A9A9A9; -} - -#contact_name -{ - padding-left: 22px; -} \ No newline at end of file diff --git a/toxygen/tests/README.txt b/toxygen/tests/README.txt 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/tox.py b/toxygen/tox.py deleted file mode 100644 index 862badd..0000000 --- a/toxygen/tox.py +++ /dev/null @@ -1,1512 +0,0 @@ -# -*- coding: utf-8 -*- -from ctypes import c_char_p, Structure, c_bool, byref, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 -from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8 -from toxcore_enums_and_consts import * -from toxav import ToxAV -from libtox import LibToxCore - - -class ToxOptions(Structure): - _fields_ = [ - ('ipv6_enabled', c_bool), - ('udp_enabled', c_bool), - ('proxy_type', c_int), - ('proxy_host', c_char_p), - ('proxy_port', c_uint16), - ('start_port', c_uint16), - ('end_port', c_uint16), - ('tcp_port', c_uint16), - ('savedata_type', c_int), - ('savedata_data', c_char_p), - ('savedata_length', c_size_t) - ] - - -def string_to_bin(tox_id): - return c_char_p(bytes.fromhex(tox_id)) if tox_id is not None else None - - -def bin_to_string(raw_id, length): - res = ''.join('{:02x}'.format(ord(raw_id[i])) for i in range(length)) - return res.upper() - - -class Tox: - - libtoxcore = LibToxCore() - - def __init__(self, tox_options=None, tox_pointer=None): - """ - Creates and initialises a new Tox instance with the options passed. - - This function will bring the instance into a valid state. Running the event loop with a new instance will - operate correctly. - - :param tox_options: An options object. If this parameter is None, the default options are used. - :param tox_pointer: Tox instance pointer. If this parameter is not None, tox_options will be ignored. - """ - if tox_pointer is not None: - self._tox_pointer = tox_pointer - else: - tox_err_new = c_int() - Tox.libtoxcore.tox_new.restype = POINTER(c_void_p) - self._tox_pointer = Tox.libtoxcore.tox_new(tox_options, byref(tox_err_new)) - tox_err_new = tox_err_new.value - if tox_err_new == TOX_ERR_NEW['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_new == TOX_ERR_NEW['MALLOC']: - raise MemoryError('The function was unable to allocate enough ' - 'memory to store the internal structures for the Tox object.') - elif tox_err_new == TOX_ERR_NEW['PORT_ALLOC']: - raise RuntimeError('The function was unable to bind to a port. This may mean that all ports have ' - 'already been bound, e.g. by other Tox instances, or it may mean a permission error.' - ' You may be able to gather more information from errno.') - elif tox_err_new == TOX_ERR_NEW['PROXY_BAD_TYPE']: - raise ArgumentError('proxy_type was invalid.') - elif tox_err_new == TOX_ERR_NEW['PROXY_BAD_HOST']: - raise ArgumentError('proxy_type was valid but the proxy_host passed had an invalid format or was NULL.') - elif tox_err_new == TOX_ERR_NEW['PROXY_BAD_PORT']: - raise ArgumentError('proxy_type was valid, but the proxy_port was invalid.') - elif tox_err_new == TOX_ERR_NEW['PROXY_NOT_FOUND']: - raise ArgumentError('The proxy address passed could not be resolved.') - elif tox_err_new == TOX_ERR_NEW['LOAD_ENCRYPTED']: - raise ArgumentError('The byte array to be loaded contained an encrypted save.') - elif tox_err_new == TOX_ERR_NEW['LOAD_BAD_FORMAT']: - raise ArgumentError('The data format was invalid. This can happen when loading data that was saved by' - ' an older version of Tox, or when the data has been corrupted. When loading from' - ' badly formatted data, some data may have been loaded, and the rest is discarded.' - ' Passing an invalid length parameter also causes this error.') - - self.self_connection_status_cb = None - self.friend_name_cb = None - self.friend_status_message_cb = None - self.friend_status_cb = None - self.friend_connection_status_cb = None - self.friend_request_cb = None - self.friend_read_receipt_cb = None - self.friend_typing_cb = None - self.friend_message_cb = None - self.file_recv_control_cb = None - self.file_chunk_request_cb = None - self.file_recv_cb = None - self.file_recv_chunk_cb = None - self.friend_lossy_packet_cb = None - self.friend_lossless_packet_cb = None - - self.AV = ToxAV(self._tox_pointer) - - def __del__(self): - del self.AV - Tox.libtoxcore.tox_kill(self._tox_pointer) - - # ----------------------------------------------------------------------------------------------------------------- - # Startup options - # ----------------------------------------------------------------------------------------------------------------- - - @staticmethod - def options_default(tox_options): - """ - Initialises a Tox_Options object with the default options. - - The result of this function is independent of the original options. All values will be overwritten, no values - will be read (so it is permissible to pass an uninitialised object). - - If options is NULL, this function has no effect. - - :param tox_options: A pointer to options object to be filled with default options. - """ - Tox.libtoxcore.tox_options_default(tox_options) - - @staticmethod - def options_new(): - """ - Allocates a new Tox_Options object and initialises it with the default options. This function can be used to - preserve long term ABI compatibility by giving the responsibility of allocation and deallocation to the Tox - library. - - Objects returned from this function must be freed using the tox_options_free function. - - :return: A pointer to new ToxOptions object with default options or raise MemoryError. - """ - tox_err_options_new = c_int() - f = Tox.libtoxcore.tox_options_new - f.restype = POINTER(ToxOptions) - result = f(byref(tox_err_options_new)) - tox_err_options_new = tox_err_options_new.value - if tox_err_options_new == TOX_ERR_OPTIONS_NEW['OK']: - return result - elif tox_err_options_new == TOX_ERR_OPTIONS_NEW['MALLOC']: - raise MemoryError('The function failed to allocate enough memory for the options struct.') - - @staticmethod - def options_free(tox_options): - """ - Releases all resources associated with an options objects. - - Passing a pointer that was not returned by tox_options_new results in undefined behaviour. - - :param tox_options: A pointer to new ToxOptions object - """ - Tox.libtoxcore.tox_options_free(tox_options) - - # ----------------------------------------------------------------------------------------------------------------- - # Creation and destruction - # ----------------------------------------------------------------------------------------------------------------- - - def get_savedata_size(self): - """ - Calculates the number of bytes required to store the tox instance with tox_get_savedata. - This function cannot fail. The result is always greater than 0. - - :return: number of bytes - """ - return Tox.libtoxcore.tox_get_savedata_size(self._tox_pointer) - - def get_savedata(self, savedata=None): - """ - Store all information associated with the tox instance to a byte array. - - :param savedata: pointer (c_char_p) to a memory region large enough to store the tox instance data. - Call tox_get_savedata_size to find the number of bytes required. If this parameter is None, this function - allocates memory for the tox instance data. - :return: pointer (c_char_p) to a memory region with the tox instance data - """ - if savedata is None: - savedata_size = self.get_savedata_size() - savedata = create_string_buffer(savedata_size) - Tox.libtoxcore.tox_get_savedata(self._tox_pointer, savedata) - return savedata[:] - - # ----------------------------------------------------------------------------------------------------------------- - # Connection lifecycle and event loop - # ----------------------------------------------------------------------------------------------------------------- - - def bootstrap(self, address, port, public_key): - """ - Sends a "get nodes" request to the given bootstrap node with IP, port, and public key to setup connections. - - This function will attempt to connect to the node using UDP. You must use this function even if - Tox_Options.udp_enabled was set to false. - - :param address: The hostname or IP address (IPv4 or IPv6) of the node. - :param port: The port on the host on which the bootstrap Tox instance is listening. - :param public_key: The long term public key of the bootstrap node (TOX_PUBLIC_KEY_SIZE bytes). - :return: True on success. - """ - tox_err_bootstrap = c_int() - result = Tox.libtoxcore.tox_bootstrap(self._tox_pointer, c_char_p(address), c_uint16(port), - string_to_bin(public_key), byref(tox_err_bootstrap)) - tox_err_bootstrap = tox_err_bootstrap.value - if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['OK']: - return bool(result) - elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']: - raise ArgumentError('The address could not be resolved to an IP ' - 'address, or the IP address passed was invalid.') - elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']: - raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).') - - def add_tcp_relay(self, address, port, public_key): - """ - Adds additional host:port pair as TCP relay. - - This function can be used to initiate TCP connections to different ports on the same bootstrap node, or to add - TCP relays without using them as bootstrap nodes. - - :param address: The hostname or IP address (IPv4 or IPv6) of the TCP relay. - :param port: The port on the host on which the TCP relay is listening. - :param public_key: The long term public key of the TCP relay (TOX_PUBLIC_KEY_SIZE bytes). - :return: True on success. - """ - tox_err_bootstrap = c_int() - result = Tox.libtoxcore.tox_add_tcp_relay(self._tox_pointer, c_char_p(address), c_uint16(port), - string_to_bin(public_key), byref(tox_err_bootstrap)) - tox_err_bootstrap = tox_err_bootstrap.value - if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['OK']: - return bool(result) - elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']: - raise ArgumentError('The address could not be resolved to an IP ' - 'address, or the IP address passed was invalid.') - elif tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']: - raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).') - - def self_get_connection_status(self): - """ - Return whether we are connected to the DHT. The return value is equal to the last value received through the - `self_connection_status` callback. - - :return: TOX_CONNECTION - """ - return Tox.libtoxcore.tox_self_get_connection_status(self._tox_pointer) - - def callback_self_connection_status(self, callback, user_data): - """ - Set the callback for the `self_connection_status` event. Pass None to unset. - - This event is triggered whenever there is a change in the DHT connection state. When disconnected, a client may - choose to call tox_bootstrap again, to reconnect to the DHT. Note that this state may frequently change for - short amounts of time. Clients should therefore not immediately bootstrap on receiving a disconnect. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - TOX_CONNECTION (c_int), - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_int, c_void_p) - self.self_connection_status_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_self_connection_status(self._tox_pointer, - self.self_connection_status_cb, user_data) - - def iteration_interval(self): - """ - Return the time in milliseconds before tox_iterate() should be called again for optimal performance. - :return: time in milliseconds - """ - return Tox.libtoxcore.tox_iteration_interval(self._tox_pointer) - - def iterate(self): - """ - The main loop that needs to be run in intervals of tox_iteration_interval() milliseconds. - """ - Tox.libtoxcore.tox_iterate(self._tox_pointer) - - # ----------------------------------------------------------------------------------------------------------------- - # Internal client information (Tox address/id) - # ----------------------------------------------------------------------------------------------------------------- - - def self_get_address(self, address=None): - """ - Writes the Tox friend address of the client to a byte array. The address is not in human-readable format. If a - client wants to display the address, formatting is required. - - :param address: pointer (c_char_p) to a memory region of at least TOX_ADDRESS_SIZE bytes. If this parameter is - None, this function allocates memory for address. - :return: Tox friend address - """ - if address is None: - address = create_string_buffer(TOX_ADDRESS_SIZE) - Tox.libtoxcore.tox_self_get_address(self._tox_pointer, address) - return bin_to_string(address, TOX_ADDRESS_SIZE) - - def self_set_nospam(self, nospam): - """ - Set the 4-byte nospam part of the address. - - :param nospam: Any 32 bit unsigned integer. - """ - Tox.libtoxcore.tox_self_set_nospam(self._tox_pointer, c_uint32(nospam)) - - def self_get_nospam(self): - """ - Get the 4-byte nospam part of the address. - - :return: nospam part of the address - """ - return Tox.libtoxcore.tox_self_get_nospam(self._tox_pointer) - - def self_get_public_key(self, public_key=None): - """ - Copy the Tox Public Key (long term) from the Tox object. - - :param public_key: A memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this parameter is NULL, this - function allocates memory for Tox Public Key. - :return: Tox Public Key - """ - if public_key is None: - public_key = create_string_buffer(TOX_PUBLIC_KEY_SIZE) - Tox.libtoxcore.tox_self_get_public_key(self._tox_pointer, public_key) - return bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE) - - def self_get_secret_key(self, secret_key=None): - """ - Copy the Tox Secret Key from the Tox object. - - :param secret_key: pointer (c_char_p) to a memory region of at least TOX_SECRET_KEY_SIZE bytes. If this - parameter is NULL, this function allocates memory for Tox Secret Key. - :return: Tox Secret Key - """ - if secret_key is None: - secret_key = create_string_buffer(TOX_SECRET_KEY_SIZE) - Tox.libtoxcore.tox_self_get_secret_key(self._tox_pointer, secret_key) - return bin_to_string(secret_key, TOX_SECRET_KEY_SIZE) - - # ----------------------------------------------------------------------------------------------------------------- - # User-visible client information (nickname/status) - # ----------------------------------------------------------------------------------------------------------------- - - def self_set_name(self, name): - """ - Set the nickname for the Tox client. - - Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is 0, the name parameter is ignored - (it can be None), and the nickname is set back to empty. - :param name: New nickname. - :return: True on success. - """ - tox_err_set_info = c_int() - result = Tox.libtoxcore.tox_self_set_name(self._tox_pointer, c_char_p(name), - c_size_t(len(name)), byref(tox_err_set_info)) - tox_err_set_info = tox_err_set_info.value - if tox_err_set_info == TOX_ERR_SET_INFO['OK']: - return bool(result) - elif tox_err_set_info == TOX_ERR_SET_INFO['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_set_info == TOX_ERR_SET_INFO['TOO_LONG']: - raise ArgumentError('Information length exceeded maximum permissible size.') - - def self_get_name_size(self): - """ - Return the length of the current nickname as passed to tox_self_set_name. - - If no nickname was set before calling this function, the name is empty, and this function returns 0. - - :return: length of the current nickname - """ - return Tox.libtoxcore.tox_self_get_name_size(self._tox_pointer) - - def self_get_name(self, name=None): - """ - Write the nickname set by tox_self_set_name to a byte array. - - If no nickname was set before calling this function, the name is empty, and this function has no effect. - - Call tox_self_get_name_size to find out how much memory to allocate for the result. - - :param name: pointer (c_char_p) to a memory region location large enough to hold the nickname. If this parameter - is NULL, the function allocates memory for the nickname. - :return: nickname - """ - if name is None: - name = create_string_buffer(self.self_get_name_size()) - Tox.libtoxcore.tox_self_get_name(self._tox_pointer, name) - return str(name.value, 'utf-8') - - def self_set_status_message(self, status_message): - """ - Set the client's status message. - - Status message length cannot exceed TOX_MAX_STATUS_MESSAGE_LENGTH. If length is 0, the status parameter is - ignored, and the user status is set back to empty. - - :param status_message: new status message - :return: True on success. - """ - tox_err_set_info = c_int() - result = Tox.libtoxcore.tox_self_set_status_message(self._tox_pointer, c_char_p(status_message), - c_size_t(len(status_message)), byref(tox_err_set_info)) - tox_err_set_info = tox_err_set_info.value - if tox_err_set_info == TOX_ERR_SET_INFO['OK']: - return bool(result) - elif tox_err_set_info == TOX_ERR_SET_INFO['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_set_info == TOX_ERR_SET_INFO['TOO_LONG']: - raise ArgumentError('Information length exceeded maximum permissible size.') - - def self_get_status_message_size(self): - """ - Return the length of the current status message as passed to tox_self_set_status_message. - - If no status message was set before calling this function, the status is empty, and this function returns 0. - - :return: length of the current status message - """ - return Tox.libtoxcore.tox_self_get_status_message_size(self._tox_pointer) - - def self_get_status_message(self, status_message=None): - """ - Write the status message set by tox_self_set_status_message to a byte array. - - If no status message was set before calling this function, the status is empty, and this function has no effect. - - Call tox_self_get_status_message_size to find out how much memory to allocate for the result. - - :param status_message: pointer (c_char_p) to a valid memory location large enough to hold the status message. - If this parameter is None, the function allocates memory for the status message. - :return: status message - """ - if status_message is None: - status_message = create_string_buffer(self.self_get_status_message_size()) - Tox.libtoxcore.tox_self_get_status_message(self._tox_pointer, status_message) - return str(status_message.value, 'utf-8') - - def self_set_status(self, status): - """ - Set the client's user status. - - :param status: One of the user statuses listed in the enumeration TOX_USER_STATUS. - """ - Tox.libtoxcore.tox_self_set_status(self._tox_pointer, c_int(status)) - - def self_get_status(self): - """ - Returns the client's user status. - - :return: client's user status - """ - return Tox.libtoxcore.tox_self_get_status(self._tox_pointer) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend list management - # ----------------------------------------------------------------------------------------------------------------- - - def friend_add(self, address, message): - """ - Add a friend to the friend list and send a friend request. - - A friend request message must be at least 1 byte long and at most TOX_MAX_FRIEND_REQUEST_LENGTH. - - Friend numbers are unique identifiers used in all functions that operate on friends. Once added, a friend number - is stable for the lifetime of the Tox object. After saving the state and reloading it, the friend numbers may - not be the same as before. Deleting a friend creates a gap in the friend number set, which is filled by the next - adding of a friend. Any pattern in friend numbers should not be relied on. - - If more than INT32_MAX friends are added, this function causes undefined behaviour. - - :param address: The address of the friend (returned by tox_self_get_address of the friend you wish to add) it - must be TOX_ADDRESS_SIZE bytes. - :param message: The message that will be sent along with the friend request. - :return: the friend number on success, UINT32_MAX on failure. - """ - tox_err_friend_add = c_int() - result = Tox.libtoxcore.tox_friend_add(self._tox_pointer, string_to_bin(address), c_char_p(message), - c_size_t(len(message)), byref(tox_err_friend_add)) - tox_err_friend_add = tox_err_friend_add.value - if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OK']: - return result - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['TOO_LONG']: - raise ArgumentError('The length of the friend request message exceeded TOX_MAX_FRIEND_REQUEST_LENGTH.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NO_MESSAGE']: - raise ArgumentError('The friend request message was empty. This, and the TOO_LONG code will never be' - ' returned from tox_friend_add_norequest.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['OWN_KEY']: - raise ArgumentError('The friend address belongs to the sending client.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['ALREADY_SENT']: - raise ArgumentError('A friend request has already been sent, or the address belongs to a friend that is' - ' already on the friend list.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['BAD_CHECKSUM']: - raise ArgumentError('The friend address checksum failed.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['SET_NEW_NOSPAM']: - raise ArgumentError('The friend was already there, but the nospam value was different.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']: - raise MemoryError('A memory allocation failed when trying to increase the friend list size.') - - def friend_add_norequest(self, public_key): - """ - Add a friend without sending a friend request. - - This function is used to add a friend in response to a friend request. If the client receives a friend request, - it can be reasonably sure that the other client added this client as a friend, eliminating the need for a friend - request. - - This function is also useful in a situation where both instances are controlled by the same entity, so that this - entity can perform the mutual friend adding. In this case, there is no need for a friend request, either. - - :param public_key: A byte array of length TOX_PUBLIC_KEY_SIZE containing the Public Key (not the Address) of the - friend to add. - :return: the friend number on success, UINT32_MAX on failure. - """ - tox_err_friend_add = c_int() - result = Tox.libtoxcore.tox_friend_add_norequest(self._tox_pointer, string_to_bin(public_key), - byref(tox_err_friend_add)) - tox_err_friend_add = tox_err_friend_add.value - if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OK']: - return result - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['TOO_LONG']: - raise ArgumentError('The length of the friend request message exceeded TOX_MAX_FRIEND_REQUEST_LENGTH.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['NO_MESSAGE']: - raise ArgumentError('The friend request message was empty. This, and the TOO_LONG code will never be' - ' returned from tox_friend_add_norequest.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['OWN_KEY']: - raise ArgumentError('The friend address belongs to the sending client.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['ALREADY_SENT']: - raise ArgumentError('A friend request has already been sent, or the address belongs to a friend that is' - ' already on the friend list.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['BAD_CHECKSUM']: - raise ArgumentError('The friend address checksum failed.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['SET_NEW_NOSPAM']: - raise ArgumentError('The friend was already there, but the nospam value was different.') - elif tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']: - raise MemoryError('A memory allocation failed when trying to increase the friend list size.') - - def friend_delete(self, friend_number): - """ - Remove a friend from the friend list. - - This does not notify the friend of their deletion. After calling this function, this client will appear offline - to the friend and no communication can occur between the two. - - :param friend_number: Friend number for the friend to be deleted. - :return: True on success. - """ - tox_err_friend_delete = c_int() - result = Tox.libtoxcore.tox_friend_delete(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_friend_delete)) - tox_err_friend_delete = tox_err_friend_delete.value - if tox_err_friend_delete == TOX_ERR_FRIEND_DELETE['OK']: - return bool(result) - elif tox_err_friend_delete == TOX_ERR_FRIEND_DELETE['FRIEND_NOT_FOUND']: - raise ArgumentError('There was no friend with the given friend number. No friends were deleted.') - - # ----------------------------------------------------------------------------------------------------------------- - # Friend list queries - # ----------------------------------------------------------------------------------------------------------------- - - def friend_by_public_key(self, public_key): - """ - Return the friend number associated with that Public Key. - - :param public_key: A byte array containing the Public Key. - :return: friend number - """ - tox_err_friend_by_public_key = c_int() - result = Tox.libtoxcore.tox_friend_by_public_key(self._tox_pointer, string_to_bin(public_key), - byref(tox_err_friend_by_public_key)) - tox_err_friend_by_public_key = tox_err_friend_by_public_key.value - if tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['OK']: - return result - elif tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NOT_FOUND']: - raise ArgumentError('No friend with the given Public Key exists on the friend list.') - - def friend_exists(self, friend_number): - """ - Checks if a friend with the given friend number exists and returns true if it does. - """ - return bool(Tox.libtoxcore.tox_friend_exists(self._tox_pointer, c_uint32(friend_number))) - - def self_get_friend_list_size(self): - """ - Return the number of friends on the friend list. - - This function can be used to determine how much memory to allocate for tox_self_get_friend_list. - - :return: number of friends - """ - return Tox.libtoxcore.tox_self_get_friend_list_size(self._tox_pointer) - - def self_get_friend_list(self, friend_list=None): - """ - Copy a list of valid friend numbers into an array. - - Call tox_self_get_friend_list_size to determine the number of elements to allocate. - - :param friend_list: pointer (c_char_p) to a memory region with enough space to hold the friend list. If this - parameter is None, this function allocates memory for the friend list. - :return: friend list - """ - friend_list_size = self.self_get_friend_list_size() - if friend_list is None: - friend_list = create_string_buffer(sizeof(c_uint32) * friend_list_size) - friend_list = POINTER(c_uint32)(friend_list) - Tox.libtoxcore.tox_self_get_friend_list(self._tox_pointer, friend_list) - return friend_list[0:friend_list_size] - - def friend_get_public_key(self, friend_number, public_key=None): - """ - Copies the Public Key associated with a given friend number to a byte array. - - :param friend_number: The friend number you want the Public Key of. - :param public_key: pointer (c_char_p) to a memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this - parameter is None, this function allocates memory for Tox Public Key. - :return: Tox Public Key - """ - if public_key is None: - public_key = create_string_buffer(TOX_PUBLIC_KEY_SIZE) - tox_err_friend_get_public_key = c_int() - Tox.libtoxcore.tox_friend_get_public_key(self._tox_pointer, c_uint32(friend_number), public_key, - byref(tox_err_friend_get_public_key)) - tox_err_friend_get_public_key = tox_err_friend_get_public_key.value - if tox_err_friend_get_public_key == TOX_ERR_FRIEND_GET_PUBLIC_KEY['OK']: - return bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE) - elif tox_err_friend_get_public_key == TOX_ERR_FRIEND_GET_PUBLIC_KEY['FRIEND_NOT_FOUND']: - raise ArgumentError('No friend with the given number exists on the friend list.') - - def friend_get_last_online(self, friend_number): - """ - Return a unix-time timestamp of the last time the friend associated with a given friend number was seen online. - This function will return UINT64_MAX on error. - - :param friend_number: The friend number you want to query. - :return: unix-time timestamp - """ - tox_err_last_online = c_int() - result = Tox.libtoxcore.tox_friend_get_last_online(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_last_online)) - tox_err_last_online = tox_err_last_online.value - if tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['OK']: - return result - elif tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['FRIEND_NOT_FOUND']: - raise ArgumentError('No friend with the given number exists on the friend list.') - - # ----------------------------------------------------------------------------------------------------------------- - # Friend-specific state queries (can also be received through callbacks) - # ----------------------------------------------------------------------------------------------------------------- - - def friend_get_name_size(self, friend_number): - """ - Return the length of the friend's name. If the friend number is invalid, the return value is unspecified. - - The return value is equal to the `length` argument received by the last `friend_name` callback. - """ - tox_err_friend_query = c_int() - result = Tox.libtoxcore.tox_friend_get_name_size(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return result - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def friend_get_name(self, friend_number, name=None): - """ - Write the name of the friend designated by the given friend number to a byte array. - - Call tox_friend_get_name_size to determine the allocation size for the `name` parameter. - - The data written to `name` is equal to the data received by the last `friend_name` callback. - - :param friend_number: number of friend - :param name: pointer (c_char_p) to a valid memory region large enough to store the friend's name. - :return: name of the friend - """ - if name is None: - name = create_string_buffer(self.friend_get_name_size(friend_number)) - tox_err_friend_query = c_int() - Tox.libtoxcore.tox_friend_get_name(self._tox_pointer, c_uint32(friend_number), name, - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return str(name.value, 'utf-8') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def callback_friend_name(self, callback, user_data): - """ - Set the callback for the `friend_name` event. Pass None to unset. - - This event is triggered when a friend changes their name. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend whose name changed, - A byte array (c_char_p) containing the same data as tox_friend_get_name would write to its `name` parameter, - A value (c_size_t) equal to the return value of tox_friend_get_name_size, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) - self.friend_name_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_name(self._tox_pointer, self.friend_name_cb, user_data) - - def friend_get_status_message_size(self, friend_number): - """ - Return the length of the friend's status message. If the friend number is invalid, the return value is SIZE_MAX. - - :return: length of the friend's status message - """ - tox_err_friend_query = c_int() - result = Tox.libtoxcore.tox_friend_get_status_message_size(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return result - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def friend_get_status_message(self, friend_number, status_message=None): - """ - Write the status message of the friend designated by the given friend number to a byte array. - - Call tox_friend_get_status_message_size to determine the allocation size for the `status_name` parameter. - - The data written to `status_message` is equal to the data received by the last `friend_status_message` callback. - - :param friend_number: - :param status_message: pointer (c_char_p) to a valid memory region large enough to store the friend's status - message. - :return: status message of the friend - """ - if status_message is None: - status_message = create_string_buffer(self.friend_get_status_message_size(friend_number)) - tox_err_friend_query = c_int() - Tox.libtoxcore.tox_friend_get_status_message(self._tox_pointer, c_uint32(friend_number), status_message, - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return str(status_message.value, 'utf-8') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def callback_friend_status_message(self, callback, user_data): - """ - Set the callback for the `friend_status_message` event. Pass NULL to unset. - - This event is triggered when a friend changes their status message. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend whose status message changed, - A byte array (c_char_p) containing the same data as tox_friend_get_status_message would write to its - `status_message` parameter, - A value (c_size_t) equal to the return value of tox_friend_get_status_message_size, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) - self.friend_status_message_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_status_message(self._tox_pointer, - self.friend_status_message_cb, c_void_p(user_data)) - - def friend_get_status(self, friend_number): - """ - Return the friend's user status (away/busy/...). If the friend number is invalid, the return value is - unspecified. - - The status returned is equal to the last status received through the `friend_status` callback. - - :return: TOX_USER_STATUS - """ - tox_err_friend_query = c_int() - result = Tox.libtoxcore.tox_friend_get_status(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return result - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def callback_friend_status(self, callback, user_data): - """ - Set the callback for the `friend_status` event. Pass None to unset. - - This event is triggered when a friend changes their user status. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend whose user status changed, - The new user status (TOX_USER_STATUS), - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) - self.friend_status_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_status(self._tox_pointer, self.friend_status_cb, c_void_p(user_data)) - - def friend_get_connection_status(self, friend_number): - """ - Check whether a friend is currently connected to this client. - - The result of this function is equal to the last value received by the `friend_connection_status` callback. - - :param friend_number: The friend number for which to query the connection status. - :return: the friend's connection status (TOX_CONNECTION) as it was received through the - `friend_connection_status` event. - """ - tox_err_friend_query = c_int() - result = Tox.libtoxcore.tox_friend_get_connection_status(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return result - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def callback_friend_connection_status(self, callback, user_data): - """ - Set the callback for the `friend_connection_status` event. Pass NULL to unset. - - This event is triggered when a friend goes offline after having been online, or when a friend goes online. - - This callback is not called when adding friends. It is assumed that when adding friends, their connection status - is initially offline. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend whose connection status changed, - The result of calling tox_friend_get_connection_status (TOX_CONNECTION) on the passed friend_number, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) - self.friend_connection_status_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_connection_status(self._tox_pointer, - self.friend_connection_status_cb, c_void_p(user_data)) - - def friend_get_typing(self, friend_number): - """ - Check whether a friend is currently typing a message. - - :param friend_number: The friend number for which to query the typing status. - :return: true if the friend is typing. - """ - tox_err_friend_query = c_int() - result = Tox.libtoxcore.tox_friend_get_typing(self._tox_pointer, c_uint32(friend_number), - byref(tox_err_friend_query)) - tox_err_friend_query = tox_err_friend_query.value - if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']: - return bool(result) - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']: - raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike' - ' the `_self_` variants of these functions, which have no effect when a parameter is' - ' NULL, these functions return an error in that case.') - elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number did not designate a valid friend.') - - def callback_friend_typing(self, callback, user_data): - """ - Set the callback for the `friend_typing` event. Pass NULL to unset. - - This event is triggered when a friend starts or stops typing. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend who started or stopped typing, - The result of calling tox_friend_get_typing (c_bool) on the passed friend_number, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_bool, c_void_p) - self.friend_typing_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_typing(self._tox_pointer, self.friend_typing_cb, c_void_p(user_data)) - - # ----------------------------------------------------------------------------------------------------------------- - # Sending private messages - # ----------------------------------------------------------------------------------------------------------------- - - def self_set_typing(self, friend_number, typing): - """ - Set the client's typing status for a friend. - - The client is responsible for turning it on or off. - - :param friend_number: The friend to which the client is typing a message. - :param typing: The typing status. True means the client is typing. - :return: True on success. - """ - tox_err_set_typing = c_int() - result = Tox.libtoxcore.tox_self_set_typing(self._tox_pointer, c_uint32(friend_number), - c_bool(typing), byref(tox_err_set_typing)) - tox_err_set_typing = tox_err_set_typing.value - if tox_err_set_typing == TOX_ERR_SET_TYPING['OK']: - return bool(result) - elif tox_err_set_typing == TOX_ERR_SET_TYPING['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend number did not designate a valid friend.') - - def friend_send_message(self, friend_number, message_type, message): - """ - Send a text chat message to an online friend. - - This function creates a chat message packet and pushes it into the send queue. - - The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages must be split by the client and sent - as separate messages. Other clients can then reassemble the fragments. Messages may not be empty. - - The return value of this function is the message ID. If a read receipt is received, the triggered - `friend_read_receipt` event will be passed this message ID. - - Message IDs are unique per friend. The first message ID is 0. Message IDs are incremented by 1 each time a - message is sent. If UINT32_MAX messages were sent, the next message ID is 0. - - :param friend_number: The friend number of the friend to send the message to. - :param message_type: Message type (TOX_MESSAGE_TYPE). - :param message: A non-None message text. - :return: message ID - """ - tox_err_friend_send_message = c_int() - result = Tox.libtoxcore.tox_friend_send_message(self._tox_pointer, c_uint32(friend_number), - c_int(message_type), c_char_p(message), c_size_t(len(message)), - byref(tox_err_friend_send_message)) - tox_err_friend_send_message = tox_err_friend_send_message.value - if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['OK']: - return result - elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend number did not designate a valid friend.') - elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['SENDQ']: - raise MemoryError('An allocation error occurred while increasing the send queue size.') - elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['TOO_LONG']: - raise ArgumentError('Message length exceeded TOX_MAX_MESSAGE_LENGTH.') - elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['EMPTY']: - raise ArgumentError('Attempted to send a zero-length message.') - - def callback_friend_read_receipt(self, callback, user_data): - """ - Set the callback for the `friend_read_receipt` event. Pass None to unset. - - This event is triggered when the friend receives the message sent with tox_friend_send_message with the - corresponding message ID. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend who received the message, - The message ID (c_uint32) as returned from tox_friend_send_message corresponding to the message sent, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) - self.friend_read_receipt_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_read_receipt(self._tox_pointer, - self.friend_read_receipt_cb, c_void_p(user_data)) - - # ----------------------------------------------------------------------------------------------------------------- - # Receiving private messages and friend requests - # ----------------------------------------------------------------------------------------------------------------- - - def callback_friend_request(self, callback, user_data): - """ - Set the callback for the `friend_request` event. Pass None to unset. - - This event is triggered when a friend request is received. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The Public Key (c_uint8 array) of the user who sent the friend request, - The message (c_char_p) they sent along with the request, - The size (c_size_t) of the message byte array, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, POINTER(c_uint8), c_char_p, c_size_t, c_void_p) - self.friend_request_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_request(self._tox_pointer, self.friend_request_cb, c_void_p(user_data)) - - def callback_friend_message(self, callback, user_data): - """ - Set the callback for the `friend_message` event. Pass None to unset. - - This event is triggered when a message from a friend is received. - - :param callback: Python function. Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend who sent the message, - Message type (TOX_MESSAGE_TYPE), - The message data (c_char_p) they sent, - The size (c_size_t) of the message byte array. - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_char_p, c_size_t, c_void_p) - self.friend_message_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_friend_message(self._tox_pointer, self.friend_message_cb, c_void_p(user_data)) - - # ----------------------------------------------------------------------------------------------------------------- - # File transmission: common between sending and receiving - # ----------------------------------------------------------------------------------------------------------------- - - @staticmethod - def hash(data, hash=None): - """ - Generates a cryptographic hash of the given data. - - This function may be used by clients for any purpose, but is provided primarily for validating cached avatars. - This use is highly recommended to avoid unnecessary avatar updates. - - If hash is NULL or data is NULL while length is not 0 the function returns false, otherwise it returns true. - - This function is a wrapper to internal message-digest functions. - - :param hash: A valid memory location the hash data. It must be at least TOX_HASH_LENGTH bytes in size. - :param data: Data to be hashed or NULL. - :return: true if hash was not NULL. - """ - if hash is None: - hash = create_string_buffer(TOX_HASH_LENGTH) - Tox.libtoxcore.tox_hash(hash, c_char_p(data), len(data)) - return bin_to_string(hash, TOX_HASH_LENGTH) - - def file_control(self, friend_number, file_number, control): - """ - Sends a file control command to a friend for a given file transfer. - - :param friend_number: The friend number of the friend the file is being transferred to or received from. - :param file_number: The friend-specific identifier for the file transfer. - :param control: The control (TOX_FILE_CONTROL) command to send. - :return: True on success. - """ - tox_err_file_control = c_int() - result = Tox.libtoxcore.tox_file_control(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), - c_int(control), byref(tox_err_file_control)) - tox_err_file_control = tox_err_file_control.value - if tox_err_file_control == TOX_ERR_FILE_CONTROL['OK']: - return bool(result) - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number passed did not designate a valid friend.') - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_FOUND']: - raise ArgumentError('No file transfer with the given file number was found for the given friend.') - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_PAUSED']: - raise RuntimeError('A RESUME control was sent, but the file transfer is running normally.') - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['DENIED']: - raise RuntimeError('A RESUME control was sent, but the file transfer was paused by the other party. Only ' - 'the party that paused the transfer can resume it.') - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['ALREADY_PAUSED']: - raise RuntimeError('A PAUSE control was sent, but the file transfer was already paused.') - elif tox_err_file_control == TOX_ERR_FILE_CONTROL['SENDQ']: - raise RuntimeError('Packet queue is full.') - - def callback_file_recv_control(self, callback, user_data): - """ - Set the callback for the `file_recv_control` event. Pass NULL to unset. - - This event is triggered when a file control command is received from a friend. - - :param callback: Python function. - When receiving TOX_FILE_CONTROL_CANCEL, the client should release the resources associated with the file number - and consider the transfer failed. - - Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend who is sending the file. - The friend-specific file number (c_uint32) the data received is associated with. - The file control (TOX_FILE_CONTROL) command received. - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p) - self.file_recv_control_cb = c_callback(callback) - Tox.libtoxcore.tox_callback_file_recv_control(self._tox_pointer, - self.file_recv_control_cb, user_data) - - def file_seek(self, friend_number, file_number, position): - """ - Sends a file seek control command to a friend for a given file transfer. - - This function can only be called to resume a file transfer right before TOX_FILE_CONTROL_RESUME is sent. - - :param friend_number: The friend number of the friend the file is being received from. - :param file_number: The friend-specific identifier for the file transfer. - :param position: The position that the file should be seeked to. - :return: True on success. - """ - tox_err_file_seek = c_int() - result = Tox.libtoxcore.tox_file_control(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), - c_uint64(position), byref(tox_err_file_seek)) - tox_err_file_seek = tox_err_file_seek.value - if tox_err_file_seek == TOX_ERR_FILE_SEEK['OK']: - return bool(result) - elif tox_err_file_seek == TOX_ERR_FILE_SEEK['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number passed did not designate a valid friend.') - elif tox_err_file_seek == TOX_ERR_FILE_SEEK['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_file_seek == TOX_ERR_FILE_SEEK['NOT_FOUND']: - raise ArgumentError('No file transfer with the given file number was found for the given friend.') - elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SEEK_DENIED']: - raise IOError('File was not in a state where it could be seeked.') - elif tox_err_file_seek == TOX_ERR_FILE_SEEK['INVALID_POSITION']: - raise ArgumentError('Seek position was invalid') - elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SENDQ']: - raise RuntimeError('Packet queue is full.') - - def file_get_file_id(self, friend_number, file_number, file_id=None): - """ - Copy the file id associated to the file transfer to a byte array. - - :param friend_number: The friend number of the friend the file is being transferred to or received from. - :param file_number: The friend-specific identifier for the file transfer. - :param file_id: A pointer (c_char_p) to memory region of at least TOX_FILE_ID_LENGTH bytes. If this parameter is - None, this function has no effect. - :return: file id. - """ - if file_id is None: - file_id = create_string_buffer(TOX_FILE_ID_LENGTH) - tox_err_file_get = c_int() - Tox.libtoxcore.tox_file_get_file_id(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), file_id, - byref(tox_err_file_get)) - tox_err_file_get = tox_err_file_get.value - if tox_err_file_get == TOX_ERR_FILE_GET['OK']: - return bin_to_string(file_id, TOX_FILE_ID_LENGTH) - elif tox_err_file_get == TOX_ERR_FILE_GET['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_file_get == TOX_ERR_FILE_GET['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number passed did not designate a valid friend.') - elif tox_err_file_get == TOX_ERR_FILE_GET['NOT_FOUND']: - raise ArgumentError('No file transfer with the given file number was found for the given friend.') - - # ----------------------------------------------------------------------------------------------------------------- - # File transmission: sending - # ----------------------------------------------------------------------------------------------------------------- - - def file_send(self, friend_number, kind, file_size, file_id, filename): - """ - Send a file transmission request. - - Maximum filename length is TOX_MAX_FILENAME_LENGTH bytes. The filename should generally just be a file name, not - a path with directory names. - - If a non-UINT64_MAX file size is provided, it can be used by both sides to determine the sending progress. File - size can be set to UINT64_MAX for streaming data of unknown size. - - File transmission occurs in chunks, which are requested through the `file_chunk_request` event. - - When a friend goes offline, all file transfers associated with the friend are purged from core. - - If the file contents change during a transfer, the behaviour is unspecified in general. What will actually - happen depends on the mode in which the file was modified and how the client determines the file size. - - - If the file size was increased - - and sending mode was streaming (file_size = UINT64_MAX), the behaviour will be as expected. - - and sending mode was file (file_size != UINT64_MAX), the file_chunk_request callback will receive length = - 0 when Core thinks the file transfer has finished. If the client remembers the file size as it was when - sending the request, it will terminate the transfer normally. If the client re-reads the size, it will think - the friend cancelled the transfer. - - If the file size was decreased - - and sending mode was streaming, the behaviour is as expected. - - and sending mode was file, the callback will return 0 at the new (earlier) end-of-file, signalling to the - friend that the transfer was cancelled. - - If the file contents were modified - - at a position before the current read, the two files (local and remote) will differ after the transfer - terminates. - - at a position after the current read, the file transfer will succeed as expected. - - In either case, both sides will regard the transfer as complete and successful. - - :param friend_number: The friend number of the friend the file send request should be sent to. - :param kind: The meaning of the file to be sent. - :param file_size: Size in bytes of the file the client wants to send, UINT64_MAX if unknown or streaming. - :param file_id: A file identifier of length TOX_FILE_ID_LENGTH that can be used to uniquely identify file - transfers across core restarts. If NULL, a random one will be generated by core. It can then be obtained by - using tox_file_get_file_id(). - :param filename: Name of the file. Does not need to be the actual name. This name will be sent along with the - file send request. - :return: A file number used as an identifier in subsequent callbacks. This number is per friend. File numbers - are reused after a transfer terminates. On failure, this function returns UINT32_MAX. Any pattern in file - numbers should not be relied on. - """ - tox_err_file_send = c_int() - result = self.libtoxcore.tox_file_send(self._tox_pointer, c_uint32(friend_number), c_uint32(kind), - c_uint64(file_size), - string_to_bin(file_id), - c_char_p(filename), - c_size_t(len(filename)), byref(tox_err_file_send)) - tox_err_file_send = tox_err_file_send.value - if tox_err_file_send == TOX_ERR_FILE_SEND['OK']: - return result - elif tox_err_file_send == TOX_ERR_FILE_SEND['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_file_send == TOX_ERR_FILE_SEND['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend_number passed did not designate a valid friend.') - elif tox_err_file_send == TOX_ERR_FILE_SEND['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_file_send == TOX_ERR_FILE_SEND['NAME_TOO_LONG']: - raise ArgumentError('Filename length exceeded TOX_MAX_FILENAME_LENGTH bytes.') - elif tox_err_file_send == TOX_ERR_FILE_SEND['TOO_MANY']: - raise RuntimeError('Too many ongoing transfers. The maximum number of concurrent file transfers is 256 per' - 'friend per direction (sending and receiving).') - - def file_send_chunk(self, friend_number, file_number, position, data): - """ - Send a chunk of file data to a friend. - - This function is called in response to the `file_chunk_request` callback. The length parameter should be equal - to the one received though the callback. If it is zero, the transfer is assumed complete. For files with known - size, Core will know that the transfer is complete after the last byte has been received, so it is not necessary - (though not harmful) to send a zero-length chunk to terminate. For streams, core will know that the transfer is - finished if a chunk with length less than the length requested in the callback is sent. - - :param friend_number: The friend number of the receiving friend for this file. - :param file_number: The file transfer identifier returned by tox_file_send. - :param position: The file or stream position from which to continue reading. - :param data: Chunk of file data - :return: true on success. - """ - tox_err_file_send_chunk = c_int() - result = self.libtoxcore.tox_file_send_chunk(self._tox_pointer, c_uint32(friend_number), c_uint32(file_number), - c_uint64(position), c_char_p(data), c_size_t(len(data)), - byref(tox_err_file_send_chunk)) - tox_err_file_send_chunk = tox_err_file_send_chunk.value - if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['OK']: - return bool(result) - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NULL']: - raise ArgumentError('The length parameter was non-zero, but data was NULL.') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['FRIEND_NOT_FOUND']: - ArgumentError('The friend_number passed did not designate a valid friend.') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NOT_FOUND']: - raise ArgumentError('No file transfer with the given file number was found for the given friend.') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NOT_TRANSFERRING']: - raise ArgumentError('File transfer was found but isn\'t in a transferring state: (paused, done, broken, ' - 'etc...) (happens only when not called from the request chunk callback).') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['INVALID_LENGTH']: - raise ArgumentError('Attempted to send more or less data than requested. The requested data size is ' - 'adjusted according to maximum transmission unit and the expected end of the file. ' - 'Trying to send less or more than requested will return this error.') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['SENDQ']: - raise RuntimeError('Packet queue is full.') - elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['WRONG_POSITION']: - raise ArgumentError('Position parameter was wrong.') - - def callback_file_chunk_request(self, callback, user_data): - """ - Set the callback for the `file_chunk_request` event. Pass None to unset. - - This event is triggered when Core is ready to send more file data. - - :param callback: Python function. - If the length parameter is 0, the file transfer is finished, and the client's resources associated with the file - number should be released. After a call with zero length, the file number can be reused for future file - transfers. - - If the requested position is not equal to the client's idea of the current file or stream position, it will need - to seek. In case of read-once streams, the client should keep the last read chunk so that a seek back can be - supported. A seek-back only ever needs to read from the last requested chunk. This happens when a chunk was - requested, but the send failed. A seek-back request can occur an arbitrary number of times for any given chunk. - - In response to receiving this callback, the client should call the function `tox_file_send_chunk` with the - requested chunk. If the number of bytes sent through that function is zero, the file transfer is assumed - complete. A client must send the full length of data requested with this callback. - - Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the receiving friend for this file. - The file transfer identifier (c_uint32) returned by tox_file_send. - The file or stream position (c_uint64) from which to continue reading. - The number of bytes (c_size_t) requested for the current chunk. - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, c_size_t, c_void_p) - self.file_chunk_request_cb = c_callback(callback) - self.libtoxcore.tox_callback_file_chunk_request(self._tox_pointer, self.file_chunk_request_cb, user_data) - - # ----------------------------------------------------------------------------------------------------------------- - # File transmission: receiving - # ----------------------------------------------------------------------------------------------------------------- - - def callback_file_recv(self, callback, user_data): - """ - Set the callback for the `file_recv` event. Pass None to unset. - - This event is triggered when a file transfer request is received. - - :param callback: Python function. - The client should acquire resources to be associated with the file transfer. Incoming file transfers start in - the PAUSED state. After this callback returns, a transfer can be rejected by sending a TOX_FILE_CONTROL_CANCEL - control command before any other control commands. It can be accepted by sending TOX_FILE_CONTROL_RESUME. - - Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend who is sending the file transfer request. - The friend-specific file number (c_uint32) the data received is associated with. - The meaning of the file (c_uint32) to be sent. - Size in bytes (c_uint64) of the file the client wants to send, UINT64_MAX if unknown or streaming. - Name of the file (c_char_p). Does not need to be the actual name. This name will be sent along with the file - send request. - Size in bytes (c_size_t) of the filename. - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_uint64, c_char_p, c_size_t, c_void_p) - self.file_recv_cb = c_callback(callback) - self.libtoxcore.tox_callback_file_recv(self._tox_pointer, self.file_recv_cb, user_data) - - def callback_file_recv_chunk(self, callback, user_data): - """ - Set the callback for the `file_recv_chunk` event. Pass NULL to unset. - - This event is first triggered when a file transfer request is received, and subsequently when a chunk of file - data for an accepted request was received. - - :param callback: Python function. - When length is 0, the transfer is finished and the client should release the resources it acquired for the - transfer. After a call with length = 0, the file number can be reused for new file transfers. - - If position is equal to file_size (received in the file_receive callback) when the transfer finishes, the file - was received completely. Otherwise, if file_size was UINT64_MAX, streaming ended successfully when length is 0. - - Should take pointer (c_void_p) to Tox object, - The friend number (c_uint32) of the friend who is sending the file. - The friend-specific file number (c_uint32) the data received is associated with. - The file position (c_uint64) of the first byte in data. - A byte array (c_char_p) containing the received chunk. - The length (c_size_t) of the received chunk. - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, POINTER(c_uint8), c_size_t, c_void_p) - self.file_recv_chunk_cb = c_callback(callback) - self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb, user_data) - - # ----------------------------------------------------------------------------------------------------------------- - # Low-level custom packet sending and receiving - # ----------------------------------------------------------------------------------------------------------------- - - def friend_send_lossy_packet(self, friend_number, data): - """ - Send a custom lossy packet to a friend. - The first byte of data must be in the range 200-254. Maximum length of a - custom packet is TOX_MAX_CUSTOM_PACKET_SIZE. - - Lossy packets behave like UDP packets, meaning they might never reach the - other side or might arrive more than once (if someone is messing with the - connection) or might arrive in the wrong order. - - Unless latency is an issue, it is recommended that you use lossless custom packets instead. - - :param friend_number: The friend number of the friend this lossy packet - :param data: python string containing the packet data - :return: True on success. - """ - tox_err_friend_custom_packet = c_int() - result = self.libtoxcore.tox_friend_send_lossy_packet(self._tox_pointer, c_uint32(friend_number), - c_char_p(data), c_size_t(len(data)), - byref(tox_err_friend_custom_packet)) - tox_err_friend_custom_packet = tox_err_friend_custom_packet.value - if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['OK']: - return bool(result) - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend number did not designate a valid friend.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['INVALID']: - raise ArgumentError('The first byte of data was not in the specified range for the packet type.' - 'This range is 200-254 for lossy, and 160-191 for lossless packets.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['EMPTY']: - raise ArgumentError('Attempted to send an empty packet.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']: - raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']: - raise RuntimeError('Packet queue is full.') - - def friend_send_lossless_packet(self, friend_number, data): - """ - Send a custom lossless packet to a friend. - The first byte of data must be in the range 160-191. Maximum length of a - custom packet is TOX_MAX_CUSTOM_PACKET_SIZE. - - Lossless packet behaviour is comparable to TCP (reliability, arrive in order) - but with packets instead of a stream. - - :param friend_number: The friend number of the friend this lossless packet - :param data: python string containing the packet data - :return: True on success. - """ - tox_err_friend_custom_packet = c_int() - result = self.libtoxcore.tox_friend_send_lossless_packet(self._tox_pointer, c_uint32(friend_number), - c_char_p(data), c_size_t(len(data)), - byref(tox_err_friend_custom_packet)) - tox_err_friend_custom_packet = tox_err_friend_custom_packet.value - if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['OK']: - return bool(result) - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_FOUND']: - raise ArgumentError('The friend number did not designate a valid friend.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_CONNECTED']: - raise ArgumentError('This client is currently not connected to the friend.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['INVALID']: - raise ArgumentError('The first byte of data was not in the specified range for the packet type.' - 'This range is 200-254 for lossy, and 160-191 for lossless packets.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['EMPTY']: - raise ArgumentError('Attempted to send an empty packet.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']: - raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.') - elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']: - raise RuntimeError('Packet queue is full.') - - def callback_friend_lossy_packet(self, callback, user_data): - """ - Set the callback for the `friend_lossy_packet` event. Pass NULL to unset. - - :param callback: Python function. - Should take pointer (c_void_p) to Tox object, - friend_number (c_uint32) - The friend number of the friend who sent a lossy packet, - A byte array (c_uint8 array) containing the received packet data, - length (c_size_t) - The length of the packet data byte array, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_void_p) - self.friend_lossy_packet_cb = c_callback(callback) - self.libtoxcore.tox_callback_friend_lossy_packet(self._tox_pointer, self.friend_lossy_packet_cb, user_data) - - def callback_friend_lossless_packet(self, callback, user_data): - """ - Set the callback for the `friend_lossless_packet` event. Pass NULL to unset. - - :param callback: Python function. - Should take pointer (c_void_p) to Tox object, - friend_number (c_uint32) - The friend number of the friend who sent a lossless packet, - A byte array (c_uint8 array) containing the received packet data, - length (c_size_t) - The length of the packet data byte array, - pointer (c_void_p) to user_data - :param user_data: pointer (c_void_p) to user data - """ - c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_void_p) - self.friend_lossless_packet_cb = c_callback(callback) - self.libtoxcore.tox_callback_friend_lossless_packet(self._tox_pointer, self.friend_lossless_packet_cb, - user_data) - - # ----------------------------------------------------------------------------------------------------------------- - # Low-level network information - # ----------------------------------------------------------------------------------------------------------------- - - def self_get_dht_id(self, dht_id=None): - """ - Writes the temporary DHT public key of this instance to a byte array. - - This can be used in combination with an externally accessible IP address and the bound port (from - tox_self_get_udp_port) to run a temporary bootstrap node. - - Be aware that every time a new instance is created, the DHT public key changes, meaning this cannot be used to - run a permanent bootstrap node. - - :param dht_id: pointer (c_char_p) to a memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this parameter is - None, this function allocates memory for dht_id. - :return: dht_id - """ - if dht_id is None: - dht_id = create_string_buffer(TOX_PUBLIC_KEY_SIZE) - Tox.libtoxcore.tox_self_get_dht_id(self._tox_pointer, dht_id) - return bin_to_string(dht_id, TOX_PUBLIC_KEY_SIZE) - - def self_get_udp_port(self): - """ - Return the UDP port this Tox instance is bound to. - """ - tox_err_get_port = c_int() - result = Tox.libtoxcore.tox_self_get_udp_port(self._tox_pointer, byref(tox_err_get_port)) - tox_err_get_port = tox_err_get_port.value - if tox_err_get_port == TOX_ERR_GET_PORT['OK']: - return result - elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: - raise RuntimeError('The instance was not bound to any port.') - - def self_get_tcp_port(self): - """ - Return the TCP port this Tox instance is bound to. This is only relevant if the instance is acting as a TCP - relay. - """ - tox_err_get_port = c_int() - result = Tox.libtoxcore.tox_self_get_tcp_port(self._tox_pointer, byref(tox_err_get_port)) - tox_err_get_port = tox_err_get_port.value - if tox_err_get_port == TOX_ERR_GET_PORT['OK']: - return result - elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: - raise RuntimeError('The instance was not bound to any port.') diff --git a/toxygen/tox_dns.py b/toxygen/tox_dns.py deleted file mode 100644 index ec8582f..0000000 --- a/toxygen/tox_dns.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -import urllib.request -from util import log -import settings -try: - from PySide import QtNetwork, QtCore -except: - from PyQt4 import QtNetwork, QtCore - - -def tox_dns(email): - """ - TOX DNS 4 - :param email: data like 'groupbot@toxme.io' - :return: tox id on success else None - """ - site = email.split('@')[1] - data = {"action": 3, "name": "{}".format(email)} - urls = ('https://{}/api'.format(site), 'http://{}/api'.format(site)) - s = settings.Settings.get_instance() - if not s['proxy_type']: # no proxy - for url in urls: - try: - return send_request(url, data) - except Exception as ex: - log('TOX DNS ERROR: ' + str(ex)) - else: # proxy - netman = QtNetwork.QNetworkAccessManager() - proxy = QtNetwork.QNetworkProxy() - proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) - proxy.setHostName(s['proxy_host']) - proxy.setPort(s['proxy_port']) - netman.setProxy(proxy) - for url in urls: - try: - request = QtNetwork.QNetworkRequest(url) - request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json") - reply = netman.post(request, bytes(json.dumps(data), 'utf-8')) - - while not reply.isFinished(): - QtCore.QThread.msleep(1) - QtCore.QCoreApplication.processEvents() - data = bytes(reply.readAll().data()) - result = json.loads(str(data, 'utf-8')) - if not result['c']: - return result['tox_id'] - except Exception as ex: - log('TOX DNS ERROR: ' + str(ex)) - - return None # error - - -def send_request(url, data): - req = urllib.request.Request(url) - req.add_header('Content-Type', 'application/json') - response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8')) - res = json.loads(str(response.read(), 'utf-8')) - if not res['c']: - return res['tox_id'] - else: - raise LookupError() diff --git a/toxygen/toxav.py b/toxygen/toxav.py deleted file mode 100644 index a9f46b3..0000000 --- a/toxygen/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 libtox import LibToxAV -from toxav_enums import * - - -class ToxAV: - """ - The ToxAV instance type. Each ToxAV instance can be bound to only one Tox instance, and Tox instance can have only - one ToxAV instance. One must make sure to close ToxAV instance prior closing Tox instance otherwise undefined - behaviour occurs. Upon closing of ToxAV instance, all active calls will be forcibly terminated without notifying - peers. - """ - - libtoxav = LibToxAV() - - # ----------------------------------------------------------------------------------------------------------------- - # 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 - """ - toxav_err_new = c_int() - ToxAV.libtoxav.toxav_new.restype = POINTER(c_void_p) - self._toxav_pointer = ToxAV.libtoxav.toxav_new(tox_pointer, byref(toxav_err_new)) - toxav_err_new = toxav_err_new.value - if toxav_err_new == TOXAV_ERR_NEW['NULL']: - raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') - elif toxav_err_new == TOXAV_ERR_NEW['MALLOC']: - raise MemoryError('Memory allocation failure while trying to allocate structures required for the A/V ' - 'session.') - elif toxav_err_new == TOXAV_ERR_NEW['MULTIPLE']: - raise RuntimeError('Attempted to create a second session for the same Tox instance.') - - self.call_state_cb = None - self.audio_receive_frame_cb = None - self.video_receive_frame_cb = None - self.call_cb = None - - def __del__(self): - """ - Releases all resources associated with the A/V session. - - If any calls were ongoing, these will be forcibly terminated without notifying peers. After calling this - function, no other functions may be called and the av pointer becomes invalid. - """ - ToxAV.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 - """ - ToxAV.libtoxav.toxav_get_tox.restype = POINTER(c_void_p) - return ToxAV.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 ToxAV.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. - """ - ToxAV.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 = ToxAV.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) - ToxAV.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 = ToxAV.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) - ToxAV.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 = ToxAV.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 = ToxAV.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 = ToxAV.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) - ToxAV.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) - ToxAV.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, self.video_receive_frame_cb, user_data) diff --git a/toxygen/toxav_enums.py b/toxygen/toxav_enums.py deleted file mode 100644 index 3f3977a..0000000 --- a/toxygen/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/toxcore_enums_and_consts.py b/toxygen/toxcore_enums_and_consts.py deleted file mode 100644 index 4d52837..0000000 --- a/toxygen/toxcore_enums_and_consts.py +++ /dev/null @@ -1,209 +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_PUBLIC_KEY_SIZE = 32 - -TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6 - -TOX_MAX_FRIEND_REQUEST_LENGTH = 1016 - -TOX_MAX_MESSAGE_LENGTH = 1372 - -TOX_MAX_NAME_LENGTH = 128 - -TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 - -TOX_SECRET_KEY_SIZE = 32 - -TOX_FILE_ID_LENGTH = 32 - -TOX_HASH_LENGTH = 32 - -TOX_MAX_CUSTOM_PACKET_SIZE = 1373 diff --git a/toxygen/toxencryptsave.py b/toxygen/toxencryptsave.py deleted file mode 100644 index 14d3aee..0000000 --- a/toxygen/toxencryptsave.py +++ /dev/null @@ -1,114 +0,0 @@ -import libtox -import util -from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool - - -TOX_ERR_ENCRYPTION = { - # The function returned successfully. - 'OK': 0, - # Some input data, or maybe the output pointer, was null. - 'NULL': 1, - # The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The - # functions accepting keys do not produce this error. - 'KEY_DERIVATION_FAILED': 2, - # The encryption itself failed. - 'FAILED': 3 -} - -TOX_ERR_DECRYPTION = { - # The function returned successfully. - 'OK': 0, - # Some input data, or maybe the output pointer, was null. - 'NULL': 1, - # The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes - 'INVALID_LENGTH': 2, - # The input data is missing the magic number (i.e. wasn't created by this module, or is corrupted) - 'BAD_FORMAT': 3, - # The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The - # functions accepting keys do not produce this error. - 'KEY_DERIVATION_FAILED': 4, - # The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect. - 'FAILED': 5, -} - -TOX_PASS_ENCRYPTION_EXTRA_LENGTH = 80 - - -class ToxEncryptSave(util.Singleton): - - libtoxencryptsave = libtox.LibToxEncryptSave() - - def __init__(self): - super().__init__() - self._passphrase = None - - def set_password(self, passphrase): - self._passphrase = passphrase - - def has_password(self): - return bool(self._passphrase) - - def is_password(self, password): - return self._passphrase == password - - def is_data_encrypted(self, data): - func = self.libtoxencryptsave.tox_is_data_encrypted - func.restype = c_bool - result = func(c_char_p(bytes(data))) - return result - - def pass_encrypt(self, data): - """ - Encrypts the given data with the given passphrase. - - :return: output array - """ - out = create_string_buffer(len(data) + TOX_PASS_ENCRYPTION_EXTRA_LENGTH) - tox_err_encryption = c_int() - self.libtoxencryptsave.tox_pass_encrypt(c_char_p(data), - c_size_t(len(data)), - c_char_p(bytes(self._passphrase, 'utf-8')), - c_size_t(len(self._passphrase)), - out, - byref(tox_err_encryption)) - tox_err_encryption = tox_err_encryption.value - if tox_err_encryption == TOX_ERR_ENCRYPTION['OK']: - return out[:] - elif tox_err_encryption == TOX_ERR_ENCRYPTION['NULL']: - raise ArgumentError('Some input data, or maybe the output pointer, was null.') - elif tox_err_encryption == TOX_ERR_ENCRYPTION['KEY_DERIVATION_FAILED']: - raise RuntimeError('The crypto lib was unable to derive a key from the given passphrase, which is usually a' - ' lack of memory issue. The functions accepting keys do not produce this error.') - elif tox_err_encryption == TOX_ERR_ENCRYPTION['FAILED']: - raise RuntimeError('The encryption itself failed.') - - def pass_decrypt(self, data): - """ - Decrypts the given data with the given passphrase. - - :return: output array - """ - out = create_string_buffer(len(data) - TOX_PASS_ENCRYPTION_EXTRA_LENGTH) - tox_err_decryption = c_int() - self.libtoxencryptsave.tox_pass_decrypt(c_char_p(bytes(data)), - c_size_t(len(data)), - c_char_p(bytes(self._passphrase, 'utf-8')), - c_size_t(len(self._passphrase)), - out, - byref(tox_err_decryption)) - tox_err_decryption = tox_err_decryption.value - if tox_err_decryption == TOX_ERR_DECRYPTION['OK']: - return out[:] - elif tox_err_decryption == TOX_ERR_DECRYPTION['NULL']: - raise ArgumentError('Some input data, or maybe the output pointer, was null.') - elif tox_err_decryption == TOX_ERR_DECRYPTION['INVALID_LENGTH']: - raise ArgumentError('The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes') - elif tox_err_decryption == TOX_ERR_DECRYPTION['BAD_FORMAT']: - raise ArgumentError('The input data is missing the magic number (i.e. wasn\'t created by this module, or is' - ' corrupted)') - elif tox_err_decryption == TOX_ERR_DECRYPTION['KEY_DERIVATION_FAILED']: - raise RuntimeError('The crypto lib was unable to derive a key from the given passphrase, which is usually a' - ' lack of memory issue. The functions accepting keys do not produce this error.') - elif tox_err_decryption == TOX_ERR_DECRYPTION['FAILED']: - raise RuntimeError('The encrypted byte array could not be decrypted. Either the data was corrupt or the ' - 'password/key was incorrect.') diff --git a/toxygen/toxygen.pro b/toxygen/toxygen.pro index aeafe07..9643c8b 100644 --- a/toxygen/toxygen.pro +++ b/toxygen/toxygen.pro @@ -1,2 +1,2 @@ -SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py plugins/plugin_super_class.py callbacks.py widgets.py avwidgets.py mainscreen_widgets.py -TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts +SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py plugins/plugin_super_class.py callbacks.py widgets.py avwidgets.py mainscreen_widgets.py passwordscreen.py +TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts translations/uk_UA.ts diff --git a/toxygen/translations/en_GB.ts b/toxygen/translations/en_GB.ts index 045735f..7186005 100644 --- a/toxygen/translations/en_GB.ts +++ b/toxygen/translations/en_GB.ts @@ -3,22 +3,22 @@ AddContact - + Add contact Add contact - + TOX ID: TOX ID: - + Message: Message: - + TOX ID or public key of contact @@ -26,7 +26,7 @@ Callback - + File from @@ -34,32 +34,32 @@ Form - + Send request Send request - + IPv6 IPv6 - + UDP UDP - + Proxy Proxy - + IP: IP: - + Port: Port: @@ -69,113 +69,118 @@ Online contacts - + HTTP HTTP - + WARNING: using proxy with enabled UDP can produce IP leak + + + Download nodes list from tox.chat + + MainWindow - + Profile - + Settings - + About - + Add contact - + Privacy - + Interface - + Notifications - + Network - + About program - + User {} wants to add you to contact list. Message: {} - + Friend request - + Choose file Choose file - + Disallow auto accept - + Allow auto accept - + Set alias - + Clear history - + Remove friend - + Enter new alias for friend {} or leave empty to use friend's name: Enter new alias for friend {} or leave empty to use friend's name: - + Audio Audio @@ -185,24 +190,24 @@ can produce IP leak Find contact - + Friend added Friend added - + Toxygen is Tox client written on Python. Version: Toxygen is Tox client written on Python. Version: - + Friend added without sending friend request Friend added without sending friend request - + Choose folder Choose folder @@ -217,180 +222,300 @@ Version: Send file - + Send message Send message - + Start audio call with friend Start audio call with friend - + Plugins - + List of plugins - + Search - + All - + Online - + Notes - + Notes about user - + Copy link location - + Copy - + Select all - + Delete - + Paste - + Cut - + Undo - + Redo - + Save - + User {} is now known as {} - + Delete message - + Lock - + Cannot lock app - + Error. Profile password is not set. - + Name - + Status message - + Public key + + + Error + + + + + Profile with this name already exists + + + + + Choose folder with sticker pack + + + + + Choose folder with smiley pack + + + + + Import plugin + + + + + Choose folder with plugin + + + + + Restart Toxygen + + + + + Plugin will be loaded after restart + + + + + Quote selected text + + + + + Chat history + + + + + Export as text + + + + + Export as HTML + + + + + Updates + + + + + Online first + + + + + Online and by name + + + + + Online first and by name + + + + + Block friend + + + + + Not found + + + + + Text "{}" was not found + + + + + Reload plugins + + + + + Video + + + + + User {} invites you to group chat. Accept? + + + + + Group chat invite + + + + + {} users in chat + + + + + Enter new title for group {}: + + + + + Set title + + + + + Create group chat + + + + + Invite to group chat + + + + + Leave chat + + MenuWindow - - Send audio message to friend {} - - - - - Start recording - - - - - Stop recording - - - - + Send screenshot Send screenshot - + Send file Send file - - Send audio message - - - - - Send video message - - - - + Add smiley - + Send sticker @@ -398,25 +523,63 @@ Version: NetworkSettings - + Network settings Network settings - + Restart TOX core Restart Tox core + + PasswordScreen + + + Profile password + + + + + Password (at least 8 symbols) + + + + + Confirm password + + + + + Set password + + + + + Passwords do not match + + + + + There is no way to recover lost passwords + + + + + Password must be at least 8 symbols + + + PluginWindow - + List of commands for plugin {} - + No commands available @@ -424,42 +587,42 @@ Version: PluginsForm - + Plugins - + Open selected plugin - + No GUI found for this plugin - + No description available - + Disable plugin - + Enable plugin - + No plugins found - + Error @@ -467,67 +630,67 @@ Version: ProfileSettingsForm - + Export profile - + Profile settings - + Name: - + Status: - + TOX ID: - + Copy TOX ID - + New avatar - + Reset avatar - + New NoSpam New NoSpam - + Profile password - + Password (at least 8 symbols) - + Confirm password - + Set password @@ -537,17 +700,17 @@ Version: - + Leaving blank will reset current password - + There is no way to recover lost passwords - + Password must be at least 8 symbols @@ -557,108 +720,128 @@ Version: - + Online - + Away - + Busy - + Mark as not default profile - + Mark as default profile - + Copy public key + + + Use new path + + + + + Do you want to move your profile to this location? + + WelcomeScreen - + Don't show again - + Tip of the day - + Press Esc if you want hide app to tray. - + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> - + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. - - Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a> - - - - - New in Toxygen v0.2.2:<br>Users can lock application using profile password.<br>Compact contact list support<br>Bug fixes<br>Tox DNS improvements - - - - + Right click on screenshot button hides app to tray during screenshot. - + Use Settings -> Interface to customize interface. - + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. - + Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. + + + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu + + + + + Use right click on inline image to save it + + + + + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> + + + + + New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes + + audioSettingsForm - + Audio settings Audio settings - + Input device: Input device: - + Output device: Output device: @@ -666,32 +849,32 @@ Version: incoming_call - + Incoming video call Incoming video call - + Incoming audio call Incoming audio call - + Outgoing video call - + Outgoing audio call - + Call declined - + Call finished @@ -699,206 +882,274 @@ Version: interfaceForm - + Interface settings - + Theme: - + Language: - + Smileys - + Smiley pack: - + Mirror mode - + Messages font size: - + Restart app to apply settings - + Restart required - + Select unread messages notification color - + Compact contact list + + + Import smiley pack + + + + + Import sticker pack + + + + + Show avatars in chat + + + + + Close to tray + + + + + Select font + + login - + Log in - + Create - + Profile name: - + Load profile - + Use as default - + Load existing profile - + Create new profile - + toxygen - + Profile name - + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? + + + Do you want to set profile password? + + + + + Do you want to save profile in default folder? If no, profile will be saved in program folder + + + + + Profile saving error! Does Toxygen have permission to write to this directory? + + + + + Update for Toxygen was found. Download and install it? + + notificationsForm - + Notification settings - + Enable notifications - + Enable call's sound - + Enable sound notifications + + + Notify about all messages in groups + + + + + pass + + + Enter password + + + + + Password: + + + + + Incorrect password + + privacySettings - + Privacy settings - + Save chat history - + Allow file auto accept - + Send typing notifications - + Auto accept default path: - + Change - + Allow inlines - + Chat history - + History will be cleaned! Continue? - + Blocked users: Blocked users: - + Unblock Unblock - + Block user Block user - + Add to friend list Add to friend list - + Do you want to add this user to friend list? Do you want to add this user to friend list? @@ -908,12 +1159,12 @@ Version: Block by TOX ID: - + Block by public key: - + Save unsent messages only @@ -921,34 +1172,115 @@ Version: tray - + Open Toxygen - + Exit - + Set status - + Online - + Away - + Busy + + updateSettingsForm + + + Update settings + + + + + Select update mode: + + + + + Update Toxygen + + + + + Disabled + + + + + Manual + + + + + Auto + + + + + Error + + + + + Problems with internet connection + + + + + Updater not found + + + + + No updates found + + + + + Toxygen is up to date + + + + + videoSettingsForm + + + Video settings + + + + + Device: + + + + + Desktop + + + + + Select region + + + diff --git a/toxygen/translations/fr_FR.qm b/toxygen/translations/fr_FR.qm index e7ab531..33b0cbc 100644 Binary files a/toxygen/translations/fr_FR.qm and b/toxygen/translations/fr_FR.qm differ diff --git a/toxygen/translations/fr_FR.ts b/toxygen/translations/fr_FR.ts index bb91c4f..1931a26 100644 --- a/toxygen/translations/fr_FR.ts +++ b/toxygen/translations/fr_FR.ts @@ -2,64 +2,64 @@ AddContact - - - Add contact - Rajouter un contact - - - - TOX ID: - ID TOX : - + Add contact + Ajouter un contact + + + + TOX ID: + ID Tox : + + + Message: Message : - + TOX ID or public key of contact - + ID Tox ou clé publique de contact Callback - + File from - + Fichier de Form - + Send request Envoyer une demande - + IPv6 IPv6 - + UDP UDP - + Proxy Proxy - + IP: IP : - + Port: Port : @@ -69,75 +69,82 @@ Contacts connectés - + HTTP HTTP - + WARNING: using proxy with enabled UDP can produce IP leak + ATTENTION : +Utiliser un proxy avec UDP +peut entrainer une fuite d'IP + + + + Download nodes list from tox.chat MainWindow - + Profile - Profile + Profil - + Settings - Paramêtres + Paramètres - + About À Propos - + Add contact - Rajouter un contact + Ajouter un contact - + Privacy Confidentialité - + Interface Interface - + Notifications Notifications - + Network Réseau - + About program - À propos du programme + À propos de toxygen - + User {} wants to add you to contact list. Message: {} - L'Utilisateur {} veut vout rajouter à sa liste de contacts. Message : {} + L'Utilisateur {} veut vous ajouter à sa liste de contacts. Message : {} - + Friend request - Demande d'amis + Demande de contact @@ -145,27 +152,27 @@ can produce IP leak Toxygen est un client Tox écris en Python 2.7. Version : - + Choose file - Choisir un fichier + Sélectionner un fichier - + Disallow auto accept Désactiver l'auto-réception - + Allow auto accept Activer l'auto-réception - + Set alias Définir un alias - + Clear history Vider l'historique @@ -175,17 +182,17 @@ can produce IP leak Copier la clé publique - + Remove friend - Retirer un ami + Retirer ce contact - + Enter new alias for friend {} or leave empty to use friend's name: - Entrez un nouvel alias pour l'ami {} ou laissez vide pour garder son nom de base : + Entrez un nouvel alias pour le contact {} ou laissez vide pour garder son nom de base : - + Audio Audio @@ -195,26 +202,26 @@ can produce IP leak Trouver le contact - + Friend added - Ami rajouté + Contact ajouté - + Toxygen is Tox client written on Python. Version: Toxygen est un client Tox écrit en Python. Version : - + Friend added without sending friend request - Ami rajouté sans avoir envoyé de demande + Contact ajouté sans envoi de demande - + Choose folder - Choisir le dossier + Sélectionner un dossier @@ -227,448 +234,631 @@ Version : Envoyer le fichier - + Send message Envoyer le message - + Start audio call with friend - Lancer un appel audio avec un ami + Démarrer un appel audio avec un ami - + Plugins - + Plugins - + List of plugins - + Liste de plugins - + Search - + Chercher + + + + All + Tous + + + + Online + En ligne + + + + Notes + Notes + + + + Notes about user + Notes sur l'utilisateur + + + + Copy link location + Copier l'emplacement du lien + + + + Copy + Copier + + + + Select all + Tout sélectionner + + + + Delete + Supprimer + + + + Paste + Coller + + + + Cut + Couper + + + + Undo + Annuler + + + + Redo + Refaire + + + + Save + Sauvegarder + + + + User {} is now known as {} + L'utilisateur {} s'appelle désormais {} + + + + Delete message + Supprimer ce message - All - - - - - Online - - - - - Notes - - - - - Notes about user - - - - - Copy link location - - - - - Copy - - - - - Select all - - - - - Delete - - - - - Paste - - - - - Cut - - - - - Undo - - - - - Redo - - - - - Save - - - - - User {} is now known as {} - - - - - Delete message - - - - Lock - + Verrouiller - + Cannot lock app - - - - - Error. Profile password is not set. - + Impossible de verrouiller l'application + Error. Profile password is not set. + Erreur. Le profil n'a pas de mot de passe. + + + Name - + Nom - + Status message + Status + + + + Public key + Clé publique + + + + Error + Erreur + + + + Profile with this name already exists + Un profil ayant ce nom existe déjà + + + + Choose folder with sticker pack + Sélectionner le dossier contenant le pack de stickers + + + + Choose folder with smiley pack + Sélectionner le dossier contenant le pack de smileys + + + + Import plugin + Importer un plugin + + + + Choose folder with plugin + Sélectionner un dossier avec des plugins + + + + Restart Toxygen + Redémarrer Toxyger + + + + Plugin will be loaded after restart + Le plugin sera chargé après le redémarrage + + + + Quote selected text + Citer le texte sélectionné + + + + Chat history + Historique de la conversation + + + + Export as text + Exporter comme texte + + + + Export as HTML + Exporter comme HTML + + + + Updates + Mises à jour + + + + Online first + En ligne d'abord + + + + Online and by name + En ligne et par nom + + + + Online first and by name + En ligne d'abord puis par nom + + + + Block friend + Bloquer le contact + + + + Not found + Non trouvé + + + + Text "{}" was not found + Le texte "{}" n'a pas été trouvé + + + + Reload plugins + Recharger les plugins + + + + Video + Vidéo + + + + User {} invites you to group chat. Accept? - - Public key + + Group chat invite + + + + + {} users in chat + + + + + Enter new title for group {}: + + + + + Set title + + + + + Create group chat + + + + + Invite to group chat + + + + + Leave chat MenuWindow - - Send audio message to friend {} - - - - - Start recording - - - - - Stop recording - - - - + Send screenshot - Envoyer une capture d'écran + Envoyer une capture d'écran - + Send file - Envoyer le fichier + Envoyer un fichier - - Send audio message - - - - - Send video message - - - - + Add smiley - + Ajouter un smiley - + Send sticker - + Ajouter un sticker NetworkSettings - + Network settings Paramètres réseaux - + Restart TOX core - Relancer le noyau TOX + Relancer le noyau Tox + + + + PasswordScreen + + + Profile password + Mot de passe du profil + + + + Password (at least 8 symbols) + Mot de passe (8 symboles minimum) + + + + Confirm password + Confirmation + + + + Set password + Enregistrer le mot de passe + + + + Passwords do not match + Les mots de passes sont différents + + + + There is no way to recover lost passwords + Il est impossible de récuperer un mot de passe perdu + + + + Password must be at least 8 symbols + Un mot de passe doit faire 8 symboles minimum PluginWindow - + List of commands for plugin {} - + Liste de commandes du plugin {} - + No commands available - + Pas de commandes disponibles PluginsForm - + Plugins - + Plugins - + Open selected plugin - + Ouvrir le plugin sélectionné - + No GUI found for this plugin - + Pas d'interface pour ce plugin - + No description available - + Pas de description - + Disable plugin - + Désactiver le plugin - + Enable plugin - + Activer le plugin - + No plugins found - + Pas de plugin trouvé - + Error - + Erreur ProfileSettingsForm - + Export profile - Exporter le profile + Exporter le profil - + Profile settings - Paramêtres du profil + Paramètres du profil - + Name: Nom : - + Status: Status : - + TOX ID: ID TOX : - + Copy TOX ID - Copier l'ID TOX + Copier l'ID Tox - + New avatar Nouvel avatar - + Reset avatar Réinitialiser l'avatar - + New NoSpam Nouveau NoSpam - + Profile password - + Mot de passe du profil - + Password (at least 8 symbols) - + Mot de passe (8 symboles minimum) - + Confirm password - + Confirmation - + Set password - + Sauvegarder le mot de passe Passwords do not match - + Les mots de passe sont différents - + Leaving blank will reset current password - + Laisser vide réinitialisera le mot de passe actuel - + There is no way to recover lost passwords - + Il est impossible de récupérer un mot de passe perdu - + Password must be at least 8 symbols - + Le mot de passe doit faire 8 symboles minimum Choose avatar - + Choisir l'avatar - + Online - + En ligne - + Away - + Absent - + Busy - + Occupé - + Mark as not default profile - + Ne plus en faire le profil par défaut - + Mark as default profile - + En faire le profil par défaut - + Copy public key - Copier la clé publique + Copier la clé publique + + + + Use new path + Utiliser un nouveau chemin + + + + Do you want to move your profile to this location? + Déplacer le profil dans ce dossier ? WelcomeScreen - + Don't show again - + Ne plus montrer + + + + Tip of the day + Astuce du jou + + + + Press Esc if you want hide app to tray. + Appuyez sur échap pour réduire l'application. + + + + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> + Vous pouvez utiliser Tox avec Tor. Pour plus d'informations, voir <a href="https://wiki.tox.chat/users/tox_over_tor_tot">cet article</a> + + + + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. + Vous pouvez mettre un mot de passe dans Profil -> Paramètres -> Mot de passe pour que toxygen encrypte votre historique et vos paramètres. + + + + Right click on screenshot button hides app to tray during screenshot. + Faire un clic droit sur le bouton de capture d'écran réduit l'application avant de capturer l'écran. + + + + Use Settings -> Interface to customize interface. + Vous pouvez customizer votre interface dans Paramètres -> Interface. + + + + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. + Toxygen permet d'envoyer des messages et fichiers en différé. Envoyez des messages ou fichiers à un contact hors ligne et il le recevra plus tard. - Tip of the day - - - - - Press Esc if you want hide app to tray. - - - - - You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> - - - - - Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. - - - - - Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a> - - - - - New in Toxygen v0.2.2:<br>Users can lock application using profile password.<br>Compact contact list support<br>Bug fixes<br>Tox DNS improvements - - - - - Right click on screenshot button hides app to tray during screenshot. - - - - - Use Settings -> Interface to customize interface. - - - - - Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. - - - - Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. + Vous pouvez empecher le spam dans les demandes de contact avec Profil -> Paramètres -> Nouveau NoSpam. + + + + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu + Pour supprimer un seul message dans une conversation, faites un clic droit sur l'heure du message et sélectionnez "Supprimer ce message" dans le menu + + + + Use right click on inline image to save it + Pour sauvegarder une image intégrée, faites un clic droit dessus + + + + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> + Depuis la version 0.1.3 Toxygen supporte les plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">En savoir plus</a> + + + + New in Toxygen 0.3.0:<br>Video calls<br>Python3.6 support<br>Migration to PyQt5 + Nouveau dans Toxygen 0.3.0 : <br>Appels vidéo<br>Support de Python3.6<br>Migration vers PyQt5 + + + + New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes audioSettingsForm - + Audio settings Paramètres audio - + Input device: Péripherique d'entrée : - + Output device: Péripherique de sortie : @@ -676,133 +866,158 @@ Version : incoming_call - + Incoming video call Appel vidéo entrant - + Incoming audio call Appel audio entrant - + Outgoing video call - + Appel vidéo sortant - + Outgoing audio call - + Appel audio sortant - + Call declined - + Appel refusé - + Call finished - + Appel terminé interfaceForm - + Interface settings - Paramêtres de l'interface + Paramètres de l'interface - + Theme: Thème : - + Language: Langue : - + Smileys - + Smileys - + Smiley pack: - + Pack de smileys : - + Mirror mode - + Mode miroir - + Messages font size: - + Taille des messages : + + + + Restart app to apply settings + Redémarrer toxygen pour appliquer les paramètres + + + + Restart required + Redémarrage nécessaire + + + + Select unread messages notification color + Sélectionner la couleur des messages non-lus + + + + Compact contact list + Liste de contacts compacte + + + + Import smiley pack + Importer un pack de smileys + + + + Import sticker pack + Importer un pack de stickers + + + + Show avatars in chat + Montrer les avatars dans la conversation + + + + Close to tray + Réduire - Restart app to apply settings - - - - - Restart required - - - - - Select unread messages notification color - - - - - Compact contact list - + Select font + Sélectionner la police login - + Log in Se connecter - + Create Créer - + Profile name: Nom du profil : - + Load profile Charger le profil - + Use as default Utiliser par défaut - + Load existing profile Charger un profil existant - + Create new profile Créer un nouveau profil - + toxygen toxygen @@ -812,110 +1027,153 @@ Version : Il semble qu'une autre instance de Toxygen utilise ce profil ! Continuer ? - + Profile name - + Nom de profil - + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? - + Ce profil semble être utilisé par une autre instance de toxygen ou avoir été incorrectement fermé . Continuer ? + + + + Do you want to set profile password? + Souhaitez vous protéger le profil par un mot de passe ? + + + + Do you want to save profile in default folder? If no, profile will be saved in program folder + Souhaitez vous conserver le profil dans le dossier par défaut ? Si non, il sera conservé dans le dossier du programme + + + + Profile saving error! Does Toxygen have permission to write to this directory? + Un problème est survenu lors de la sauvegarde du profil ! Toxygen as t'il le droit d'écrire dans ce dossier ? + + + + Update for Toxygen was found. Download and install it? + Une mise à jour est disponible. La télécharger et l'installer ? notificationsForm - + Notification settings - Paramêtres de notification + Paramètres de notification - + Enable notifications Activer les notifications - + Enable call's sound Activer les sons d'appel - + Enable sound notifications Activer les sons de notifications + + + Notify about all messages in groups + + + + + pass + + + Enter password + Entrer le mot de passe + + + + Password: + Mot de passe : + + + + Incorrect password + Mot de passe incorrect + privacySettings - + Privacy settings - Paramêtres de confidentialité + Paramètres de confidentialité - + Save chat history - Sauvegarder l'historique de chat + Sauvegarder l'historique de conversation - + Allow file auto accept Autoriser les fichier automatiquement - + Send typing notifications - Notifier la frappe + Informer de la frappe - + Auto accept default path: - Chemin d'accès des fichiers acceptés automatiquement : + Chemin par défaut des fichiers acceptés automatiquement : - + Change Modifier - + Allow inlines - Activer l'auto-réception + Activer l'affichage integré - + Chat history - Historique de chat + Historique de conversation - + History will be cleaned! Continue? - L'Historique va être nettoyé ! Confirmer ? + L'Historique va être vidé ! Confirmer ? - + Blocked users: Utilisateurs bloqués : - + Unblock Débloquer - + Block user Bloquer l'utilisateur - + Add to friend list - Ajouter à la liste des amis + Ajouter à la liste de contacts - + Do you want to add this user to friend list? - Voulez vous rajouter cet utilisateur à votre liste d'amis ? + Voulez vous aajouter cet utilisateur à votre liste de contacts ? @@ -923,46 +1181,127 @@ Version : Bloquer l'ID TOX : - + Block by public key: - + Bloquer par clé publique : - + Save unsent messages only - + Sauvegarder les messages non envoyés uniquement tray - + Open Toxygen Ouvrir Toxygen - + Exit Quitter - + Set status - + Changer le status - + Online - + En ligne - + Away + Absent + + + + Busy + Occupé + + + + updateSettingsForm + + + Update settings + Paramètres de mise à jour + + + + Select update mode: + Sélectionner le mode de mise à jour : + + + + Update Toxygen + Mettre à jour toxygen + + + + Disabled + Désactivé + + + + Manual + Manuel + + + + Auto + Automatique + + + + Error + Erreur + + + + Problems with internet connection + Il y à des problèmes avec votre connexion internet + + + + Updater not found + Updater non trouvé + + + + No updates found + Pas de mises à jour trouvés + + + + Toxygen is up to date + Toxygen est à jour + + + + videoSettingsForm + + + Video settings + Paramètres vidéo + + + + Device: + Périphérique : + + + + Desktop - - Busy + + Select region diff --git a/toxygen/translations/ru_RU.qm b/toxygen/translations/ru_RU.qm index 76bc9b3..1231884 100644 Binary files a/toxygen/translations/ru_RU.qm and b/toxygen/translations/ru_RU.qm differ diff --git a/toxygen/translations/ru_RU.ts b/toxygen/translations/ru_RU.ts index 0f51862..8d6c63c 100644 --- a/toxygen/translations/ru_RU.ts +++ b/toxygen/translations/ru_RU.ts @@ -1,25 +1,24 @@ - - + AddContact - + Add contact Добавить контакт - + TOX ID: TOX ID: - + Message: Сообщение: - + TOX ID or public key of contact TOX ID или публичный ключ контакта @@ -27,7 +26,7 @@ Callback - + File from Файл от @@ -35,32 +34,32 @@ Form - + Send request Отправить запрос - + IPv6 IPv6 - + UDP UDP - + Proxy Прокси - + IP: IP: - + Port: Порт: @@ -70,12 +69,12 @@ Контакты в сети - + HTTP HTTP - + WARNING: using proxy with enabled UDP can produce IP leak @@ -83,88 +82,93 @@ can produce IP leak использование прокси с UDP может привести к утечке IP + + + Download nodes list from tox.chat + + MainWindow - + Profile Профиль - + Settings Настройки - + About О программе - + Add contact Добавить контакт - + Privacy Приватность - + Interface Интерфейс - + Notifications Уведомления - + Network Сеть - + About program О программе - + User {} wants to add you to contact list. Message: {} Пользователь {} хочет добавить Вас в список контактов. Сообщение: {} - + Friend request Запрос на добавление в друзья - + Choose file Выберите файл - + Disallow auto accept Запретить автоматическое получение файлов - + Allow auto accept Разрешить автоматическое получение файлов - + Set alias Изменить псевдоним - + Clear history Очистить историю @@ -174,17 +178,17 @@ can produce IP leak Копировать публичный ключ - + Remove friend Удалить друга - + Enter new alias for friend {} or leave empty to use friend's name: Введите новый псевдоним для друга {} или оставьте пустым для использования его имени: - + Audio Аудио @@ -194,23 +198,23 @@ can produce IP leak Найти контакт - + Friend added Друг добавлен - + Toxygen is Tox client written on Python. Version: Toxygen - клиент для мессенджера Tox, написанный на Python. Версия: - + Friend added without sending friend request Друг добавлен без отправки запроса на добавление в друзья - + Choose folder Выбрать папку @@ -225,180 +229,325 @@ Version: Отправить файл - + Send message Отправить сообщение - + Start audio call with friend Начать аудиозвонок с другом - + Plugins Плагины - + List of plugins Список плагинов - + Search Поиск - + All Все - + Online Онлайн - + Notes Заметки - + Notes about user Заметки о пользователе - + Copy link location Копировать адрес ссылки - + Copy Копировать - + Select all Выделить всё - + Delete Удалить - + Paste Вставить - + Cut Вырезать - + Undo Отменить - + Redo Повторить - + Save Сохранить - + User {} is now known as {} Пользователь {} сейчас известен как {} - + Delete message Удалить сообщение - + Lock Заблокировать - + Cannot lock app Невозможно заблокировать приложение - + Error. Profile password is not set. Ошибка. Пароль профиля не установлен. - + Name Имя - + Status message Статус - + Public key Публичный ключ + + + Error + Ошибка + + + + Profile with this name already exists + Профиль с данным именем уже существует + + + + Choose folder with sticker pack + Выберите папку в паком стикеров + + + + Choose folder with smiley pack + Выберите папку с паком смайлов + + + + Import plugin + Импортировать плагин + + + + Choose folder with plugin + Выберите папку с плагином + + + + Restart Toxygen + Перезапустите Toxygen + + + + Plugin will be loaded after restart + Плагин будет загружен после перезапуска + + + + Quote selected text + Цитировать выбранный текст + + + + Chat history + История чата + + + + Export as text + Экспортировать как текст + + + + Export as HTML + Экспортировать как HTML + + + + Updates + Обновления + + + + Online first + Сначала онлайн + + + + Online and by name + Онлайн и по имени + + + + Online first and by name + Сначала онлайн и по имени + + + + Block friend + Заблокировать друга + + + + Not found + Не найдено + + + + Text "{}" was not found + Текст "{}" не был найден + + + + Reload plugins + Перезагрузить плагины + + + + Video + Видео + + + + User {} invites you to group chat. Accept? + Пользователь {} приглашает Вас в групповой чат. Принять приглашение? + + + + Group chat invite + Приглашение в групповой чат + + + + {} users in chat + {} пользователей в чате + + + + Enter new title for group {}: + Введите название для группы {}: + + + + Set title + Изменить название + + + + Create group chat + Создать групповой чат + + + + Invite to group chat + Пригласить в групповой чат + + + + Leave chat + Покинуть чат + MenuWindow Send audio message to friend {} - Отправить аудиосообщение другу + Отправить аудиосообщение другу Start recording - Начать запись + Начать запись Stop recording - Остановить запись + Остановить запись - + Send screenshot Отправить снимок экрана - + Send file Отправить файл - + Send audio message - Отправить аудиосообщение + Отправить аудиосообщение - + Send video message - Отправить видеосообщение + Отправить видеосообщение - + Add smiley Добавить смайлик - + Send sticker Отправить стикер @@ -406,25 +555,63 @@ Version: NetworkSettings - + Network settings Настройки сети - + Restart TOX core Перезапустить ядро TOX + + PasswordScreen + + + Profile password + Пароль профиля + + + + Password (at least 8 symbols) + Пароль (минимум 8 символов) + + + + Confirm password + Подтверждение пароля + + + + Set password + Изменить пароль + + + + Passwords do not match + Пароли не совпадают + + + + There is no way to recover lost passwords + Восстановление забытых паролей не поддерживается + + + + Password must be at least 8 symbols + Пароль должен быть длиной не менее 8 символов + + PluginWindow - + List of commands for plugin {} Список команд для плагина {} - + No commands available Команды не найдены @@ -432,42 +619,42 @@ Version: PluginsForm - + Plugins Плагины - + Open selected plugin Открыть выбранный плагин - + No GUI found for this plugin GUI для данного плагина не найден - + No description available Описание недоступно - + Disable plugin Отключить плагин - + Enable plugin Включить плагин - + No plugins found Плагины не найдены - + Error Ошибка @@ -475,32 +662,32 @@ Version: ProfileSettingsForm - + Export profile Экспорт профиля - + Profile settings Настройки профиля - + Name: Имя: - + Status: Статус: - + TOX ID: TOX ID: - + Copy TOX ID Копировать TOX ID @@ -510,37 +697,37 @@ Version: Язык: - + New avatar Новый аватар - + Reset avatar Сбросить аватар - + New NoSpam Новый NoSpam - + Profile password Пароль профиля - + Password (at least 8 symbols) Пароль (минимум 8 символов) - + Confirm password Подтверждение пароля - + Set password Изменить пароль @@ -550,17 +737,17 @@ Version: Пароли не совпадают - + Leaving blank will reset current password Пустое поле сбросит текущий пароль - + There is no way to recover lost passwords Восстановление забытых паролей не поддерживается - + Password must be at least 8 symbols Пароль должен быть длиной не менее 8 символов @@ -570,50 +757,60 @@ Version: Выбрать аватар - + Online Онлайн - + Away Нет на месте - + Busy Занят - + Mark as not default profile Отключить автозагрузку профиля - + Mark as default profile Сделать профилем по умолчанию - + Copy public key Копировать публичный ключ + + + Use new path + Использовать новый путь + + + + Do you want to move your profile to this location? + Вы хотите переместить ваш профиль в эту папку? + WelcomeScreen - + Don't show again Не показывать снова - + Tip of the day Подсказка дня - + Press Esc if you want hide app to tray. Нажатие Esc сворачивает приложение в трей. @@ -623,7 +820,7 @@ Version: Правый клик на кнопке скриншота сворачивает приложение в трей на время скриншота - + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> Вы можете использовать Tox через Tor. Дополнительная информация <a href="https://wiki.tox.chat/users/tox_over_tor_tot">тут</a> @@ -633,19 +830,19 @@ Version: Используйте Настройки -> Интерфейс для настройки интерфейса - + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. Установите пароль профиля: Профиль -> Настройки. Пароль позволяет шифровать историю переписки и настройки. - + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a> - С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> + С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> New in Toxygen v0.2.2:<br>Users can lock application using profile password.<br>Compact contact list support<br>Bug fixes<br>Tox DNS improvements - С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> + С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> @@ -658,40 +855,80 @@ Version: Установите новый NoSpam, чтобы избежать спам запросов в друзья: Профиль->Настройки->Новый NoSpam - + Right click on screenshot button hides app to tray during screenshot. Правый клик на кнопке скриншота сворачивает приложение в трей на время скриншота. - + Use Settings -> Interface to customize interface. Используйте Настройки -> Интерфейс для настройки интерфейса. - + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. Toxygen поддерживает псевдооффлайн сообщения и файл трансферы. - + Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. Установите новый NoSpam, чтобы избежать спам запросов в друзья: Профиль->Настройки->Новый NoSpam. + + + New in Toxygen v0.2.3:<br>TCS compliance<br>Plugins, smileys and stickers import<br>Bug fixes + Новое в Toxygen 0.2.3:<br>Соответствие TCS<br>Импорт плагинов, смайлов и стикеров<br>Исправления ошибок + + + + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu + Чтобы удалить отдельное сообщение в чате сделайте правый клик на спиннер или время сообщения и выберите "Удалить" в меню + + + + Use right click on inline image to save it + Правый клик на инлайн изображении позволит сохранить его + + + + New in Toxygen v0.2.4:<br>File transfers update<br>Autoreconnection<br>Improvements<br>Bug fixes + Новое в Toxygen v0.2.4:<br>Передача файлов обновлена<br>Автопереподключение<br>Улучшения<br>Исправления ошибок + + + + New in Toxygen v0.2.6:<br>Updater<br>Better contact sorting<br>Plugins improvements + Новое в Toxygen v0.2.6:<br>Поддержка обновлений<br>Улучшенная сортировка контактов<br>Улучшения в работе плагинов + + + + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> + С версии 0.1.3 Toxygen поддерживает плагины. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Узнать больше.</a> + + + + New in Toxygen 0.3.0:<br>Video calls<br>Python3.6 support<br>Migration to PyQt5 + Новое в Toxygen 0.3.0:<br>Видеозвонки<br>Поддержка Python3.6<br>Миграция на PyQt5 + + + + New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes + + audioSettingsForm - + Audio settings Настройки аудио - + Input device: Устройство ввода: - + Output device: Устройство вывода: @@ -699,32 +936,32 @@ Version: incoming_call - + Incoming video call Входящий видеозвонок - + Incoming audio call Входящий аудиозвонок - + Outgoing video call Исходящий видеозвонок - + Outgoing audio call Исходящий аудиозвонок - + Call declined Звонок отменен - + Call finished Звонок завершен @@ -732,100 +969,125 @@ Version: interfaceForm - + Interface settings Настройки интерфейса - + Theme: Тема: - + Language: Язык: - + Smileys Смайлики - + Smiley pack: Набор смайликов: - + Mirror mode Зеркальный режим - + Messages font size: Размер шрифта сообщений: - + Restart app to apply settings Для применения настроек необходимо перезапустить приложение - + Restart required Требуется перезапуск - + Select unread messages notification color Цвет уведомления о сообщении - + Compact contact list Компактный список контактов + + + Import smiley pack + Импортировать смайлы + + + + Import sticker pack + Импортировать стикеры + + + + Show avatars in chat + Показывать аватары в чате + + + + Close to tray + Сворачивать в трей + + + + Select font + Выбрать шрифт + login - + Log in Вход - + Create Создать - + Profile name: Имя профиля: - + Load profile Загрузить профиль - + Use as default По умолчанию - + Load existing profile Загрузить профиль - + Create new profile Создать новый профиль - + toxygen toxygen @@ -835,109 +1097,152 @@ Version: Похоже, что этот профиль используется другим экземпляром Toxygen! Продолжить? - + Profile name Имя профиля - + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? Этот профиль используется другим экземпляром Toxygen или не был правильно закрыт. Продолжить? + + + Do you want to set profile password? + Хотите ли вы установить пароль профиля? + + + + Do you want to save profile in default folder? If no, profile will be saved in program folder + Вы хотите сохранить профиль в папку по умолчанию? Если нет, профиль будет сохранен в папке с программой + + + + Profile saving error! Does Toxygen have permission to write to this directory? + Ошибка сохранения профиля! Toxygen имеет разрешение на запись в данную папку? + + + + Update for Toxygen was found. Download and install it? + Обновление для Toxygen было найдено. Загрузить и установить его? + notificationsForm - + Notification settings Настройки уведомлений - + Enable notifications Включить уведомления - + Enable call's sound Включить звук звонка - + Enable sound notifications Включить звуковые уведомления + + + Notify about all messages in groups + Уведомлять обо всех сообщениях в группах + + + + pass + + + Enter password + Введите пароль + + + + Password: + Пароль: + + + + Incorrect password + Неверный пароль + privacySettings - + Privacy settings Настройки приватности - + Save chat history Сохранять историю переписки - + Allow file auto accept Разрешить автополучение файлов - + Send typing notifications Посылать уведомления о наборе текста - + Auto accept default path: Путь автоприема файлов: - + Change Изменить - + Allow inlines Разрешать инлайны - + Chat history История чата - + History will be cleaned! Continue? История переписки будет очищена! Продолжить? - + Blocked users: Заблокированные пользователи: - + Unblock Разблокировать - + Block user Заблокировать пользователя - + Add to friend list Добавить в список друзей - + Do you want to add this user to friend list? Добавить этого пользователя в список друзей? @@ -947,12 +1252,12 @@ Version: Блокировать по TOX ID: - + Block by public key: Блокировать по публичному ключу: - + Save unsent messages only Сохранять только неотправленные сообщения @@ -960,34 +1265,115 @@ Version: tray - + Open Toxygen Открыть Toxygen - + Exit Выход - + Set status Изменить статус - + Online Онлайн - + Away Нет на месте - + Busy Занят + + updateSettingsForm + + + Update settings + Обновить настройки + + + + Select update mode: + Выбрать режим обновлений: + + + + Update Toxygen + Обновить Toxygen + + + + Disabled + Отключены + + + + Manual + Вручную + + + + Auto + Автоматически + + + + Error + Ошибка + + + + Problems with internet connection + Проблемы с соединением + + + + Updater not found + Апдейтер не был найден + + + + No updates found + Обновления не найдены + + + + Toxygen is up to date + Toxygen уже обновлен + + + + videoSettingsForm + + + Video settings + Настройки видео + + + + Device: + Устройство: + + + + Desktop + Рабочий стол + + + + Select region + Выберите область + + diff --git a/toxygen/translations/uk_UA.qm b/toxygen/translations/uk_UA.qm new file mode 100644 index 0000000..a4082ef Binary files /dev/null and b/toxygen/translations/uk_UA.qm differ diff --git a/toxygen/translations/uk_UA.ts b/toxygen/translations/uk_UA.ts new file mode 100644 index 0000000..c36ecf0 --- /dev/null +++ b/toxygen/translations/uk_UA.ts @@ -0,0 +1,1297 @@ + + + + AddContact + + + TOX ID: + TOX ID: + + + + Add contact + Додати контакт + + + + Message: + Повідомлення: + + + + TOX ID or public key of contact + + + + + Callback + + + File from + + + + + Form + + + IP: + IP: + + + + UDP + UDP + + + + HTTP + HTTP + + + + IPv6 + IPv6 + + + + Port: + Порт: + + + + Proxy + Проксі + + + + Online contacts + Контактів онлайн + + + + Send request + Відправити запит + + + + WARNING: +using proxy with enabled UDP +can produce IP leak + + + + + Download nodes list from tox.chat + + + + + MainWindow + + + About program + Про проґраму + + + + Friend request + Запит дружби + + + + About + Про + + + + Audio + Звук + + + + Friend added + Друга додано + + + + Send file + Надіслати файл + + + + User {} wants to add you to contact list. Message: +{} + Користувач {} хоче додати вас до списку контактів. Повідомлення +{} + + + + Network + Мережа + + + + Clear history + Очистити журнал + + + + Copy public key + Копіювати публічний ключ + + + + Send message + Надіслати повідомлення + + + + Set alias + Встановити скорочення + + + + Privacy + Приватність + + + + Profile + Профіль + + + + Toxygen is Tox client written on Python. +Version: + Toxygen — це клієнт Tox написаний на Python. +Версія: + + + + Choose file + Обрати файл + + + + Enter new alias for friend {} or leave empty to use friend's name: + Введіть нове скорочення для друга {} або залишіть порожнім, щоб використовувати його псевдо: + + + + Add contact + Додати контакт + + + + Friend added without sending friend request + Друга додано без надсилання запиту дружби + + + + Interface + Зовнішній вигляд + + + + Settings + Налаштування + + + + Notifications + Сповіщення + + + + Remove friend + Вилучити друга + + + + Find contact + Знайти контакт + + + + Choose folder + Обрати теку + + + + Allow auto accept + Дозволити автоприймання + + + + Disallow auto accept + Заборонити автоприймання + + + + Start audio call with friend + Почати звуковий дзвінок + + + + Send screenshot + Надіслати знімок екрану + + + + Error + + + + + Profile with this name already exists + + + + + User {} is now known as {} + + + + + Choose folder with sticker pack + + + + + Choose folder with smiley pack + + + + + Quote selected text + + + + + Plugins + + + + + Delete message + + + + + Lock + + + + + List of plugins + + + + + Video + + + + + Updates + + + + + Search + + + + + All + + + + + Online + + + + + Online first + + + + + Name + + + + + Online and by name + + + + + Online first and by name + + + + + Import plugin + + + + + Reload plugins + + + + + Choose folder with plugin + + + + + Restart Toxygen + + + + + Plugin will be loaded after restart + + + + + Cannot lock app + + + + + Error. Profile password is not set. + + + + + Chat history + Журнал бесіди + + + + Export as text + + + + + Export as HTML + + + + + Copy + + + + + Status message + + + + + Public key + + + + + Block friend + + + + + Notes + + + + + Notes about user + + + + + Copy link location + + + + + Select all + + + + + Delete + + + + + Paste + + + + + Cut + + + + + Undo + + + + + Redo + + + + + Save + + + + + Text "{}" was not found + + + + + Not found + + + + + User {} invites you to group chat. Accept? + + + + + Group chat invite + + + + + {} users in chat + + + + + Enter new title for group {}: + + + + + Set title + + + + + Create group chat + + + + + Invite to group chat + + + + + Leave chat + + + + + MenuWindow + + + Send screenshot + Надіслати знімок екрану + + + + Send file + Надіслати файл + + + + Add smiley + + + + + Send sticker + + + + + NetworkSettings + + + Network settings + Налаштування мережі + + + + Restart TOX core + Перезапустити ядро Tox + + + + PasswordScreen + + + Profile password + + + + + Password (at least 8 symbols) + + + + + Confirm password + + + + + Set password + + + + + Passwords do not match + + + + + There is no way to recover lost passwords + + + + + Password must be at least 8 symbols + + + + + PluginWindow + + + List of commands for plugin {} + + + + + No commands available + + + + + PluginsForm + + + Plugins + + + + + Open selected plugin + + + + + No GUI found for this plugin + + + + + Error + + + + + No description available + + + + + Disable plugin + + + + + Enable plugin + + + + + No plugins found + + + + + ProfileSettingsForm + + + Name: + Псевдо: + + + + Profile settings + Налаштування профілю + + + + Reset avatar + Скинути аватар + + + + New NoSpam + Новий NoSpam + + + + Copy TOX ID + Копіювати TOX ID + + + + New avatar + Новий аватар + + + + Export profile + Експортувати профіль + + + + TOX ID: + TOX ID: + + + + Status: + Статус: + + + + Profile password + + + + + Password (at least 8 symbols) + + + + + Confirm password + + + + + Set password + + + + + Passwords do not match + + + + + Leaving blank will reset current password + + + + + There is no way to recover lost passwords + + + + + Online + + + + + Away + + + + + Busy + + + + + Copy public key + Копіювати публічний ключ + + + + Mark as not default profile + + + + + Mark as default profile + + + + + Password must be at least 8 symbols + + + + + Choose avatar + + + + + Use new path + + + + + Do you want to move your profile to this location? + + + + + WelcomeScreen + + + Don't show again + + + + + Tip of the day + + + + + Press Esc if you want hide app to tray. + + + + + Right click on screenshot button hides app to tray during screenshot. + + + + + You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a> + + + + + Use Settings -> Interface to customize interface. + + + + + Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings. + + + + + Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a> + + + + + Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later. + + + + + Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu + + + + + Use right click on inline image to save it + + + + + Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam. + + + + + New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes + + + + + audioSettingsForm + + + Output device: + Пристрій виводу: + + + + Audio settings + Налаштування звуку + + + + Input device: + Пристрій вводу: + + + + incoming_call + + + Incoming video call + Вхідний відеодзвінок + + + + Incoming audio call + Вхідний аудіодзвінок + + + + Outgoing video call + + + + + Outgoing audio call + + + + + Call declined + + + + + Call finished + + + + + interfaceForm + + + Language: + Мова: + + + + Theme: + Тема: + + + + Interface settings + Налаштування зовнішнього вигляду + + + + Show avatars in chat + + + + + Smileys + + + + + Smiley pack: + + + + + Mirror mode + + + + + Messages font size: + + + + + Select unread messages notification color + + + + + Compact contact list + + + + + Import smiley pack + + + + + Import sticker pack + + + + + Close to tray + + + + + Select font + + + + + Restart app to apply settings + + + + + Restart required + + + + + login + + + Profile name: + Псевдо профілю: + + + + Load profile + Завантажити профіль + + + + Use as default + За замовчуванням + + + + Create new profile + Створити новий профіль + + + + Create + Створити + + + + Log in + Увійти + + + + Load existing profile + Завантажити існуючий + + + + toxygen + toxygen + + + + Looks like other instance of Toxygen uses this profile! Continue? + Схоже, що інша копія Toxygenʼу використовує цей профіль! Продовжити? + + + + Do you want to set profile password? + + + + + Do you want to save profile in default folder? If no, profile will be saved in program folder + + + + + Profile saving error! Does Toxygen have permission to write to this directory? + + + + + Other instance of Toxygen uses this profile or profile was not properly closed. Continue? + + + + + Update for Toxygen was found. Download and install it? + + + + + Profile name + + + + + notificationsForm + + + Enable sound notifications + Увімкнути звукові сповіщення + + + + Enable notifications + Увімкнути сповіщення + + + + Notification settings + Налаштування сповіщень + + + + Enable call's sound + Увімкнути звук дзвінка + + + + Notify about all messages in groups + + + + + pass + + + Enter password + + + + + Password: + + + + + Incorrect password + + + + + privacySettings + + + Privacy settings + Налаштування приватності + + + + Add to friend list + Додати до списку друзів + + + + Block by TOX ID: + Блокувати по TOX ID: + + + + Blocked users: + Блоковані користувачі: + + + + Change + Змінити + + + + Send typing notifications + Надсилати сповіщення про те, що я друкую + + + + Allow file auto accept + Дозволити автоприймання файлів + + + + Allow inlines + Дозволити інлайни + + + + Save chat history + Зберігати журнал бесіди + + + + Block user + Блокувати користувача + + + + Chat history + Журнал бесіди + + + + Unblock + Розблокувати + + + + History will be cleaned! Continue? + Журнал буде очищено! Продовжити? + + + + Auto accept default path: + Шлях за замовчуванням для автоприймання: + + + + Do you want to add this user to friend list? + Ви хочете додати цього користувача у список друзів? + + + + Block by public key: + + + + + Save unsent messages only + + + + + tray + + + Exit + Вихід + + + + Open Toxygen + Відкрити Toxygen + + + + Set status + + + + + Online + + + + + Away + + + + + Busy + + + + + updateSettingsForm + + + Update settings + + + + + Select update mode: + + + + + Update Toxygen + + + + + Disabled + + + + + Manual + + + + + Auto + + + + + Error + + + + + Problems with internet connection + + + + + Updater not found + + + + + No updates found + + + + + Toxygen is up to date + + + + + videoSettingsForm + + + Video settings + + + + + Device: + + + + + Desktop + + + + + Select region + + + + diff --git a/toxygen/ui/__init__.py b/toxygen/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/ui/av_widgets.py b/toxygen/ui/av_widgets.py new file mode 100644 index 0000000..e750231 --- /dev/null +++ b/toxygen/ui/av_widgets.py @@ -0,0 +1,182 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import logging +import threading +import wave + +from qtpy import QtCore, QtGui, QtWidgets + +from ui import widgets +import utils.util as util +import toxygen_wrapper.tests.support_testing as ts +with ts.ignoreStderr(): + import pyaudio + +global LOG +LOG = logging.getLogger('app.'+__name__) + +class IncomingCallWidget(widgets.CenteredWidget): + + def __init__(self, settings, calls_manager, friend_number, text, name): + super().__init__() + self._settings = settings + self._calls_manager = calls_manager + self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint) # | QtCore.Qt.WindowStaysOnTopHint + self.resize(QtCore.QSize(500, 270)) + self.avatar_label = QtWidgets.QLabel(self) + self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) + self.avatar_label.setScaledContents(False) + self.name = widgets.DataLabel(self) + self.name.setGeometry(QtCore.QRect(90, 20, 300, 25)) + self._friend_number = friend_number + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(16) + font.setBold(True) + self.name.setFont(font) + self.call_type = widgets.DataLabel(self) + self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25)) + self.call_type.setFont(font) + self.accept_audio = QtWidgets.QPushButton(self) + self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150)) + self.accept_video = QtWidgets.QPushButton(self) + self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150)) + self.decline = QtWidgets.QPushButton(self) + self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150)) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'accept_audio.png')) + icon = QtGui.QIcon(pixmap) + self.accept_audio.setIcon(icon) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'accept_video.png')) + icon = QtGui.QIcon(pixmap) + self.accept_video.setIcon(icon) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'decline_call.png')) + icon = QtGui.QIcon(pixmap) + self.decline.setIcon(icon) + self.accept_audio.setIconSize(QtCore.QSize(150, 150)) + self.accept_video.setIconSize(QtCore.QSize(140, 140)) + self.decline.setIconSize(QtCore.QSize(140, 140)) + #self.accept_audio.setStyleSheet("QPushButton { border: none }") + #self.accept_video.setStyleSheet("QPushButton { border: none }") + #self.decline.setStyleSheet("QPushButton { border: none }") + self.setWindowTitle(text) + self.name.setText(name) + self.call_type.setText(text) + self._processing = False + self.accept_audio.clicked.connect(self.accept_call_with_audio) + self.accept_video.clicked.connect(self.accept_call_with_video) + self.decline.clicked.connect(self.decline_call) + + output_device_index = self._settings._oArgs.audio['output'] + + if self._settings['calls_sound']: + class SoundPlay(QtCore.QThread): + + def __init__(self): + QtCore.QThread.__init__(self) + self.a = None + + def run(self): + class AudioFile: + chunk = 1024 + + def __init__(self, fl): + self.stop = False + self.fl = fl + self.wf = wave.open(self.fl, 'rb') + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wf.getsampwidth()), + channels=self.wf.getnchannels(), + rate=self.wf.getframerate(), + # why no device? + output_device_index=output_device_index, + output=True) + + def play(self): + while not self.stop: + data = self.wf.readframes(self.chunk) + # dunno + if not data: break + while data and not self.stop: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + self.wf = wave.open(self.fl, 'rb') + + def close(self): + try: + self.stream.close() + self.p.terminate() + except Exception as e: + # malloc_consolidate(): unaligned fastbin chunk detected + LOG.warn("SoundPlay close exception {e}") + + self.a = AudioFile(util.join_path(util.get_sounds_directory(), 'call.wav')) + self.a.play() + self.a.close() + + self.thread = SoundPlay() + self.thread.start() + else: + self.thread = None + + def stop(self): + LOG.debug(f"stop from friend_number={self._friend_number}") + if self._processing: + self.close() + if self.thread is not None: + self.thread.a.stop = True + i = 0 + while i < ts.iTHREAD_JOINS: + self.thread.wait(ts.iTHREAD_TIMEOUT) + if not self.thread.isRunning(): break + i = i + 1 + else: + LOG.warn(f"stop {self.thread.a} BLOCKED") + self.thread.a.stream.close() + self.thread.a.p.terminate() + self.thread.a.close() + # dunno -failsafe + self.thread.terminate() + #? dunno + self._processing = False + + def accept_call_with_audio(self): + if self._processing: + LOG.warn(f" accept_call_with_audio from {self._friend_number}") + return + LOG.debug(f" accept_call_with_audio from {self._friend_number}") + self._processing = True + try: + self._calls_manager.accept_call(self._friend_number, True, False) + finally: + #? self.stop() + LOG.debug(f" accept_call_with_audio NOT stop from={self._friend_number}") + pass + + def accept_call_with_video(self): + # ts.trepan_handler() + + if self._processing: + LOG.warn(f" accept_call_with_video from {self._friend_number}") + return + self.setWindowTitle('Answering video call') + self._processing = True + LOG.debug(f" accept_call_with_video from {self._friend_number}") + try: + self._calls_manager.accept_call(self._friend_number, True, True) + finally: + self.stop() + + def decline_call(self): + LOG.debug(f"decline_call from {self._friend_number}") + if self._processing: + return + self._processing = True + try: + self._calls_manager.stop_call(self._friend_number, False) + except Exception as e: + LOG.warn(f"decline_call from {self._friend_number} {e}") + finally: + self.stop() + + def set_pixmap(self, pixmap): + self.avatar_label.setPixmap(pixmap) diff --git a/toxygen/ui/contact_items.py b/toxygen/ui/contact_items.py new file mode 100644 index 0000000..bdff447 --- /dev/null +++ b/toxygen/ui/contact_items.py @@ -0,0 +1,104 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtGui, QtWidgets +from toxygen_wrapper.toxcore_enums_and_consts import * + +from utils.util import * +from ui.widgets import DataLabel + + +class ContactItem(QtWidgets.QWidget): + """ + Contact in friends list + """ + + def __init__(self, settings, parent=None, kind='friend'): + QtWidgets.QWidget.__init__(self, parent) + mode = settings['compact_mode'] + self.setBaseSize(QtCore.QSize(250, 40 if mode else 70)) + self.avatar_label = QtWidgets.QLabel(self) + size = 32 if mode else 64 + self.avatar_label.setGeometry(QtCore.QRect(3, 4, size, size)) + self.avatar_label.setScaledContents(False) + self.avatar_label.setAlignment(QtCore.Qt.AlignCenter) + self.name = DataLabel(self) + self.name.setGeometry(QtCore.QRect(50 if mode else 75, 3 if mode else 10, 150, 15 if mode else 25)) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(10 if mode else 12) + font.setBold(True) + self.name.setFont(font) + self.status_message = DataLabel(self) + self.status_message.setGeometry(QtCore.QRect(50 if mode else 75, 20 if mode else 30, 170, 15 if mode else 20)) + font.setPointSize(10) + font.setBold(False) + self.status_message.setFont(font) + self.kind = DataLabel(self) + self.kind.setGeometry(QtCore.QRect(50 if mode else 75, 38 if mode else 48, 190, 15 if mode else 20)) + font.setBold(False) + font.setItalic(True) + self.kind.setFont(font) + self.connection_status = StatusCircle(self) + self.connection_status.setGeometry(QtCore.QRect(230, -2 if mode else 5, 32, 32)) + self.messages = UnreadMessagesCount(settings, self) + self.messages.setGeometry(QtCore.QRect(20 if mode else 52, 20 if mode else 50, 30, 20)) + + +class StatusCircle(QtWidgets.QWidget): + """ + Connection status + """ + def __init__(self, parent): + QtWidgets.QWidget.__init__(self, parent) + self.setGeometry(0, 0, 32, 32) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) + self.unread = False + + def update(self, status, unread_messages=None): + if unread_messages is None: + unread_messages = self.unread + else: + self.unread = unread_messages + if status == TOX_USER_STATUS['NONE']: + name = 'online' + elif status == TOX_USER_STATUS['AWAY']: + name = 'idle' + elif status == TOX_USER_STATUS['BUSY']: + name = 'busy' + else: + name = 'offline' + if unread_messages: + name += '_notification' + self.label.setGeometry(QtCore.QRect(0, 0, 32, 32)) + else: + self.label.setGeometry(QtCore.QRect(2, 0, 32, 32)) + pixmap = QtGui.QPixmap(join_path(get_images_directory(), '{}.png'.format(name))) + self.label.setPixmap(pixmap) + + +class UnreadMessagesCount(QtWidgets.QWidget): + + def __init__(self, settings, parent=None): + super().__init__(parent) + self._settings = settings + self.resize(30, 20) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(0, 0, 30, 20)) + self.label.setVisible(False) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(12) + font.setBold(True) + self.label.setFont(font) + self.label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter) + color = settings['unread_color'] + self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') + + def update(self, messages_count): + color = self._settings['unread_color'] + self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }') + if messages_count: + self.label.setVisible(True) + self.label.setText(str(messages_count)) + else: + self.label.setVisible(False) diff --git a/toxygen/ui/create_profile_screen.py b/toxygen/ui/create_profile_screen.py new file mode 100644 index 0000000..f507a1d --- /dev/null +++ b/toxygen/ui/create_profile_screen.py @@ -0,0 +1,54 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import uic + +from ui.widgets import * +import utils.util as util +import utils.ui as util_ui + +class CreateProfileScreenResult: + + def __init__(self, save_into_default_folder, password): + self._save_into_default_folder = save_into_default_folder + self._password = password + + def get_save_into_default_folder(self): + return self._save_into_default_folder + + save_into_default_folder = property(get_save_into_default_folder) + + def get_password(self): + return self._password + + password = property(get_password) + + +class CreateProfileScreen(CenteredWidget, DialogWithResult): + + def __init__(self): + CenteredWidget.__init__(self) + DialogWithResult.__init__(self) + uic.loadUi(util.get_views_path('create_profile_screen'), self) + self.center() + self.createProfile.clicked.connect(self._create_profile) + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('New profile settings')) + self.defaultFolder.setText(util_ui.tr('Save in default folder')) + self.programFolder.setText(util_ui.tr('Save in program folder')) + self.password.setPlaceholderText(util_ui.tr('Password')) + self.confirmPassword.setPlaceholderText(util_ui.tr('Confirm password')) + self.createProfile.setText(util_ui.tr('Create profile')) + self.passwordLabel.setText(util_ui.tr('Password (at least 8 symbols):')) + + def _create_profile(self): + password = self.password.text() + if password != self.confirmPassword.text(): + self.errorLabel.setText(util_ui.tr('Passwords do not match')) + return + if 0 < len(password) < 8: + self.errorLabel.setText(util_ui.tr('Password must be at least 8 symbols')) + return + result = CreateProfileScreenResult(self.defaultFolder.isChecked(), password) + self.close_with_result(result) diff --git a/toxygen/ui/group_bans_widgets.py b/toxygen/ui/group_bans_widgets.py new file mode 100644 index 0000000..60f1e9e --- /dev/null +++ b/toxygen/ui/group_bans_widgets.py @@ -0,0 +1,76 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import uic, QtWidgets, QtCore + +from ui.widgets import CenteredWidget +import utils.util as util +import utils.ui as util_ui + +class GroupBanItem(QtWidgets.QWidget): + + def __init__(self, ban, cancel_ban, can_cancel_ban, parent=None): + super().__init__(parent) + self._ban = ban + self._cancel_ban = cancel_ban + self._can_cancel_ban = can_cancel_ban + + uic.loadUi(util.get_views_path('gc_ban_item'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.banTargetLabel.setText(self._ban.ban_target) + ban_time = self._ban.ban_time + self.banTimeLabel.setText(util.unix_time_to_long_str(ban_time)) + + self.cancelPushButton.clicked.connect(self.cancel_ban) + self.cancelPushButton.setEnabled(self.can_cancel_ban) + + def _retranslate_ui(self): + self.cancelPushButton.setText(util_ui.tr('Cancel ban')) + + def cancel_ban(self): # pylint: disable=method-hidden + # FixMe broken + # self._cancel_ban(self._ban.ban_id) + pass + + def can_cancel_ban(self): # pylint: disable=method-hidden + # FixMe missing + pass + +class GroupBansScreen(CenteredWidget): + + def __init__(self, groups_service, group): + super().__init__() + self._groups_service = groups_service + self._group = group + + uic.loadUi(util.get_views_path('bans_list_screen'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self._refresh_bans_list() + + def _retranslate_ui(self): +# self.setWindowTitle(util_ui.tr('Bans list for group "{}"').format(self._group.name)) + pass + + def _refresh_bans_list(self): + self.bansListWidget.clear() + can_cancel_ban = self._group.is_self_moderator_or_founder() + for ban in self._group.bans: + self._create_ban_item(ban, can_cancel_ban) + + def _create_ban_item(self, ban, can_cancel_ban): + item = GroupBanItem(ban, self._on_ban_cancelled, can_cancel_ban, self.bansListWidget) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(item.width(), item.height())) + self.bansListWidget.addItem(elem) + self.bansListWidget.setItemWidget(elem, item) + + def _on_ban_cancelled(self, ban_id): + self._groups_service.cancel_ban(self._group.number, ban_id) + self._refresh_bans_list() diff --git a/toxygen/ui/group_invites_widgets.py b/toxygen/ui/group_invites_widgets.py new file mode 100644 index 0000000..6b9fa9e --- /dev/null +++ b/toxygen/ui/group_invites_widgets.py @@ -0,0 +1,138 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import logging +from qtpy import uic, QtWidgets + +import utils.util as util +from ui.widgets import * + +global LOG +LOG = logging.getLogger('app') + +class GroupInviteItem(QtWidgets.QWidget): + + def __init__(self, parent, chat_name, avatar, friend_name): + super().__init__(parent) + uic.loadUi(util.get_views_path('gc_invite_item'), self) + + self.groupNameLabel.setText(chat_name) + self.friendNameLabel.setText(friend_name) + self.friendAvatarLabel.setPixmap(avatar) + + def is_selected(self): + return self.selectCheckBox.isChecked() + + def subscribe_checked_event(self, callback): + self.selectCheckBox.clicked.connect(callback) + + +class GroupInvitesScreen(CenteredWidget): + + def __init__(self, groups_service, profile, contacts_provider): + super().__init__() + self._groups_service = groups_service + self._profile = profile + self._contacts_provider = contacts_provider + self._tox = self._groups_service._tox + + uic.loadUi(util.get_views_path('group_invites_screen'), self) + + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self._refresh_invites_list() + + self.nickLineEdit.setText(self._profile.name) + self.statusComboBox.setCurrentIndex(self._profile.status or 0) + + self.nickLineEdit.textChanged.connect(self._nick_changed) + self.acceptPushButton.clicked.connect(self._accept_invites) + self.declinePushButton.clicked.connect(self._decline_invites) + + self.invitesListWidget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.invitesListWidget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + + self._update_buttons_state() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Group chat invites')) + self.noInvitesLabel.setText(util_ui.tr('No group invites found')) + self.acceptPushButton.setText(util_ui.tr('Accept')) + self.declinePushButton.setText(util_ui.tr('Decline')) + self.statusComboBox.addItem(util_ui.tr('Online')) + self.statusComboBox.addItem(util_ui.tr('Away')) + self.statusComboBox.addItem(util_ui.tr('Busy')) + self.nickLineEdit.setPlaceholderText(util_ui.tr('Your nick in chat')) + self.passwordLineEdit.setPlaceholderText(util_ui.tr('Optional password')) + + def _get_friend(self, public_key): + return self._contacts_provider.get_friend_by_public_key(public_key) + + def _accept_invites(self): + nick = self.nickLineEdit.text() + password = self.passwordLineEdit.text() + status = self.statusComboBox.currentIndex() + + if not nick: + nick = self._tox.self_get_name() + selected_invites = self._get_selected_invites() + for invite in selected_invites: + LOG.debug(f"_accept_invites {nick}") + self._groups_service.accept_group_invite(invite, nick, status, password) + + self._refresh_invites_list() + self._close_window_if_needed() + + def _decline_invites(self): + selected_invites = self._get_selected_invites() + for invite in selected_invites: + LOG.debug(f"_groups_service.decline_group_invite") + self._groups_service.decline_group_invite(invite) + + self._refresh_invites_list() + self._close_window_if_needed() + + def _get_selected_invites(self): + all_invites = self._groups_service.get_group_invites() + selected = [] + items_count = len(all_invites) + for index in range(items_count): + list_item = self.invitesListWidget.item(index) + item_widget = self.invitesListWidget.itemWidget(list_item) + if item_widget and item_widget.is_selected(): + selected.append(all_invites[index]) + + return selected + + def _refresh_invites_list(self): + self.invitesListWidget.clear() + invites = self._groups_service.get_group_invites() + for invite in invites: + self._create_invite_item(invite) + + def _create_invite_item(self, invite): + friend = self._get_friend(invite.friend_public_key) + item = GroupInviteItem(self.invitesListWidget, invite.chat_name, friend.get_pixmap(), friend.name) + item.subscribe_checked_event(self._item_selected) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(item.width(), item.height())) + self.invitesListWidget.addItem(elem) + self.invitesListWidget.setItemWidget(elem, item) + + def _item_selected(self): + self._update_buttons_state() + + def _nick_changed(self): + self._update_buttons_state() + + def _update_buttons_state(self): + nick = self.nickLineEdit.text() + selected_items = self._get_selected_invites() + self.acceptPushButton.setEnabled(bool(nick) and len(selected_items)) + self.declinePushButton.setEnabled(len(selected_items) > 0) + + def _close_window_if_needed(self): + if self._groups_service.group_invites_count == 0: + self.close() diff --git a/toxygen/ui/group_peers_list.py b/toxygen/ui/group_peers_list.py new file mode 100644 index 0000000..ec8f95d --- /dev/null +++ b/toxygen/ui/group_peers_list.py @@ -0,0 +1,37 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from ui.widgets import * +from toxygen_wrapper.toxcore_enums_and_consts import * + +class PeerItem(QtWidgets.QWidget): + + def __init__(self, peer, handler, width, parent=None): + super().__init__(parent) + self.resize(QtCore.QSize(width, 34)) + self.nameLabel = DataLabel(self) + self.nameLabel.setGeometry(5, 0, width - 5, 34) + name = peer.name + if peer.is_current_user: + name += util_ui.tr(' *') + self.nameLabel.setText(name) + if peer.status == TOX_USER_STATUS['NONE']: + if peer.is_current_user: + style = 'QLabel {color: magenta}' + else: + style = 'QLabel {color: green}' + elif peer.status == TOX_USER_STATUS['AWAY']: + style = 'QLabel {color: blue}' + else: + style = 'QLabel {color: red}' + self.nameLabel.setStyleSheet(style) + self.nameLabel.mousePressEvent = lambda x: handler(peer.id) + + +class PeerTypeItem(QtWidgets.QWidget): + + def __init__(self, text, width, parent=None): + super().__init__(parent) + self.resize(QtCore.QSize(width, 34)) + self.nameLabel = DataLabel(self) + self.nameLabel.setGeometry(5, 0, width - 5, 34) + self.nameLabel.setText(text) diff --git a/toxygen/ui/group_settings_widgets.py b/toxygen/ui/group_settings_widgets.py new file mode 100644 index 0000000..5fd04d4 --- /dev/null +++ b/toxygen/ui/group_settings_widgets.py @@ -0,0 +1,86 @@ +from ui.widgets import CenteredWidget +from qtpy import uic +import utils.util as util +import utils.ui as util_ui + +class GroupManagementScreen(CenteredWidget): + + def __init__(self, groups_service, group): + super().__init__() + self._groups_service = groups_service + self._group = group + + uic.loadUi(util.get_views_path('group_management_screen'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.passwordLineEdit.setText(self._group.password) + self.privacyStateComboBox.setCurrentIndex(1 if self._group.is_private else 0) + self.peersLimitSpinBox.setValue(self._group.peers_limit) + + self.deletePushButton.clicked.connect(self._delete) + self.savePushButton.clicked.connect(self._save) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Group "{}"').format(self._group.name)) + self.passwordLabel.setText(util_ui.tr('Password:')) + self.peerLimitLabel.setText(util_ui.tr('Peer limit:')) + self.privacyStateLabel.setText(util_ui.tr('Privacy state:')) + self.deletePushButton.setText(util_ui.tr('Delete')) + self.savePushButton.setText(util_ui.tr('Save')) + + self.privacyStateComboBox.clear() + self.privacyStateComboBox.addItem(util_ui.tr('Public')) + self.privacyStateComboBox.addItem(util_ui.tr('Private')) + + def _delete(self): + self._groups_service.leave_group(self._group.number) + self.close() + + def _disconnect(self): + self._groups_service.disconnect_from_group(self._group.number) + self.close() + + def _save(self): + password = self.passwordLineEdit.text() + privacy_state = self.privacyStateComboBox.currentIndex() + peers_limit = self.peersLimitSpinBox.value() + + self._groups_service.set_group_password(self._group, password) + self._groups_service.set_group_privacy_state(self._group, privacy_state) + self._groups_service.set_group_peers_limit(self._group, peers_limit) + + self.close() + + +class GroupSettingsScreen(CenteredWidget): + + def __init__(self, group): + super().__init__() + self._group = group + + uic.loadUi(util.get_views_path('gc_settings_screen'), self) + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.copyPasswordPushButton.clicked.connect(self._copy_password) + self.copyPasswordPushButton.setEnabled(bool(self._group.password)) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Group "{}"').format(self._group.name)) + if self._group.password: + password_label_text = '{} {}'.format(util_ui.tr('Password:'), self._group.password) + else: + password_label_text = util_ui.tr('Password is not set') + self.passwordLabel.setText(password_label_text) + self.peerLimitLabel.setText('{} {}'.format(util_ui.tr('Peer limit:'), self._group.peers_limit)) + privacy_state = util_ui.tr('Private') if self._group.is_private else util_ui.tr('Public') + self.privacyStateLabel.setText('{} {}'.format(util_ui.tr('Privacy state:'), privacy_state)) + self.copyPasswordPushButton.setText(util_ui.tr('Copy password')) + + def _copy_password(self): + util_ui.copy_to_clipboard(self._group.password) diff --git a/toxygen/ui/groups_widgets.py b/toxygen/ui/groups_widgets.py new file mode 100644 index 0000000..ceda0ef --- /dev/null +++ b/toxygen/ui/groups_widgets.py @@ -0,0 +1,125 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import uic + +import utils.util as util +from ui.widgets import * +from toxygen_wrapper.toxcore_enums_and_consts import * + +class BaseGroupScreen(CenteredWidget): + + def __init__(self, groups_service, profile): + super().__init__() + self._groups_service = groups_service + self._profile = profile + + def _retranslate_ui(self): + self.nickLineEdit.setPlaceholderText(util_ui.tr('Your nick in chat')) + self.nickLabel.setText(util_ui.tr('Nickname:')) + self.statusLabel.setText(util_ui.tr('Status:')) + self.statusComboBox.addItem(util_ui.tr('Online')) + self.statusComboBox.addItem(util_ui.tr('Away')) + self.statusComboBox.addItem(util_ui.tr('Busy')) + + +class CreateGroupScreen(BaseGroupScreen): + + def __init__(self, groups_service, profile): + super().__init__(groups_service, profile) + uic.loadUi(util.get_views_path('create_group_screen'), self) + self.center() + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.statusComboBox.setCurrentIndex(self._profile.status or 0) + self.nickLineEdit.setText(self._profile.name) + + self.addGroupButton.clicked.connect(self._create_group) + self.groupNameLineEdit.textChanged.connect(self._group_name_changed) + self.nickLineEdit.textChanged.connect(self._nick_changed) + + def _retranslate_ui(self): + super()._retranslate_ui() + self.setWindowTitle(util_ui.tr('Create new group chat')) + self.groupNameLabel.setText(util_ui.tr('Group name:')) + self.groupTypeLabel.setText(util_ui.tr('Group type:')) + self.groupNameLineEdit.setPlaceholderText(util_ui.tr('Group\'s persistent name')) + self.addGroupButton.setText(util_ui.tr('Create group')) + self.groupTypeComboBox.addItem(util_ui.tr('Public')) + self.groupTypeComboBox.addItem(util_ui.tr('Private')) + self.groupTypeComboBox.setCurrentIndex(1) + + def _create_group(self): + group_name = self.groupNameLineEdit.text() + privacy_state = self.groupTypeComboBox.currentIndex() + nick = self.nickLineEdit.text() + status = self.statusComboBox.currentIndex() + self._groups_service.create_new_gc(group_name, privacy_state, nick, status) + self.close() + + def _nick_changed(self): + self._update_button_state() + + def _group_name_changed(self): + self._update_button_state() + + def _update_button_state(self): + is_nick_set = bool(self.nickLineEdit.text()) + is_group_name_set = bool(self.groupNameLineEdit.text()) + self.addGroupButton.setEnabled(is_nick_set and is_group_name_set) + + +class JoinGroupScreen(BaseGroupScreen): + + def __init__(self, groups_service, profile): + super().__init__(groups_service, profile) + uic.loadUi(util.get_views_path('join_group_screen'), self) + self.center() + self._update_ui() + + def _update_ui(self): + self._retranslate_ui() + + self.statusComboBox.setCurrentIndex(self._profile.status or 0) + self.nickLineEdit.setText(self._profile.name) + + self.chatIdLineEdit.textChanged.connect(self._chat_id_changed) + self.joinGroupButton.clicked.connect(self._join_group) + self.nickLineEdit.textChanged.connect(self._nick_changed) + + def _retranslate_ui(self): + super()._retranslate_ui() + self.setWindowTitle(util_ui.tr('Join public group chat')) + self.chatIdLabel.setText(util_ui.tr('Group ID:')) + self.passwordLabel.setText(util_ui.tr('Password:')) + self.chatIdLineEdit.setPlaceholderText(util_ui.tr('Group\'s chat ID')) + self.joinGroupButton.setText(util_ui.tr('Join group')) + self.passwordLineEdit.setPlaceholderText(util_ui.tr('Optional password')) + + def _chat_id_changed(self): + self._update_button_state() + + def _nick_changed(self): + self._update_button_state() + + def _update_button_state(self): + chat_id = self._get_chat_id() + is_nick_set = bool(self.nickLineEdit.text()) + self.joinGroupButton.setEnabled(len(chat_id) == TOX_GROUP_CHAT_ID_SIZE * 2 and is_nick_set) + + def _join_group(self): + chat_id = self._get_chat_id() + password = self.passwordLineEdit.text() + nick = self.nickLineEdit.text() + status = self.statusComboBox.currentIndex() + self._groups_service.join_gc_by_id(chat_id, password, nick, status) + self.close() + + def _get_chat_id(self): + chat_id = self.chatIdLineEdit.text().strip() + if chat_id.startswith('tox:'): + chat_id = chat_id[4:] + + return chat_id diff --git a/toxygen/ui/items_factories.py b/toxygen/ui/items_factories.py new file mode 100644 index 0000000..530839c --- /dev/null +++ b/toxygen/ui/items_factories.py @@ -0,0 +1,109 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from ui.contact_items import * +from ui.messages_widgets import * + +class ContactItemsFactory: + + def __init__(self, settings, main_screen): + self._settings = settings + self._friends_list = main_screen.friends_list + + def create_contact_item(self): + item = ContactItem(self._settings) + elem = QtWidgets.QListWidgetItem(self._friends_list) + elem.setSizeHint(QtCore.QSize(250, 40 if self._settings['compact_mode'] else 70)) + self._friends_list.addItem(elem) + self._friends_list.setItemWidget(elem, item) + + return item + + +class MessagesItemsFactory: + + def __init__(self, settings, plugin_loader, smiley_loader, main_screen, delete_action): + self._file_transfers_handler = None + self._settings, self._plugin_loader = settings, plugin_loader + self._smiley_loader, self._delete_action = smiley_loader, delete_action + self._messages = main_screen.messages + self._message_edit = main_screen.messageEdit + + def set_file_transfers_handler(self, file_transfers_handler): + self._file_transfers_handler = file_transfers_handler + + def create_message_item(self, message, append=True, pixmap=None): + item = message.get_widget(self._settings, self._create_message_browser, + self._delete_action, self._messages) + if pixmap is not None: + item.set_avatar(pixmap) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(0, elem) + self._messages.setItemWidget(elem, item) + + return item + +# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_handler.py", line 216, in transfer_finished +# self._file_transfers_message_service.add_inline_message(transfer, index) +# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_messages_service.py", line 47, in add_inline_message +# self._create_inline_item(transfer.data, count + index + 1) +# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_messages_service.py", line 75, in _create_inline_item +# return self._messages_items_factory.create_inline_item(data, False, position) +# File "/var/local/src/toxygen/toxygen/ui/items_factories.py", line 50, in create_inline_item +# item = InlineImageItem(message.data, self._messages.width(), elem, self._messages) +# AttributeError: 'bytes' object has no attribute 'data' + + def create_inline_item(self, message, append=True, position=0): + elem = QtWidgets.QListWidgetItem() + # AttributeError: 'bytes' object has no attribute 'data' + if type(message) == bytes: + # was used + data = message + elif hasattr(message, 'data'): + # used + data = message.data + else: + # unreached + return None + item = InlineImageItem(data, self._messages.width(), elem, self._messages) + elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height())) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(position, elem) + self._messages.setItemWidget(elem, item) + + return item + + def create_unsent_file_item(self, message, append=True): + item = message.get_widget(self._file_transfers_handler, self._settings, self._messages.width(), self._messages) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(0, elem) + self._messages.setItemWidget(elem, item) + + return item + + def create_file_transfer_item(self, message, append=True): + item = message.get_widget(self._file_transfers_handler, self._settings, self._messages.width(), self._messages) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34)) + if append: + self._messages.addItem(elem) + else: + self._messages.insertItem(0, elem) + self._messages.setItemWidget(elem, item) + + return item + + # Private methods + + def _create_message_browser(self, text, width, message_type, parent=None): + return MessageBrowser(self._settings, self._message_edit, self._smiley_loader, self._plugin_loader, + text, width, message_type, parent) diff --git a/toxygen/ui/login_screen.py b/toxygen/ui/login_screen.py new file mode 100644 index 0000000..93362fd --- /dev/null +++ b/toxygen/ui/login_screen.py @@ -0,0 +1,80 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os.path + +from qtpy import uic + +from ui.widgets import * +import utils.util as util +import utils.ui as util_ui + +class LoginScreenResult: + + def __init__(self, profile_path, load_as_default, password=None): + self._profile_path = profile_path + self._load_as_default = load_as_default + self._password = password + + def get_profile_path(self): + return self._profile_path + + profile_path = property(get_profile_path) + + def get_load_as_default(self): + return self._load_as_default + + load_as_default = property(get_load_as_default) + + def get_password(self): + return self._password + + password = property(get_password) + + def is_new_profile(self): + return not os.path.isfile(self._profile_path) + + +class LoginScreen(CenteredWidget, DialogWithResult): + + def __init__(self): + CenteredWidget.__init__(self) + DialogWithResult.__init__(self) + uic.loadUi(util.get_views_path('login_screen'), self) + self.center() + self._profiles = [] + self._update_ui() + + def update_select(self, profiles): + profiles = sorted(profiles, key=lambda p: p[1]) + self._profiles = list(profiles) + self.profilesComboBox.addItems(list(map(lambda p: p[1], profiles))) + self.loadProfilePushButton.setEnabled(len(profiles) > 0) + + def _update_ui(self): + self.profileNameLineEdit = LineEditWithEnterSupport(self._create_profile, self) + self.profileNameLineEdit.setGeometry(QtCore.QRect(20, 100, 160, 30)) + self._retranslate_ui() + self.createProfilePushButton.clicked.connect(self._create_profile) + self.loadProfilePushButton.clicked.connect(self._load_existing_profile) + + def _create_profile(self): + path = self.profileNameLineEdit.text() + load_as_default = self.defaultProfileCheckBox.isChecked() + result = LoginScreenResult(path, load_as_default) + self.close_with_result(result) + + def _load_existing_profile(self): + index = self.profilesComboBox.currentIndex() + load_as_default = self.defaultProfileCheckBox.isChecked() + path = util.join_path(self._profiles[index][0], self._profiles[index][1] + '.tox') + result = LoginScreenResult(path, load_as_default) + self.close_with_result(result) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Log in')) + self.profileNameLineEdit.setPlaceholderText(util_ui.tr('Profile name')) + self.createProfilePushButton.setText(util_ui.tr('Create')) + self.loadProfilePushButton.setText(util_ui.tr('Load profile')) + self.defaultProfileCheckBox.setText(util_ui.tr('Use as default')) + self.existingProfileGroupBox.setTitle(util_ui.tr('Load existing profile')) + self.newProfileGroupBox.setTitle(util_ui.tr('Create new profile')) diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py new file mode 100644 index 0000000..f65a0af --- /dev/null +++ b/toxygen/ui/main_screen.py @@ -0,0 +1,1073 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import os +import traceback + +from qtpy import uic +from qtpy import QtCore, QtGui, QtWidgets +from qtpy.QtGui import (QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QFontMetrics) + +from ui.contact_items import * +from ui.widgets import MultilineEdit +from ui.main_screen_widgets import * +import utils.util as util +import utils.ui as util_ui +from user_data.settings import Settings + +import logging +global LOG +LOG = logging.getLogger('app.'+'mains') + +iMAX = 70 + +try: + # https://github.com/pyqtconsole/pyqtconsole + from pyqtconsole.console import PythonConsole + import pyqtconsole.highlighter as hl +except Exception as e: + LOG.warn(e) + PythonConsole = None +else: + if True: + # I want to do reverse video but I cant figure how + bg='white' + def hl_format(color, style=''): + """Return a QTextCharFormat with the given attributes. + """ + _color = QColor() + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + _bgcolor = QColor() + _bgcolor.setNamedColor(bg) + _format.setBackground(_bgcolor) + return _format + + aFORMATS = { + 'keyword': hl_format('blue', 'bold'), + 'operator': hl_format('red'), + 'brace': hl_format('darkGray'), + 'defclass': hl_format('black', 'bold'), + 'string': hl_format('magenta'), + 'string2': hl_format('darkMagenta'), + 'comment': hl_format('darkGreen', 'italic'), + 'self': hl_format('black', 'italic'), + 'numbers': hl_format('brown'), + 'inprompt': hl_format('darkBlue', 'bold'), + 'outprompt': hl_format('darkRed', 'bold'), + } + else: + bg = 'black' + def hl_format(color, style=''): + + """Return a QTextCharFormat with the given attributes. + unused + """ + _color = QColor() + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + _bgcolor = QColor() + _bgcolor.setNamedColor(bg) + _format.setBackground(_bgcolor) + return _format + aFORMATS = { + 'keyword': hl_format('blue', 'bold'), + 'operator': hl_format('red'), + 'brace': hl_format('lightGray'), + 'defclass': hl_format('white', 'bold'), + 'string': hl_format('magenta'), + 'string2': hl_format('lightMagenta'), + 'comment': hl_format('lightGreen', 'italic'), + 'self': hl_format('white', 'italic'), + 'numbers': hl_format('lightBrown'), + 'inprompt': hl_format('lightBlue', 'bold'), + 'outprompt': hl_format('lightRed', 'bold'), + } + +class QTextEditLogger(logging.Handler): + def __init__(self, parent, app): + super().__init__() + self.widget = QtWidgets.QPlainTextEdit(parent) + self.widget.setReadOnly(True) + + if app and app._settings: + size = app._settings['message_font_size'] + font_name = app._settings['font'] + else: + size = 12 + font_name = "Courier New" + font = QtGui.QFont(font_name, size, QtGui.QFont.Bold) + self.widget.setFont(font) + + def emit(self, record): + msg = self.format(record) + self.widget.appendPlainText(msg) + + +class LogDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit): + + def __init__(self, parent=None, app=None): + global iMAX + super().__init__(parent) + + logTextBox = QTextEditLogger(self, app) + # You can format what is printed to text box - %(levelname)s + logTextBox.setFormatter(logging.Formatter('%(name)s %(asctime)-4s - %(message)s')) + logTextBox.setLevel(app._args.loglevel) + logging.getLogger().addHandler(logTextBox) + + self._button = QtWidgets.QPushButton(self) + self._button.setText('Copy All') + self._logTextBox = logTextBox + + layout = QtWidgets.QVBoxLayout() + # Add the new logging box widget to the layout + layout.addWidget(logTextBox.widget) + layout.addWidget(self._button) + self.setLayout(layout) + settings = Settings.get_default_settings(app._args) + #self.setBaseSize( + self.resize(min(iMAX * settings['message_font_size'], parent.width()), 350) + + # Connect signal to slot + self._button.clicked.connect(self.test) + + def test(self): + # FixMe: 65:8: E1101: Instance of 'QTextEditLogger' has no 'selectAll' member (no-member) + # :66:8: E1101: Instance of 'QTextEditLogger' has no 'copy' member (no-member) + if hasattr(self._logTextBox, 'selectAll'): + self._logTextBox.selectAll() + self._logTextBox.copy() + +class MainWindow(QtWidgets.QMainWindow): + + def __init__(self, settings, tray, app): + super().__init__() + self._settings = settings + self._contacts_manager = None + self._tray = tray + self._app = app + self._tox = app._tox + self._widget_factory = None + self._modal_window = None + self._plugins_loader = None + self.setAcceptDrops(True) + self._saved = False + self._smiley_window = None + self._profile = None + self._toxes = None + self._messenger = None + self._file_transfer_handler = self._history_loader = self._groups_service = self._calls_manager = None + self._should_show_group_peers_list = False + self.initUI() + global iMAX + if iMAX == 100: + # take a rough guess of 2/3 the default width at the default font + iMAX = settings['width'] * 2/3 / settings['message_font_size'] + self._me = LogDialog(self, app) + self._pe = None + self._we = None + + def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader, + file_transfer_handler, history_loader, calls_manager, groups_service, toxes, app): + self._widget_factory = widget_factory + self._tray = tray + self._contacts_manager = contacts_manager + self._profile = profile + self._plugins_loader = plugins_loader + self._file_transfer_handler = file_transfer_handler + self._history_loader = history_loader + self._calls_manager = calls_manager + self._groups_service = groups_service + self._toxes = toxes + self._app = app + self._messenger = messenger + self._contacts_manager.active_contact_changed.add_callback(self._new_contact_selected) + self.messageEdit.set_dependencies(messenger, contacts_manager, file_transfer_handler) + + self.update_gc_invites_button_state() + + def show(self): + super().show() + self._contacts_manager.update() + if self._settings['show_welcome_screen']: + self._modal_window = self._widget_factory.create_welcome_window() + + def setup_menu(self, window): + self.menubar = QtWidgets.QMenuBar(window) + self.menubar.setObjectName("menubar") + self.menubar.setNativeMenuBar(True) # was False + self.menubar.setMinimumSize(self.width(), 250) + self.menubar.setMaximumSize(self.width(), 32) + self.menubar.setBaseSize(self.width(), 250) + + self.actionTest_tox = QtWidgets.QAction(window) + self.actionTest_tox.setObjectName("actionTest_tox") + self.actionTest_nmap = QtWidgets.QAction(window) + self.actionTest_nmap.setObjectName("actionTest_nmap") + self.actionTest_main = QtWidgets.QAction(window) + self.actionTest_main.setObjectName("actionTest_main") + self.actionQuit_program = QtWidgets.QAction(window) + self.actionQuit_program.setObjectName("actionQuit_program") + + self.menuProfile = QtWidgets.QMenu(self.menubar) + self.menuProfile.setObjectName("menuProfile") + self.menuGC = QtWidgets.QMenu(self.menubar) + self.menuSettings = QtWidgets.QMenu(self.menubar) + self.menuSettings.setObjectName("menuSettings") + self.menuPlugins = QtWidgets.QMenu(self.menubar) + self.menuPlugins.setObjectName("menuPlugins") + self.menuAbout = QtWidgets.QMenu(self.menubar) # alignment=QtCore.Qt.AlignRight + self.menuAbout.setObjectName("menuAbout") + + self.actionAdd_friend = QtWidgets.QAction(window) + self.actionAdd_friend.setObjectName("actionAdd_friend") + + self.actionProfile_settings = QtWidgets.QAction(window) + self.actionProfile_settings.setObjectName("actionProfile_settings") + self.actionPrivacy_settings = QtWidgets.QAction(window) + self.actionPrivacy_settings.setObjectName("actionPrivacy_settings") + self.actionInterface_settings = QtWidgets.QAction(window) + self.actionInterface_settings.setObjectName("actionInterface_settings") + self.actionNotifications = QtWidgets.QAction(window) + self.actionNotifications.setObjectName("actionNotifications") + self.actionNetwork = QtWidgets.QAction(window) + self.actionNetwork.setObjectName("actionNetwork") + self.actionAbout_program = QtWidgets.QAction(window) + self.actionAbout_program.setObjectName("actionAbout_program") + + self.actionLog_console = QtWidgets.QAction(window) + self.actionLog_console.setObjectName("actionLog_console") + self.actionPython_console = QtWidgets.QAction(window) + self.actionPython_console.setObjectName("actionLog_console") + self.actionWeechat_console = QtWidgets.QAction(window) + self.actionWeechat_console.setObjectName("actionLog_console") + self.updateSettings = QtWidgets.QAction(window) + self.actionSettings = QtWidgets.QAction(window) + self.actionSettings.setObjectName("actionSettings") + self.audioSettings = QtWidgets.QAction(window) + self.videoSettings = QtWidgets.QAction(window) + self.pluginData = QtWidgets.QAction(window) + self.importPlugin = QtWidgets.QAction(window) + self.reloadPlugins = QtWidgets.QAction(window) + self.reloadToxchat = QtWidgets.QAction(window) + + self.lockApp = QtWidgets.QAction(window) + self.createGC = QtWidgets.QAction(window) + self.joinGC = QtWidgets.QAction(window) + self.gc_invites = QtWidgets.QAction(window) + + self.menuProfile.addAction(self.actionAdd_friend) + self.menuProfile.addAction(self.actionSettings) + self.menuProfile.addAction(self.lockApp) + self.menuProfile.addAction(self.actionTest_tox) + self.menuProfile.addAction(self.actionTest_nmap) + self.menuProfile.addAction(self.actionTest_main) + self.menuProfile.addAction(self.actionQuit_program) + + self.menuGC.addAction(self.createGC) + self.menuGC.addAction(self.joinGC) + self.menuGC.addAction(self.gc_invites) + self.menuSettings.addAction(self.actionProfile_settings) + self.menuSettings.addAction(self.actionPrivacy_settings) + self.menuSettings.addAction(self.actionInterface_settings) + self.menuSettings.addAction(self.actionNotifications) + self.menuSettings.addAction(self.actionNetwork) + self.menuSettings.addAction(self.audioSettings) + self.menuSettings.addAction(self.videoSettings) +## self.menuSettings.addAction(self.updateSettings) + self.menuPlugins.addAction(self.pluginData) + self.menuPlugins.addAction(self.importPlugin) + self.menuPlugins.addAction(self.reloadPlugins) + self.menuPlugins.addAction(self.reloadToxchat) + self.menuPlugins.addAction(self.actionLog_console) + self.menuPlugins.addAction(self.actionPython_console) + self.menuPlugins.addAction(self.actionWeechat_console) + + self.menuAbout.addAction(self.actionAbout_program) + + self.menubar.addAction(self.menuProfile.menuAction()) + self.menubar.addAction(self.menuGC.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuPlugins.menuAction()) + self.menubar.addAction(self.menuAbout.menuAction()) + + self.actionTest_nmap.triggered.connect(self.test_nmap) + self.actionTest_main.triggered.connect(self.test_main) + self.actionTest_tox.triggered.connect(self.test_tox) + + self.actionQuit_program.triggered.connect(self.quit_program) + self.actionAbout_program.triggered.connect(self.about_program) + self.actionLog_console.triggered.connect(self.log_console) + self.actionPython_console.triggered.connect(self.python_console) + self.actionWeechat_console.triggered.connect(self.weechat_console) + self.actionNetwork.triggered.connect(self.network_settings) + self.actionAdd_friend.triggered.connect(self.add_contact_triggered) + self.createGC.triggered.connect(self.create_gc) + self.joinGC.triggered.connect(self.join_gc) + self.actionProfile_settings.triggered.connect(self.profile_settings) + self.actionPrivacy_settings.triggered.connect(self.privacy_settings) + self.actionInterface_settings.triggered.connect(self.interface_settings) + self.actionNotifications.triggered.connect(self.notification_settings) + self.audioSettings.triggered.connect(self.audio_settings) + self.videoSettings.triggered.connect(self.video_settings) +## self.updateSettings.triggered.connect(self.update_settings) + self.pluginData.triggered.connect(self.plugins_menu) + self.lockApp.triggered.connect(self.lock_app) + self.importPlugin.triggered.connect(self.import_plugin) + self.reloadPlugins.triggered.connect(self.reload_plugins) + self.reloadToxchat.triggered.connect(self.reload_toxchat) + self.gc_invites.triggered.connect(self._open_gc_invites_list) + + def languageChange(self, *args, **kwargs): + self.retranslateUi() + + def event(self, event): + if event.type() == QtCore.QEvent.WindowActivate: + if hasattr(self, '_tray') and self._tray: + self._tray.setIcon(QtGui.QIcon(util.join_path(util.get_images_directory(), 'icon.png'))) + self.messages.repaint() + return super().event(event) + + def status(self, line): + """For now, this uses the unused space on the menubar line + It could be a status line at the bottom, or a statusline with history.""" + self.menuAbout.setTitle(line[:iMAX]) + return line + + def retranslateUi(self): + self.lockApp.setText(util_ui.tr("Lock")) + self.menuPlugins.setTitle(util_ui.tr("Plugins")) + self.menuGC.setTitle(util_ui.tr("Group chats")) + self.pluginData.setText(util_ui.tr("List of plugins")) + self.menuProfile.setTitle(util_ui.tr("Profile")) + self.menuSettings.setTitle(util_ui.tr("Settings")) + self.menuAbout.setTitle(util_ui.tr("About")) + self.actionAdd_friend.setText(util_ui.tr("Add contact")) + self.createGC.setText(util_ui.tr("Create group chat")) + self.joinGC.setText(util_ui.tr("Join group chat")) + self.gc_invites.setText(util_ui.tr("Group invites")) + self.actionProfile_settings.setText(util_ui.tr("Profile")) + self.actionPrivacy_settings.setText(util_ui.tr("Privacy")) + self.actionInterface_settings.setText(util_ui.tr("Interface")) + self.actionNotifications.setText(util_ui.tr("Notifications")) + self.actionNetwork.setText(util_ui.tr("Network")) + self.actionAbout_program.setText(util_ui.tr("About program")) + self.actionLog_console.setText(util_ui.tr("Console Log")) + self.actionPython_console.setText(util_ui.tr("Python Console")) + self.actionWeechat_console.setText(util_ui.tr("Weechat Console")) + self.actionTest_tox.setText(util_ui.tr("Bootstrap")) + self.actionTest_nmap.setText(util_ui.tr("Test Nodes")) + self.actionTest_main.setText(util_ui.tr("Test Program")) + self.actionQuit_program.setText(util_ui.tr("Quit program")) + self.actionSettings.setText(util_ui.tr("Settings")) + self.audioSettings.setText(util_ui.tr("Audio")) + self.videoSettings.setText(util_ui.tr("Video")) + self.updateSettings.setText(util_ui.tr("Updates")) + self.importPlugin.setText(util_ui.tr("Import plugin")) + self.reloadPlugins.setText(util_ui.tr("Reload plugins")) + self.reloadToxchat.setText(util_ui.tr("Reload tox.chat")) + + self.searchLineEdit.setPlaceholderText(util_ui.tr("Search")) + self.sendMessageButton.setToolTip(util_ui.tr("Send message")) + self.callButton.setToolTip(util_ui.tr("Start audio call with friend")) + self.contactsFilterComboBox.clear() + self.contactsFilterComboBox.addItem(util_ui.tr("All")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online first")) + self.contactsFilterComboBox.addItem(util_ui.tr("Name")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online and by name")) + self.contactsFilterComboBox.addItem(util_ui.tr("Online first and by name")) + self.contactsFilterComboBox.addItem(util_ui.tr("Kind")) + + def setup_right_bottom(self, Form): + Form.resize(650, 60) + self.messageEdit = MessageArea(Form, self) + self.messageEdit.setGeometry(QtCore.QRect(0, 3, 450, 55)) + font = QtGui.QFont() + font.setPointSize(11) + font.setBold(True) + font.setFamily(self._settings['font']) + self.messageEdit.setFont(font) + + self.sendMessageButton = QtWidgets.QPushButton(Form) + self.sendMessageButton.setGeometry(QtCore.QRect(565, 3, 60, 55)) + + self.menuButton = MenuButton(Form, self.show_menu) + self.menuButton.setGeometry(QtCore.QRect(QtCore.QRect(455, 3, 55, 55))) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'send.png')) + icon = QtGui.QIcon(pixmap) + self.sendMessageButton.setIcon(icon) + self.sendMessageButton.setIconSize(QtCore.QSize(45, 60)) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'menu.png')) + icon = QtGui.QIcon(pixmap) + self.menuButton.setIcon(icon) + self.menuButton.setIconSize(QtCore.QSize(40, 40)) + + self.sendMessageButton.clicked.connect(self.send_message) + + QtCore.QMetaObject.connectSlotsByName(Form) + + def setup_left_column(self, left_column): + uic.loadUi(util.get_views_path('ms_left_column'), left_column) + + pixmap = QtGui.QPixmap() + pixmap.load(util.join_path(util.get_images_directory(), 'search.png')) + left_column.searchLabel.setPixmap(pixmap) + + self.name = DataLabel(left_column) + self.name.setGeometry(QtCore.QRect(75, 15, 150, 25)) + font = QtGui.QFont() + font.setFamily(self._settings['font']) + font.setPointSize(14) + font.setBold(True) + self.name.setFont(font) + + self.status_message = DataLabel(left_column) + self.status_message.setGeometry(QtCore.QRect(75, 35, 170, 25)) + + self.connection_status = StatusCircle(left_column) + self.connection_status.setGeometry(QtCore.QRect(230, 10, 32, 32)) + + left_column.contactsFilterComboBox.activated[int].connect(lambda x: self._filtering()) + + self.avatar_label = left_column.avatarLabel + self.searchLineEdit = left_column.searchLineEdit + self.contacts_filter = self.contactsFilterComboBox = left_column.contactsFilterComboBox + + self.groupInvitesPushButton = left_column.groupInvitesPushButton + + self.groupInvitesPushButton.clicked.connect(self._open_gc_invites_list) + self.avatar_label.mouseReleaseEvent = self.profile_settings + self.status_message.mouseReleaseEvent = self.profile_settings + self.name.mouseReleaseEvent = self.profile_settings + + self.friends_list = left_column.friendsListWidget + self.friends_list.itemSelectionChanged.connect(self._selected_contact_changed) + self.friends_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.friends_list.customContextMenuRequested.connect(self._friend_right_click) + self.friends_list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.friends_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.friends_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.friends_list.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) + + def setup_right_top(self, Form): + Form.resize(650, 75) + self.account_avatar = QtWidgets.QLabel(Form) + self.account_avatar.setGeometry(QtCore.QRect(10, 5, 64, 64)) + self.account_avatar.setScaledContents(False) + self.account_name = DataLabel(Form) + self.account_name.setGeometry(QtCore.QRect(100, 0, 400, 25)) + self.account_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) + font = QtGui.QFont() + font.setFamily(self._settings['font']) + font.setPointSize(14) + font.setBold(True) + self.account_name.setFont(font) + self.account_status = DataLabel(Form) + self.account_status.setGeometry(QtCore.QRect(100, 20, 400, 25)) + self.account_status.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) + font.setPointSize(12) + font.setBold(False) + self.account_status.setFont(font) + self.account_status.setObjectName("account_status") + self.callButton = QtWidgets.QPushButton(Form) + self.callButton.setGeometry(QtCore.QRect(550, 5, 50, 50)) + self.callButton.setObjectName("callButton") + self.callButton.clicked.connect(lambda: self._calls_manager.call_click(True)) + self.videocallButton = QtWidgets.QPushButton(Form) + self.videocallButton.setGeometry(QtCore.QRect(550, 5, 50, 50)) + self.videocallButton.setObjectName("videocallButton") + self.videocallButton.clicked.connect(lambda: self._calls_manager.call_click(True, True)) + self.groupMenuButton = QtWidgets.QPushButton(Form) + self.groupMenuButton.setGeometry(QtCore.QRect(470, 10, 50, 50)) + self.groupMenuButton.clicked.connect(self._toggle_gc_peers_list) + self.groupMenuButton.setVisible(False) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'menu.png')) + icon = QtGui.QIcon(pixmap) + self.groupMenuButton.setIcon(icon) + self.groupMenuButton.setIconSize(QtCore.QSize(45, 60)) + self.update_call_state('call') + self.typing = QtWidgets.QLabel(Form) + self.typing.setGeometry(QtCore.QRect(500, 25, 50, 30)) + pixmap = QtGui.QPixmap(QtCore.QSize(50, 30)) + pixmap.load(util.join_path(util.get_images_directory(), 'typing.png')) + self.typing.setScaledContents(False) + self.typing.setPixmap(pixmap.scaled(50, 30, QtCore.Qt.KeepAspectRatio)) + self.typing.setVisible(False) + QtCore.QMetaObject.connectSlotsByName(Form) + + def setup_right_center(self, widget): + self.messages = QtWidgets.QListWidget(widget) + self.messages.setGeometry(0, 0, 620, 310) + self.messages.setObjectName("messages") + self.messages.setSpacing(1) + self.messages.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.messages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.messages.focusOutEvent = lambda event: self.messages.clearSelection() + self.messages.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) + + def load(pos): + if not pos: + contact = self._contacts_manager.get_curr_contact() + self._history_loader.load_history(contact) + self.messages.verticalScrollBar().setValue(1) + self.messages.verticalScrollBar().valueChanged.connect(load) + self.messages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.messages.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + self.peers_list = QtWidgets.QListWidget(widget) + self.peers_list.setGeometry(0, 0, 0, 0) + self.peers_list.setObjectName("peersList") + self.peers_list.setSpacing(1) + self.peers_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.peers_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.peers_list.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu) + self.peers_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + + def initUI(self): + self.setMinimumSize(920, 500) + s = self._settings + self.setGeometry(s['x'], s['y'], s['width'], s['height']) + self.setWindowTitle('Toxygen') + menu = QtWidgets.QWidget() + main = QtWidgets.QWidget() + grid = QtWidgets.QGridLayout() + info = QtWidgets.QWidget() + left_column = QtWidgets.QWidget() + messages = QtWidgets.QWidget() + message_buttons = QtWidgets.QWidget() + self.setup_right_center(messages) + self.setup_right_top(info) + self.setup_right_bottom(message_buttons) + self.setup_left_column(left_column) + self.setup_menu(menu) + if not s['mirror_mode']: + grid.addWidget(left_column, 1, 0, 4, 1) + grid.addWidget(messages, 2, 1, 2, 1) + grid.addWidget(info, 1, 1) + grid.addWidget(message_buttons, 4, 1) + grid.setColumnMinimumWidth(1, 500) + grid.setColumnMinimumWidth(0, 270) + else: + grid.addWidget(left_column, 1, 1, 4, 1) + grid.addWidget(messages, 2, 0, 2, 1) + grid.addWidget(info, 1, 0) + grid.addWidget(message_buttons, 4, 0) + grid.setColumnMinimumWidth(0, 500) + grid.setColumnMinimumWidth(1, 270) + + grid.addWidget(menu, 0, 0, 1, 2) + grid.setSpacing(0) + grid.setContentsMargins(0, 0, 0, 0) + grid.setRowMinimumHeight(0, 25) + grid.setRowMinimumHeight(1, 75) + grid.setRowMinimumHeight(2, 25) + grid.setRowMinimumHeight(3, 320) + grid.setRowMinimumHeight(4, 55) + grid.setColumnStretch(1, 1) + grid.setRowStretch(3, 1) + main.setLayout(grid) + self.setCentralWidget(main) + self.messageEdit.setFocus() + self.friend_info = info + self.retranslateUi() + + def closeEvent(self, event): + close_setting = self._settings['close_app'] + if close_setting == 0 or self._settings.closing: + if self._saved: + return + self._saved = True + self._settings['x'] = self.geometry().x() + self._settings['y'] = self.geometry().y() + self._settings['width'] = self.width() + self._settings['height'] = self.height() + self._settings.save() + util_ui.close_all_windows() + event.accept() + elif close_setting == 2 and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + event.ignore() + self.hide() + else: + event.ignore() + self.showMinimized() + + def close_window(self): + self._settings.closing = True + self.close() + + def resizeEvent(self, *args, **kwargs): + width = self.width() - 270 + if not self._should_show_group_peers_list: + self.messages.setGeometry(0, 0, width, self.height() - 155) + self.peers_list.setGeometry(0, 0, 0, 0) + else: + self.messages.setGeometry(0, 0, width * 3 // 4, self.height() - 155) + self.peers_list.setGeometry(width * 3 // 4, 0, width - width * 3 // 4, self.height() - 155) + + invites_button_visible = self.groupInvitesPushButton.isVisible() +# LOG.debug(f"invites_button_visible={invites_button_visible}") + self.friends_list.setGeometry(0, 125 if invites_button_visible else 100, + 270, self.height() - 150 if invites_button_visible else self.height() - 125) + + self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 10, 50, 50)) + self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 10, 50, 50)) + self.groupMenuButton.setGeometry(QtCore.QRect(self.width() - 450, 10, 50, 50)) + self.typing.setGeometry(QtCore.QRect(self.width() - 450, 20, 50, 30)) + + self.messageEdit.setGeometry(QtCore.QRect(55, 0, self.width() - 395, 55)) + self.menuButton.setGeometry(QtCore.QRect(0, 0, 55, 55)) + self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 340, 0, 70, 55)) + + self.account_name.setGeometry(QtCore.QRect(100, 15, self.width() - 560, 25)) + self.account_status.setGeometry(QtCore.QRect(100, 35, self.width() - 560, 25)) + self.messageEdit.setFocus() + + def keyPressEvent(self, event): + key, modifiers = event.key(), event.modifiers() + if key == QtCore.Qt.Key_Escape and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): + self.hide() + elif key == QtCore.Qt.Key_C and modifiers & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): + rows = list(map(lambda x: self.messages.row(x), self.messages.selectedItems())) + indexes = (rows[0] - self.messages.count(), rows[-1] - self.messages.count()) + s = self._history_loader.export_history(self._contacts_manager.get_curr_friend(), True, indexes) + self.copy_text(s) + elif key == QtCore.Qt.Key_Z and modifiers & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): + self.messages.clearSelection() + elif key == QtCore.Qt.Key_F and modifiers & QtCore.Qt.ControlModifier: + self.show_search_field() + else: + super().keyPressEvent(event) + + # Functions which called when user click in menu + + def log_console(self): + self._me.show() + + def python_console(self): + if not PythonConsole: return + app = self._app + if app and app._settings: + size = app._settings['message_font_size'] + font_name = app._settings['font'] + else: + size = 12 + font_name = "Courier New" + + size = font_width = 10 + font_name = "DejaVu Sans Mono" + + try: + if not self._pe: + self._pe = PythonConsole(formats=aFORMATS) + self._pe.setWindowTitle('variable: app is the application') +# self._pe.edit.setStyleSheet('foreground: white; background-color: black;}') + # Fix the pyconsole geometry + + font = self._pe.edit.document().defaultFont() + font.setFamily(font_name) + font.setBold(True) + if font_width is None: + font_width = QFontMetrics(font).width('M') + self._pe.setFont(font) + geometry = self._pe.geometry() + geometry.setWidth(int(font_width*50+20)) + geometry.setHeight(int(font_width*24*13/8)) + self._pe.setGeometry(geometry) + self._pe.resize(int(font_width*50+20), int(font_width*24*13/8)) + + self._pe.show() + self._pe.eval_queued() + # or self._pe.eval_in_thread() + return + except Exception as e: + LOG.warn(f"python_console EXCEPTION {e}") + + def weechat_console(self): + if self._we: + self._we.show() + return + try: + from qweechat import qweechat + from qweechat.config import write + LOG.info("Loading WeechatConsole") + except ImportError as e: + LOG.error(f"ImportError Loading import qweechat {e} {sys.path}") + LOG.debug(traceback.print_exc()) + text = f"ImportError Loading import qweechat {e} {sys.path}" + title = util_ui.tr('Error importing qweechat') + util_ui.message_box(text, title) + return + + try: + # WeeChat backported from PySide6 to PyQt5 + LOG.info("Adding WeechatConsole") + class WeechatConsole(qweechat.MainWindow): + def __init__(self, *args): + qweechat.MainWindow.__init__(self, *args) + + def closeEvent(self, event): + """Called when QWeeChat window is closed.""" + self.network.disconnect_weechat() + if self.network.debug_dialog: + self.network.debug_dialog.close() + write(self.config) + except Exception as e: + LOG.exception(f"ERROR WeechatConsole {e}") + MainWindow = None + return + app = self._app + if app and app._settings: + size = app._settings['message_font_size'] + font_name = app._settings['font'] + else: + size = 12 + font_name = "Courier New" + + font_name = "DejaVu Sans Mono" + + try: + LOG.info("Creating WeechatConsole") + self._we = WeechatConsole() + self._we.show() + self._we.setWindowTitle('File/Connect to 127.0.0.1:9000') + # Fix the pyconsole geometry + try: + font = self._we.buffers[0].widget.chat.defaultFont() + font.setFamily(font_name) + font.setBold(True) + if font_width is None: + font_width = QFontMetrics(font).width('M') + self._we.setFont(font) + except Exception as e: +# LOG.debug(e) + font_width = size + geometry = self._we.geometry() + # make this configable? + geometry.setWidth(int(font_width*70)) + geometry.setHeight(int(font_width*(2+24)*11/8)) + self._we.setGeometry(geometry) + #? QtCore.QSize() + self._we.resize(int(font_width*80+20), int(font_width*(2+24)*11/8)) + + self._we.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Preferred) + self._we.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + + LOG.info("Showing WeechatConsole") + self._we.show() + # or self._we.eval_in_thread() + return + except Exception as e: + LOG.exception(f"Error creating WeechatConsole {e}") + + def about_program(self): + # TODO: replace with window + text = util_ui.tr('Toxygen is Tox client written in Python.\nVersion: ') + text += '' + '\nGitHub: https://git.plastiras.org/emdee/toxygen' + title = util_ui.tr('About') + util_ui.message_box(text, title) + + def network_settings(self): + self._modal_window = self._widget_factory.create_network_settings_window() + self._modal_window.show() + + def plugins_menu(self): + self._modal_window = self._widget_factory.create_plugins_settings_window() + self._modal_window.show() + + def add_contact_triggered(self, _): + self.add_contact() + + def add_contact(self, link=''): + self._modal_window = self._widget_factory.create_add_contact_window(link) + self._modal_window.show() + + def create_gc(self): + self._modal_window = self._widget_factory.create_group_screen_window() + self._modal_window.show() + + def join_gc(self): + self._modal_window = self._widget_factory.create_join_group_screen_window() + self._modal_window.show() + + def profile_settings(self, _): + self._modal_window = self._widget_factory.create_profile_settings_window() + self._modal_window.show() + + def privacy_settings(self): + self._modal_window = self._widget_factory.create_privacy_settings_window() + self._modal_window.show() + + def notification_settings(self): + self._modal_window = self._widget_factory.create_notification_settings_window() + self._modal_window.show() + + def interface_settings(self): + self._modal_window = self._widget_factory.create_interface_settings_window() + self._modal_window.show() + + def audio_settings(self): + self._modal_window = self._widget_factory.create_audio_settings_window() + self._modal_window.show() + + def video_settings(self): + self._modal_window = self._widget_factory.create_video_settings_window() + self._modal_window.show() + + def update_settings(self): + self._modal_window = self._widget_factory.create_update_settings_window() + self._modal_window.show() + + def reload_plugins(self): + if hasattr(self, '_plugin_loader') and self._plugin_loader is not None: + self._plugin_loader.reload() + + def reload_toxchat(self): + pass + + @staticmethod + def import_plugin(): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with plugins')) + if directory and os.path.isdir(directory): + src = directory + '/' + dest = util.get_plugins_directory() + util.copy(src, dest) + util_ui.message_box(util_ui.tr('Plugin will be loaded after restart'), util_ui.tr("Restart Toxygen")) + + def lock_app(self): + if self._toxes.has_password(): + self._settings.locked = True + self.hide() + else: + util_ui.message_box(util_ui.tr('Error. Profile password is not set.'), util_ui.tr("Cannot lock app")) + + def test_tox(self): + self._app._test_tox() + + def test_nmap(self): + self._app._test_nmap() + + def test_main(self): + self._app._test_main() + + def quit_program(self): + try: + self.close_window() + self._app._stop_app() + except KeyboardInterrupt: + pass + sys.stderr.write('sys.exit' +'\n') + # unreached? + sys.exit(0) + + def show_menu(self): + if not hasattr(self, 'menu'): + self.menu = DropdownMenu(self) + self.menu.setGeometry(QtCore.QRect(0 if self._settings['mirror_mode'] else 270, + self.height() - 120, + 180, + 120)) + self.menu.show() + + # Messages, calls and file transfers + + def send_message(self): + self._messenger.send_message() + + def send_file(self): + self.menu.hide() + if self._contacts_manager.is_active_a_friend(): + caption = util_ui.tr('Choose file') + name = util_ui.file_dialog(caption) + if name[0]: + self._file_transfer_handler.send_file(name[0], self._contacts_manager.get_active_number()) + + def send_screenshot(self, hide=False): + self.menu.hide() + if self._contacts_manager.is_active_a_friend(): + self.sw = self._widget_factory.create_screenshot_window(self) + self.sw.show() + if hide: + self.hide() + + def send_smiley(self): + self.menu.hide() + if self._contacts_manager.get_curr_contact() is None: + return + self._smiley_window = self._widget_factory.create_smiley_window(self) + rect = QtCore.QRect(self.menu.x(), + self.menu.y() - self.menu.height(), + self._smiley_window.width(), + self._smiley_window.height()) + self._smiley_window.setGeometry(rect) + self._smiley_window.show() + + def send_sticker(self): + self.menu.hide() + if self._contacts_manager.is_active_a_friend(): + self.sticker = self._widget_factory.create_sticker_window() + self.sticker.setGeometry(QtCore.QRect(self.x() if self._settings['mirror_mode'] else 270 + self.x(), + self.y() + self.height() - 200, + self.sticker.width(), + self.sticker.height())) + self.sticker.show() + + def active_call(self): + self.update_call_state('finish_call') + + def incoming_call(self): + self.update_call_state('incoming_call') + + def call_finished(self): + self.update_call_state('call') + + def update_call_state(self, state): + pixmap = QtGui.QPixmap(os.path.join(util.get_images_directory(), '{}.png'.format(state))) + icon = QtGui.QIcon(pixmap) + self.callButton.setIcon(icon) + self.callButton.setIconSize(QtCore.QSize(50, 50)) + + pixmap = QtGui.QPixmap(os.path.join(util.get_images_directory(), '{}_video.png'.format(state))) + icon = QtGui.QIcon(pixmap) + self.videocallButton.setIcon(icon) + self.videocallButton.setIconSize(QtCore.QSize(35, 35)) + + # Functions which called when user open context menu in friends list + + def _friend_right_click(self, pos): + item = self.friends_list.itemAt(pos) + number = self.friends_list.indexFromItem(item).row() + contact = self._contacts_manager.get_contact(number) + if contact is None or item is None: + return + generator = contact.get_context_menu_generator() + self.listMenu = generator.generate(self._plugins_loader, self._contacts_manager, self, self._settings, number, + self._groups_service, self._history_loader) + parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) + self.listMenu.move(parent_position + pos) + self.listMenu.show() + + def show_note(self, friend): + note = self._settings['notes'][friend.tox_id] if friend.tox_id in self._settings['notes'] else '' + user = util_ui.tr('Notes about user') + user = '{} {}'.format(user, friend.name) + + def save_note(text): + if friend.tox_id in self._settings['notes']: + del self._settings['notes'][friend.tox_id] + if text: + self._settings['notes'][friend.tox_id] = text + self._settings.save() + self.note = MultilineEdit(user, note, save_note) + self.note.show() + + def set_alias(self, num): + self._contacts_manager.set_alias(num) + + def remove_friend(self, num): + self._contacts_manager.delete_friend(num) + + def block_friend(self, num): + friend = self._contacts_manager.get_contact(num) + self._contacts_manager.block_user(friend.tox_id) + + @staticmethod + def copy_text(text): + util_ui.copy_to_clipboard(text) + + def auto_accept(self, num, value): + tox_id = self._contacts_manager.friend_public_key(num) + if value: + self._settings['auto_accept_from_friends'].append(tox_id) + else: + self._settings['auto_accept_from_friends'].remove(tox_id) + self._settings.save() + + def invite_friend_to_gc(self, friend_number, group_number): + self._contacts_manager.invite_friend(friend_number, group_number) + + def select_contact_row(self, row_index): + self.friends_list.setCurrentRow(row_index) + + # Functions which called when user click somewhere else + + def _selected_contact_changed(self): + num = self.friends_list.currentRow() + if self._contacts_manager.active_contact != num: + self._contacts_manager.active_contact = num + self.groupMenuButton.setVisible(self._contacts_manager.is_active_a_group()) + + def mouseReleaseEvent(self, event): + pos = self.connection_status.pos() + x, y = pos.x(), pos.y() + 25 + if (x < event.x() < x + 32) and (y < event.y() < y + 32): + self._profile.change_status() + else: + super().mouseReleaseEvent(event) + + def _filtering(self): + index = self.contactsFilterComboBox.currentIndex() + search_text = self.searchLineEdit.text() + self._contacts_manager.filtration_and_sorting(index, search_text) + + def show_search_field(self): + if hasattr(self, 'search_field') and self.search_field.isVisible(): + #? + self.search_field.show() + return + if not hasattr(self._contacts_manager, 'get_curr_friend') or \ + self._contacts_manager.get_curr_friend() is None: + #? return + pass + self.search_field = self._widget_factory.create_search_screen(self.messages) + x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40 + self.search_field.setGeometry(x, y, self.messages.width(), 40) + self.messages.setGeometry(x, self.messages.y(), self.messages.width(), self.messages.height() - 40) + if self._should_show_group_peers_list: + self.peers_list.setFixedHeight(self.peers_list.height() - 40) + self.search_field.show() + + def _toggle_gc_peers_list(self): + self._should_show_group_peers_list = not self._should_show_group_peers_list + self.resizeEvent() + if self._should_show_group_peers_list: + self._groups_service.generate_peers_list() + + def _new_contact_selected(self, _): + if self._should_show_group_peers_list: + self._toggle_gc_peers_list() + index = self.friends_list.currentRow() + if self._contacts_manager.active_contact != index: + self.friends_list.setCurrentRow(self._contacts_manager.active_contact) + self.resizeEvent() + + def _open_gc_invites_list(self): + self._modal_window = self._widget_factory.create_group_invites_window() + self._modal_window.show() + + def update_gc_invites_button_state(self): + invites_count = self._groups_service.group_invites_count + LOG.debug(f"update_gc_invites_button_state invites_count={invites_count}") + + # Fixme + self.groupInvitesPushButton.setVisible(True) # invites_count > 0 + text = util_ui.tr(f'{invites_count} new invites to group chats') + self.groupInvitesPushButton.setText(text) + self.resizeEvent() diff --git a/toxygen/ui/main_screen_widgets.py b/toxygen/ui/main_screen_widgets.py new file mode 100644 index 0000000..064cb09 --- /dev/null +++ b/toxygen/ui/main_screen_widgets.py @@ -0,0 +1,512 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import urllib +import re + +from qtpy import QtCore, QtGui, QtWidgets +from qtpy.QtCore import Signal + +from ui.widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit +import utils.util as util +import utils.ui as util_ui +from stickers.stickers import load_stickers + +import logging +LOG = logging.getLogger('app.'+'msw') + +class MessageArea(QtWidgets.QPlainTextEdit): + """User types messages here""" + + def __init__(self, parent, form): + super().__init__(parent) + self._messenger = None + self._contacts_manager = self._file_transfer_handler = None + self.parent = form + self.setAcceptDrops(True) + self._timer = QtCore.QTimer(self) + self._timer.timeout.connect(lambda: self._messenger.send_typing(False)) + + def set_dependencies(self, messenger, contacts_manager, file_transfer_handler): + self._messenger = messenger + self._contacts_manager = contacts_manager + self._file_transfer_handler = file_transfer_handler + + def keyPressEvent(self, event): + if event.matches(QtGui.QKeySequence.Paste): + mimeData = QtWidgets.QApplication.clipboard().mimeData() + if mimeData.hasUrls(): + for url in mimeData.urls(): + self.pasteEvent(url.toString()) + else: + self.pasteEvent() + + elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + modifiers = event.modifiers() + if modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier: + self.insertPlainText('\n') + else: + if self._timer.isActive(): + self._timer.stop() + try: + self._messenger.send_typing(False) + self._messenger.send_message() + except Exception as e: + LOG.error(f"keyPressEvent ERROR send_message to {self._messenger}") + util_ui.message_box(str(e), + util_ui.tr(f"keyPressEvent ERROR send_message to {self._messenger}")) + + elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): + self.appendPlainText(self._messenger.get_last_message()) + + elif event.key() == QtCore.Qt.Key_Tab and self._contacts_manager.is_active_a_group(): + text = self.toPlainText() + text_cursor = self.textCursor() + pos = text_cursor.position() + current_word = re.split(r"\s+", text[:pos])[-1] + start_index = text.rindex(current_word, 0, pos) + peer_name = self._contacts_manager.get_gc_peer_name(current_word) + self.setPlainText(text[:start_index] + peer_name + text[pos:]) + new_pos = start_index + len(peer_name) + text_cursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor) + self.setTextCursor(text_cursor) + else: + self._messenger.send_typing(True) + if self._timer.isActive(): + self._timer.stop() + self._timer.start(5000) + super().keyPressEvent(event) + + def contextMenuEvent(self, event): + menu = create_menu(self.createStandardContextMenu()) + menu.exec_(event.globalPos()) + del menu + + def dragEnterEvent(self, e): + e.accept() + + def dragMoveEvent(self, e): + e.accept() + + def dropEvent(self, e): + if e.mimeData().hasFormat('text/plain') or e.mimeData().hasFormat('text/html'): + e.accept() + self.pasteEvent(e.mimeData().text()) + elif e.mimeData().hasUrls(): + for url in e.mimeData().urls(): + self.pasteEvent(url.toString()) + e.accept() + else: + e.ignore() + + def pasteEvent(self, text=None): + text = text or QtWidgets.QApplication.clipboard().text() + if text.startswith('file://'): + if not self._contacts_manager.is_active_a_friend(): + return + friend_number = self._contacts_manager.get_active_number() + file_path = self._parse_file_path(text) + self._file_transfer_handler.send_file(file_path, friend_number) + else: + self.insertPlainText(text) + + @staticmethod + def _parse_file_path(file_name): + if file_name.endswith('\r\n'): + file_name = file_name[:-2] + file_name = urllib.parse.unquote(file_name) + + return file_name[8 if util.get_platform() == 'Windows' else 7:] + + +class ScreenShotWindow(RubberBandWindow): + + def __init__(self, file_transfer_handler, contacts_manager, *args): + super().__init__(*args) + self._file_transfer_handler = file_transfer_handler + self._contacts_manager = contacts_manager + + def closeEvent(self, *args): + if self.parent.isHidden(): + self.parent.show() + + def mouseReleaseEvent(self, event): + if self.rubberband.isVisible(): + self.rubberband.hide() + rect = self.rubberband.geometry() + if rect.width() and rect.height(): + screen = QtWidgets.QApplication.primaryScreen() + p = screen.grabWindow(0, + rect.x() + 4, + rect.y() + 4, + rect.width() - 8, + rect.height() - 8) + byte_array = QtCore.QByteArray() + buffer = QtCore.QBuffer(byte_array) + buffer.open(QtCore.QIODevice.WriteOnly) + p.save(buffer, 'PNG') + friend = self._contacts_manager.get_curr_contact() + self._file_transfer_handler.send_screenshot(bytes(byte_array.data()), friend.number) + self.close() + + +class SmileyWindow(QtWidgets.QWidget): + """ + Smiley selection window + """ + + def __init__(self, parent, smiley_loader): + super().__init__(parent) + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + self._parent = parent + self._data = smiley_loader.get_smileys() + + count = len(self._data) + if not count: + self.close() + + self._page_size = int(pow(count / 8, 0.5) + 1) * 8 # smileys per page + if count % self._page_size == 0: + self._page_count = count // self._page_size + else: + self._page_count = round(count / self._page_size + 0.5) + self._page = -1 + self._radio = [] + + for i in range(self._page_count): # pages - radio buttons + elem = QtWidgets.QRadioButton(self) + elem.setGeometry(QtCore.QRect(i * 20 + 5, 160, 20, 20)) + elem.clicked.connect(lambda c, t=i: self._checked(t)) + self._radio.append(elem) + + width = max(self._page_count * 20 + 30, (self._page_size + 5) * 8 // 10) + self.setMaximumSize(width, 200) + self.setMinimumSize(width, 200) + self._buttons = [] + + for i in range(self._page_size): # buttons with smileys + b = QtWidgets.QPushButton(self) + b.setGeometry(QtCore.QRect((i // 8) * 20 + 5, (i % 8) * 20, 20, 20)) + b.clicked.connect(lambda c, t=i: self._clicked(t)) + self._buttons.append(b) + self._checked(0) + + def leaveEvent(self, event): + self.close() + + def _checked(self, pos): # new page opened + self._radio[self._page].setChecked(False) + self._radio[pos].setChecked(True) + self._page = pos + start = self._page * self._page_size + for i in range(self._page_size): + try: + self._buttons[i].setVisible(True) + pixmap = QtGui.QPixmap(self._data[start + i][1]) + icon = QtGui.QIcon(pixmap) + self._buttons[i].setIcon(icon) + except: + self._buttons[i].setVisible(False) + + def _clicked(self, pos): # smiley selected + pos += self._page * self._page_size + smiley = self._data[pos][0] + self._parent.messageEdit.insertPlainText(smiley) + self.close() + + +class MenuButton(QtWidgets.QPushButton): + + def __init__(self, parent, enter): + super().__init__(parent) + self.enter = enter + + def enterEvent(self, event): + self.enter() + super().enterEvent(event) + + +class DropdownMenu(QtWidgets.QWidget): + + def __init__(self, parent): + super().__init__(parent) + self.installEventFilter(self) + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + self.setMaximumSize(120, 120) + self.setMinimumSize(120, 120) + self.screenshotButton = QRightClickButton(self) + self.screenshotButton.setGeometry(QtCore.QRect(0, 60, 60, 60)) + self.screenshotButton.setObjectName("screenshotButton") + + self.fileTransferButton = QtWidgets.QPushButton(self) + self.fileTransferButton.setGeometry(QtCore.QRect(60, 60, 60, 60)) + self.fileTransferButton.setObjectName("fileTransferButton") + + self.smileyButton = QtWidgets.QPushButton(self) + self.smileyButton.setGeometry(QtCore.QRect(0, 0, 60, 60)) + + self.stickerButton = QtWidgets.QPushButton(self) + self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60)) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'file.png')) + icon = QtGui.QIcon(pixmap) + self.fileTransferButton.setIcon(icon) + self.fileTransferButton.setIconSize(QtCore.QSize(50, 50)) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'screenshot.png')) + icon = QtGui.QIcon(pixmap) + self.screenshotButton.setIcon(icon) + self.screenshotButton.setIconSize(QtCore.QSize(50, 60)) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'smiley.png')) + icon = QtGui.QIcon(pixmap) + self.smileyButton.setIcon(icon) + self.smileyButton.setIconSize(QtCore.QSize(50, 50)) + + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'sticker.png')) + icon = QtGui.QIcon(pixmap) + self.stickerButton.setIcon(icon) + self.stickerButton.setIconSize(QtCore.QSize(55, 55)) + + self.screenshotButton.setToolTip(util_ui.tr("Send screenshot")) + self.fileTransferButton.setToolTip(util_ui.tr("Send file")) + self.smileyButton.setToolTip(util_ui.tr("Add smiley")) + self.stickerButton.setToolTip(util_ui.tr("Send sticker")) + + self.fileTransferButton.clicked.connect(parent.send_file) + self.screenshotButton.clicked.connect(parent.send_screenshot) + self.screenshotButton.rightClicked.connect(lambda: parent.send_screenshot(True)) + self.smileyButton.clicked.connect(parent.send_smiley) + self.stickerButton.clicked.connect(parent.send_sticker) + + def leaveEvent(self, event): + self.close() + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.WindowDeactivate: + self.close() + return False + + +class StickerItem(QtWidgets.QWidget): + + def __init__(self, fl): + super().__init__() + self._image_label = QtWidgets.QLabel(self) + self.path = fl + self.pixmap = QtGui.QPixmap() + self.pixmap.load(fl) + if self.pixmap.width() > 150: + self.pixmap = self.pixmap.scaled(150, 200, QtCore.Qt.KeepAspectRatio) + self.setFixedSize(150, self.pixmap.height()) + self._image_label.setPixmap(self.pixmap) + + +class StickerWindow(QtWidgets.QWidget): + """Sticker selection window""" + + def __init__(self, file_transfer_handler, contacts_manager): + super().__init__() + self._file_transfer_handler = file_transfer_handler + self._contacts_manager = contacts_manager + self.setWindowFlags(QtCore.Qt.FramelessWindowHint) + self.setMaximumSize(250, 200) + self.setMinimumSize(250, 200) + self.list = QtWidgets.QListWidget(self) + self.list.setGeometry(QtCore.QRect(0, 0, 250, 200)) + self._stickers = load_stickers() + for sticker in self._stickers: + item = StickerItem(sticker) + elem = QtWidgets.QListWidgetItem() + elem.setSizeHint(QtCore.QSize(250, item.height())) + self.list.addItem(elem) + self.list.setItemWidget(elem, item) + self.list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.list.setSpacing(3) + self.list.clicked.connect(self.click) + + def click(self, index): + num = index.row() + friend = self._contacts_manager.get_curr_contact() + self._file_transfer_handler.send_sticker(self._stickers[num], friend.number) + self.close() + + def leaveEvent(self, event): + self.close() + + +class WelcomeScreen(CenteredWidget): + + def __init__(self, settings): + super().__init__() + self._settings = settings + self.setMaximumSize(250, 200) + self.setMinimumSize(250, 200) + self.center() + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.text = QtWidgets.QTextBrowser(self) + self.text.setGeometry(QtCore.QRect(0, 0, 250, 170)) + self.text.setOpenExternalLinks(True) + self.checkbox = QtWidgets.QCheckBox(self) + self.checkbox.setGeometry(QtCore.QRect(5, 170, 240, 30)) + self.checkbox.setText(util_ui.tr( "Don't show again")) + self.setWindowTitle(util_ui.tr( 'Tip of the day')) + import random + num = random.randint(0, 10) + if num == 0: + text = util_ui.tr('Press Esc if you want hide app to tray.') + elif num == 1: + text = util_ui.tr('Right click on screenshot button hides app to tray during screenshot.') + elif num == 2: + text = util_ui.tr('You can use Tox over Tor. For more info read this post') + elif num == 3: + text = util_ui.tr('Use Settings -> Interface to customize interface.') + elif num == 4: + text = util_ui.tr('Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.') + elif num == 5: + text = util_ui.tr('Since v0.1.3 Toxygen supports plugins. Read more') + elif num == 6: + text = util_ui.tr('Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.') + elif num == 7: + text = util_ui.tr('New in Toxygen 0.4.1:
Downloading nodes from tox.chat
Bug fixes') + elif num == 8: + text = util_ui.tr('Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu') + elif num == 9: + text = util_ui.tr( 'Use right click on inline image to save it') + else: + text = util_ui.tr('Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.') + self.text.setHtml(text) + self.checkbox.stateChanged.connect(self.not_show) + QtCore.QTimer.singleShot(1000, self.show) + + def not_show(self): + self._settings['show_welcome_screen'] = False + self._settings.save() + + +class MainMenuButton(QtWidgets.QPushButton): + + def __init__(self, *args): + super().__init__(*args) + self.setObjectName("mainmenubutton") + + def setText(self, text): + metrics = QtGui.QFontMetrics(self.font()) + self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20) + super().setText(text) + + +class ClickableLabel(QtWidgets.QLabel): + # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' + clicked = Signal() + + def __init__(self, *args): + super().__init__(*args) + + def mouseReleaseEvent(self, ev): + self.clicked.emit() + + +class SearchScreen(QtWidgets.QWidget): + + def __init__(self, contacts_manager, history_loader, messages, width, *args): + super().__init__(*args) + self._contacts_manager = contacts_manager + self._history_loader = history_loader + self.setMaximumSize(width, 40) + self.setMinimumSize(width, 40) + self._messages = messages + + self.search_text = LineEdit(self) + self.search_text.setGeometry(0, 0, width - 160, 40) + + self.search_button = ClickableLabel(self) + self.search_button.setGeometry(width - 160, 0, 40, 40) + pixmap = QtGui.QPixmap() + pixmap.load(util.join_path(util.get_images_directory(), 'search.png')) + self.search_button.setScaledContents(False) + self.search_button.setAlignment(QtCore.Qt.AlignCenter) + self.search_button.setPixmap(pixmap) + self.search_button.clicked.connect(self.search) + + font = QtGui.QFont() + font.setPointSize(32) + font.setBold(True) + + self.prev_button = QtWidgets.QPushButton(self) + self.prev_button.setGeometry(width - 120, 0, 40, 40) + self.prev_button.clicked.connect(self.prev) + self.prev_button.setText('\u25B2') + + self.next_button = QtWidgets.QPushButton(self) + self.next_button.setGeometry(width - 80, 0, 40, 40) + self.next_button.clicked.connect(self.next) + self.next_button.setText('\u25BC') + + self.close_button = QtWidgets.QPushButton(self) + self.close_button.setGeometry(width - 40, 0, 40, 40) + self.close_button.clicked.connect(self.close) + self.close_button.setText('×') + self.close_button.setFont(font) + + font.setPointSize(18) + self.next_button.setFont(font) + self.prev_button.setFont(font) + + self.retranslateUi() + + def retranslateUi(self): + self.search_text.setPlaceholderText(util_ui.tr('Search')) + + def show(self): + super().show() + self.search_text.setFocus() + + def search(self): + self._contacts_manager.update() + text = self.search_text.text() + contact = self._contacts_manager.get_curr_contact() + if text and contact and util.is_re_valid(text): + index = contact.search_string(text) + self.load_messages(index) + + def prev(self): + contact = self._contacts_manager.get_curr_contact() + if contact is not None: + index = contact.search_prev() + self.load_messages(index) + + def next(self): + contact = self._contacts_manager.get_curr_contact() + text = self.search_text.text() + if contact is not None: + index = contact.search_next() + if index is not None: + count = self._messages.count() + index += count + item = self._messages.item(index) + self._messages.scrollToItem(item) + self._messages.itemWidget(item).select_text(text) + else: + self.not_found(text) + + def load_messages(self, index): + text = self.search_text.text() + if index is not None: + count = self._messages.count() + while count + index < 0: + self._history_loader.load_history() + count = self._messages.count() + index += count + item = self._messages.item(index) + self._messages.scrollToItem(item) + self._messages.itemWidget(item).select_text(text) + else: + self.not_found(text) + + def closeEvent(self, *args): + self._messages.setGeometry(0, 0, self._messages.width(), self._messages.height() + 40) + super().closeEvent(*args) + + @staticmethod + def not_found(text): + util_ui.message_box(util_ui.tr('Text "{}" was not found').format(text), util_ui.tr('Not found')) diff --git a/toxygen/ui/menu.py b/toxygen/ui/menu.py new file mode 100644 index 0000000..9f0fc05 --- /dev/null +++ b/toxygen/ui/menu.py @@ -0,0 +1,804 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtGui, QtWidgets, uic + +import toxygen_wrapper.tests.support_testing as ts +with ts.ignoreStderr(): # not out + import pyaudio + +from user_data.settings import * +from utils.util import * +from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow +import updater.updater as updater +import utils.ui as util_ui +from user_data import settings + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +global oPYA +oPYA = pyaudio.PyAudio() +class AddContact(CenteredWidget): + """Add contact form""" + + def __init__(self, dsettings, contacts_manager, tox_id=''): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + self._contacts_manager = contacts_manager + uic.loadUi(get_views_path('add_contact_screen'), self) + self._update_ui(tox_id) + self._adding = False + self._bootstrap = False + + def _update_ui(self, tox_id): + self.toxIdLineEdit = LineEdit(self) + self.toxIdLineEdit.setGeometry(QtCore.QRect(50, 40, 460, 30)) + self.toxIdLineEdit.setText(tox_id) + + self.messagePlainTextEdit.document().setPlainText(util_ui.tr('Hello! Please add me to your contact list.')) + self.addContactPushButton.clicked.connect(self._add_friend) + + # self.addBootstrapPushButton.clicked.connect(self._add_bootstrap) + self._retranslate_ui() + + def _add_bootstrap(self): + if self._bootstrap: + return + self._bootstrap = True + + def _add_friend(self): + if self._adding: + return + self._adding = True + tox_id = self.toxIdLineEdit.text().strip() + if tox_id.startswith('tox:'): + tox_id = tox_id[4:] + message = self.messagePlainTextEdit.toPlainText() + send = self._contacts_manager.send_friend_request(tox_id, message) + self._adding = False + if send is True: + # request was successful + pass + elif send and type(send) == str: # print error data + self.errorLabel.setText(send) + self.close() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Add contact')) + self.addContactPushButton.setText(util_ui.tr('Send request')) + self.toxIdLabel.setText(util_ui.tr('TOX ID:')) + self.messageLabel.setText(util_ui.tr('Message:')) + self.toxIdLineEdit.setPlaceholderText(util_ui.tr('TOX ID or public key of contact')) + +# unfinished copy of addContact +class AddBootstrap(CenteredWidget): + """Add bootstrap form""" + + def __init__(self, dsettings, bootstraps_manager, tox_id=''): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + self._bootstraps_manager = bootstraps_manager + uic.loadUi(get_views_path('add_bootstrap_screen'), self) + self._update_ui(tox_id) + self._adding = False + self._bootstrap = False + + def _update_ui(self, tox_id): + self.toxIdLineEdit = LineEdit(self) + self.toxIdLineEdit.setGeometry(QtCore.QRect(50, 40, 460, 30)) + self.toxIdLineEdit.setText(tox_id) + + self.messagePlainTextEdit.document().setPlainText(util_ui.tr('Hello! Please add me to your bootstrap list.')) + self.addBootstrapPushButton.clicked.connect(self._add_friend) + + # self.addBootstrapPushButton.clicked.connect(self._add_bootstrap) + self._retranslate_ui() + + def _add_bootstrap(self): + if self._bootstrap: + return + self._bootstrap = True + tox_id = self.toxIdLineEdit.text().strip() + if tox_id.startswith('tox:'): + tox_id = tox_id[4:] + message = self.messagePlainTextEdit.toPlainText() + send = self._bootstraps_manager.send_friend_request(tox_id, message) + self._adding = False + if send is True: + # request was successful + self.close() + else: # print error data + self.errorLabel.setText(send) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Add bootstrap')) + self.addBootstrapPushButton.setText(util_ui.tr('Send request')) + self.toxIdLabel.setText(util_ui.tr('Port:')) + self.messageLabel.setText(util_ui.tr('Message:')) + self.toxIdLineEdit.setPlaceholderText(util_ui.tr('IP or hostname of public key of bootstrap')) + + +class NetworkSettings(CenteredWidget): + """Network settings form: UDP, Ipv6 and proxy""" + def __init__(self, dsettings, reset): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + self._reset = reset + uic.loadUi(get_views_path('network_settings_screen'), self) + self._update_ui() + + def _update_ui(self): + self.ipLineEdit = LineEdit(self) + self.ipLineEdit.setGeometry(100, 280, 270, 30) + + self.portLineEdit = LineEdit(self) + self.portLineEdit.setGeometry(100, 325, 270, 30) + + self.urlLineEdit = LineEdit(self) + self.urlLineEdit.setGeometry(100, 370, 270, 30) + + self.restartCorePushButton.clicked.connect(self._restart_core) + self.ipv6CheckBox.setChecked(self._settings['ipv6_enabled']) + self.udpCheckBox.setChecked(self._settings['udp_enabled']) + self.proxyCheckBox.setChecked(self._settings['proxy_type']) + self.ipLineEdit.setText(self._settings['proxy_host']) + self.portLineEdit.setText(str(self._settings['proxy_port'])) + self.urlLineEdit.setText(str(self._settings['download_nodes_url'])) + self.httpProxyRadioButton.setChecked(self._settings['proxy_type'] == 1) + self.socksProxyRadioButton.setChecked(self._settings['proxy_type'] != 1) + self.downloadNodesCheckBox.setChecked(self._settings['download_nodes_list']) + self.lanCheckBox.setChecked(self._settings['local_discovery_enabled']) + self._retranslate_ui() + self.proxyCheckBox.stateChanged.connect(lambda x: self._activate_proxy()) + self._activate_proxy() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Network settings")) + self.ipv6CheckBox.setText(util_ui.tr("IPv6")) + self.udpCheckBox.setText(util_ui.tr("UDP")) + self.lanCheckBox.setText(util_ui.tr("LAN")) + self.proxyCheckBox.setText(util_ui.tr("Proxy")) + self.ipLabel.setText(util_ui.tr("IP:")) + self.portLabel.setText(util_ui.tr("Port:")) + self.urlLabel.setText(util_ui.tr("ChatUrl:")) + self.restartCorePushButton.setText(util_ui.tr("Restart TOX core")) + self.httpProxyRadioButton.setText(util_ui.tr("HTTP")) + self.socksProxyRadioButton.setText(util_ui.tr("Socks 5")) + self.downloadNodesCheckBox.setText(util_ui.tr("Download nodes list from tox.chat")) +# self.warningLabel.setText(util_ui.tr("WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) + self.warningLabel.setText(util_ui.tr("Changing settings require 'Restart TOX core'")) + + def _activate_proxy(self): + bl = self.proxyCheckBox.isChecked() + self.ipLineEdit.setEnabled(bl) + self.portLineEdit.setEnabled(bl) + self.httpProxyRadioButton.setEnabled(bl) + self.socksProxyRadioButton.setEnabled(bl) + self.ipLabel.setEnabled(bl) + self.portLabel.setEnabled(bl) + + def _restart_core(self): + try: + self._settings['ipv6_enabled'] = self.ipv6CheckBox.isChecked() + self._settings['udp_enabled'] = self.udpCheckBox.isChecked() + proxy_enabled = self.proxyCheckBox.isChecked() + self._settings['proxy_type'] = 2 - int(self.httpProxyRadioButton.isChecked()) if proxy_enabled else 0 + self._settings['proxy_host'] = str(self.ipLineEdit.text()) + self._settings['proxy_port'] = int(self.portLineEdit.text()) + self._settings['download_nodes_url'] = str(self.urlLineEdit.text()) + self._settings['download_nodes_list'] = self.downloadNodesCheckBox.isChecked() + self._settings['local_discovery_enabled'] = self.lanCheckBox.isChecked() + self._settings.save() + # recreate tox instance + self._reset() + self.close() + except Exception as ex: + LOG.error('ERROR: Exception in restart: ' + str(ex)) + + +class PrivacySettings(CenteredWidget): + """Privacy settings form: history, typing notifications""" + + def __init__(self, contacts_manager, dsettings): + """ + :type contacts_manager: ContactsManager + """ + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._contacts_manager = contacts_manager + self._settings = dsettings + self.initUI() + self.center() + + def initUI(self): + self.setObjectName("privacySettings") + self.resize(370, 600) + self.setMinimumSize(QtCore.QSize(370, 600)) + self.setMaximumSize(QtCore.QSize(370, 600)) + self.saveHistory = QtWidgets.QCheckBox(self) + self.saveHistory.setGeometry(QtCore.QRect(10, 20, 350, 22)) + self.saveUnsentOnly = QtWidgets.QCheckBox(self) + self.saveUnsentOnly.setGeometry(QtCore.QRect(10, 60, 350, 22)) + + self.fileautoaccept = QtWidgets.QCheckBox(self) + self.fileautoaccept.setGeometry(QtCore.QRect(10, 100, 350, 22)) + + self.typingNotifications = QtWidgets.QCheckBox(self) + self.typingNotifications.setGeometry(QtCore.QRect(10, 140, 350, 30)) + self.inlines = QtWidgets.QCheckBox(self) + self.inlines.setGeometry(QtCore.QRect(10, 180, 350, 30)) + self.auto_path = QtWidgets.QLabel(self) + self.auto_path.setGeometry(QtCore.QRect(10, 230, 350, 30)) + self.path = QtWidgets.QPlainTextEdit(self) + self.path.setGeometry(QtCore.QRect(10, 265, 350, 45)) + self.change_path = QtWidgets.QPushButton(self) + self.change_path.setGeometry(QtCore.QRect(10, 320, 350, 30)) + self.typingNotifications.setChecked(self._settings['typing_notifications']) + self.fileautoaccept.setChecked(self._settings['allow_auto_accept']) + self.saveHistory.setChecked(self._settings['save_history']) + self.inlines.setChecked(self._settings['allow_inline']) + self.saveUnsentOnly.setChecked(self._settings['save_unsent_only']) + self.saveUnsentOnly.setEnabled(self._settings['save_history']) + self.saveHistory.stateChanged.connect(self.update) + self.path.setPlainText(self._settings['auto_accept_path'] or curr_directory()) + self.change_path.clicked.connect(self.new_path) + self.block_user_label = QtWidgets.QLabel(self) + self.block_user_label.setGeometry(QtCore.QRect(10, 360, 350, 30)) + self.block_id = QtWidgets.QPlainTextEdit(self) + self.block_id.setGeometry(QtCore.QRect(10, 390, 350, 30)) + self.block = QtWidgets.QPushButton(self) + self.block.setGeometry(QtCore.QRect(10, 430, 350, 30)) + self.block.clicked.connect(lambda: self._contacts_manager.block_user(self.block_id.toPlainText()) or self.close()) + self.blocked_users_label = QtWidgets.QLabel(self) + self.blocked_users_label.setGeometry(QtCore.QRect(10, 470, 350, 30)) + self.comboBox = QtWidgets.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(10, 500, 350, 30)) + self.comboBox.addItems(self._settings['blocked']) + self.unblock = QtWidgets.QPushButton(self) + self.unblock.setGeometry(QtCore.QRect(10, 540, 350, 30)) + self.unblock.clicked.connect(lambda: self.unblock_user()) + self.retranslateUi() + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle(util_ui.tr("Privacy settings")) + self.saveHistory.setText(util_ui.tr("Save chat history")) + self.fileautoaccept.setText(util_ui.tr("Allow file auto accept")) + self.typingNotifications.setText(util_ui.tr("Send typing notifications")) + self.auto_path.setText(util_ui.tr("Auto accept default path:")) + self.change_path.setText(util_ui.tr("Change")) + self.inlines.setText(util_ui.tr("Allow inlines")) + self.block_user_label.setText(util_ui.tr("Block by public key:")) + self.blocked_users_label.setText(util_ui.tr("Blocked users:")) + self.unblock.setText(util_ui.tr("Unblock")) + self.block.setText(util_ui.tr("Block user")) + self.saveUnsentOnly.setText(util_ui.tr("Save unsent messages only")) + + def update(self, new_state): + self.saveUnsentOnly.setEnabled(new_state) + if not new_state: + self.saveUnsentOnly.setChecked(False) + + def unblock_user(self): + if not self.comboBox.count(): + return + title = util_ui.tr("Add to friend list") + info = util_ui.tr("Do you want to add this user to friend list?") + reply = util_ui.question(info, title) + self._contacts_manager.unblock_user(self.comboBox.currentText(), reply) + self.close() + + def closeEvent(self, event): + self._settings['typing_notifications'] = self.typingNotifications.isChecked() + self._settings['allow_auto_accept'] = self.fileautoaccept.isChecked() + text = util_ui.tr('History will be cleaned! Continue?') + title = util_ui.tr('Chat history') + + if self._settings['save_history'] and not self.saveHistory.isChecked(): # clear history + reply = util_ui.question(text, title) + if reply: + self._history_loader.clear_history() + self._settings['save_history'] = self.saveHistory.isChecked() + else: + self._settings['save_history'] = self.saveHistory.isChecked() + if self.saveUnsentOnly.isChecked() and not self._settings['save_unsent_only']: + reply = util_ui.question(text, title) + if reply: + self._history_loader.clear_history(None, True) + self._settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() + else: + self._settings['save_unsent_only'] = self.saveUnsentOnly.isChecked() + self._settings['auto_accept_path'] = self.path.toPlainText() + self._settings['allow_inline'] = self.inlines.isChecked() + self._settings.save() + + def new_path(self): + directory = util_ui.directory_dialog() + if directory: + self.path.setPlainText(directory) + + +class NotificationsSettings(CenteredWidget): + """Notifications settings form""" + + def __init__(self, dsettings): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings # pylint: disable=undefined-variable + uic.loadUi(get_views_path('notifications_settings_screen'), self) + self._update_ui() + self.center() + + def closeEvent(self, *args, **kwargs): + self._settings['notifications'] = self.notificationsCheckBox.isChecked() + self._settings['sound_notifications'] = self.soundNotificationsCheckBox.isChecked() + self._settings['group_notifications'] = self.groupNotificationsCheckBox.isChecked() + self._settings['calls_sound'] = self.callsSoundCheckBox.isChecked() + self._settings.save() + + def _update_ui(self): + self.notificationsCheckBox.setChecked(self._settings['notifications']) + self.soundNotificationsCheckBox.setChecked(self._settings['sound_notifications']) + self.groupNotificationsCheckBox.setChecked(self._settings['group_notifications']) + self.callsSoundCheckBox.setChecked(self._settings['calls_sound']) + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Notifications settings")) + self.notificationsCheckBox.setText(util_ui.tr("Enable notifications")) + self.groupNotificationsCheckBox.setText(util_ui.tr("Notify about all messages in groups")) + self.callsSoundCheckBox.setText(util_ui.tr("Enable call\'s sound")) + self.soundNotificationsCheckBox.setText(util_ui.tr("Enable sound notifications")) + + +class InterfaceSettings(CenteredWidget): + """Interface settings form""" + + def __init__(self, dsettings, smiley_loader): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + self._smiley_loader = smiley_loader + + uic.loadUi(get_views_path('interface_settings_screen'), self) + self._update_ui() + self.center() + + def _update_ui(self): + themes = list(settings.built_in_themes().keys()) + self.themeComboBox.addItems(themes) + theme = self._settings['theme'] + if theme in settings.built_in_themes().keys(): + index = themes.index(theme) + else: + index = 0 + self.themeComboBox.setCurrentIndex(index) + + supported_languages = sorted(Settings.supported_languages().keys(), reverse=True) + for key in supported_languages: + self.languageComboBox.insertItem(0, key) + if self._settings['language'] == key: + self.languageComboBox.setCurrentIndex(0) + + smiley_packs = self._smiley_loader.get_packs_list() + self.smileysPackComboBox.addItems(smiley_packs) + try: + index = smiley_packs.index(self._settings['smiley_pack']) + except: + index = smiley_packs.index('default') + self.smileysPackComboBox.setCurrentIndex(index) + + self._app_closing_setting = self._settings['close_app'] + self.closeRadioButton.setChecked(self._app_closing_setting == 0) + self.hideRadioButton.setChecked(self._app_closing_setting == 1) + self.closeToTrayRadioButton.setChecked(self._app_closing_setting == 2) + + self.compactModeCheckBox.setChecked(self._settings['compact_mode']) + self.showAvatarsCheckBox.setChecked(self._settings['show_avatars']) + self.smileysCheckBox.setChecked(self._settings['smileys']) + + self.importSmileysPushButton.clicked.connect(self._import_smileys) + self.importStickersPushButton.clicked.connect(self._import_stickers) + + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Interface settings")) + self.showAvatarsCheckBox.setText(util_ui.tr("Show avatars in chat")) + self.themeLabel.setText(util_ui.tr("Theme:")) + self.languageLabel.setText(util_ui.tr("Language:")) + self.smileysGroupBox.setTitle(util_ui.tr("Smileys settings")) + self.smileysPackLabel.setText(util_ui.tr("Smiley pack:")) + self.smileysCheckBox.setText(util_ui.tr("Smileys")) + self.closeRadioButton.setText(util_ui.tr("Close app")) + self.hideRadioButton.setText(util_ui.tr("Hide app")) + self.closeToTrayRadioButton.setText(util_ui.tr("Close to tray")) +# self.mirrorModeCheckBox.setText(util_ui.tr("Mirror mode")) + self.compactModeCheckBox.setText(util_ui.tr("Compact contact list")) + self.importSmileysPushButton.setText(util_ui.tr("Import smiley pack")) + self.importStickersPushButton.setText(util_ui.tr("Import sticker pack")) + self.appClosingGroupBox.setTitle(util_ui.tr("App closing settings")) + + @staticmethod + def _import_stickers(): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with sticker pack')) + if directory: + dest = join_path(get_stickers_directory(), os.path.basename(directory)) + copy(directory, dest) + + @staticmethod + def _import_smileys(): + directory = util_ui.directory_dialog(util_ui.tr('Choose folder with smiley pack')) + if not directory: + return + src = directory + '/' + dest = join_path(get_smileys_directory(), os.path.basename(directory)) + copy(src, dest) + + def closeEvent(self, event): + + self._settings['theme'] = str(self.themeComboBox.currentText()) + try: + theme = self._settings['theme'] + styles_path = join_path(get_styles_directory(), settings.built_in_themes()[theme]) + with open(styles_path) as fl: + style = fl.read() + self._app.setStyleSheet(style) + except IsADirectoryError: + pass + + self._settings['smileys'] = self.smileysCheckBox.isChecked() + + restart = False +# if self._settings['mirror_mode'] != self.mirrorModeCheckBox.isChecked(): +# self._settings['mirror_mode'] = self.mirrorModeCheckBox.isChecked() +# restart = True + + if self._settings['compact_mode'] != self.compactModeCheckBox.isChecked(): + self._settings['compact_mode'] = self.compactModeCheckBox.isChecked() + restart = True + + if self._settings['show_avatars'] != self.showAvatarsCheckBox.isChecked(): + self._settings['show_avatars'] = self.showAvatarsCheckBox.isChecked() + restart = True + + self._settings['smiley_pack'] = self.smileysPackComboBox.currentText() + self._smiley_loader.load_pack() + + language = self.languageComboBox.currentText() + if self._settings['language'] != language: + self._settings['language'] = language + path = Settings.supported_languages()[language] + self._app.removeTranslator(self._app.translator) + self._app.translator.load(join_path(get_translations_directory(), path)) + self._app.installTranslator(self._app.translator) + + app_closing_setting = 0 + if self.hideRadioButton.isChecked(): + app_closing_setting = 1 + elif self.closeToTrayRadioButton.isChecked(): + app_closing_setting = 2 + self._settings['close_app'] = app_closing_setting + self._settings.save() + + if restart: + util_ui.message_box(util_ui.tr('Restart app to apply settings'), util_ui.tr('Restart required')) + + +class AudioSettings(CenteredWidget): + """ + Audio calls settings form + """ + + def __init__(self, dsettings): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + self._in_indexes = self._out_indexes = None + uic.loadUi(get_views_path('audio_settings_screen'), self) + self._update_ui() + self.center() + + def closeEvent(self, event): + if 'audio' not in self._settings: + ex = f"self._settings=id(self._settings) {self._settings}" + LOG.warn('AudioSettings.closeEvent settings error: ' + str(ex)) + else: + self._settings['audio']['input'] = \ + self._in_indexes[self.inputDeviceComboBox.currentIndex()] + self._settings['audio']['output'] = \ + self._out_indexes[self.outputDeviceComboBox.currentIndex()] + self._settings.save() + + def _update_ui(self): + p = oPYA + self._in_indexes, self._out_indexes = [], [] + for i in range(p.get_device_count()): + device = p.get_device_info_by_index(i) + if device["maxInputChannels"]: + self.inputDeviceComboBox.addItem(str(device["name"])) + self._in_indexes.append(i) + if device["maxOutputChannels"]: + self.outputDeviceComboBox.addItem(str(device["name"])) + self._out_indexes.append(i) + try: + self.inputDeviceComboBox.setCurrentIndex(self._in_indexes.index(self._settings['audio']['input'])) + self.outputDeviceComboBox.setCurrentIndex(self._out_indexes.index(self._settings['audio']['output'])) + except: pass + + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Audio settings")) + self.inputDeviceLabel.setText(util_ui.tr("Input device:")) + self.outputDeviceLabel.setText(util_ui.tr("Output device:")) + + +class DesktopAreaSelectionWindow(RubberBandWindow): + + def mouseReleaseEvent(self, event): + if self.rubberband.isVisible(): + self.rubberband.hide() + rect = self.rubberband.geometry() + width, height = rect.width(), rect.height() + if width >= 8 and height >= 8: + self.parent.save(rect.x(), rect.y(), width - (width % 4), height - (height % 4)) + self.close() + + +class VideoSettings(CenteredWidget): + """ + Video calls settings form + """ + + def __init__(self, dsettings): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + uic.loadUi(get_views_path('video_settings_screen'), self) + self._devices = self._frame_max_sizes = None + self._update_ui() + self.center() + self.desktopAreaSelection = None + + def closeEvent(self, event): + if self.deviceComboBox.currentIndex() == 0: + return + try: + # AttributeError: 'VideoSettings' object has no attribute 'devices' + # ERROR: Saving video settings error: 'VideoSettings' object has no attribute 'input' + index = self.deviceComboBox.currentIndex() + if index in self._devices: + self._settings['video']['device'] = self._devices[index] + else: + LOG.warn(f"{index} not in deviceComboBox self._devices {self._devices}") + text = self.resolutionComboBox.currentText() + if len(text.split(' ')[0]) > 1: + self._settings['video']['width'] = int(text.split(' ')[0]) + self._settings['video']['height'] = int(text.split(' ')[-1]) + self._settings.save() + except Exception as ex: + LOG.error('ERROR: Saving video settings error: ' + str(ex)) + + def save(self, x, y, width, height): + self.desktopAreaSelection = None + self._settings['video']['device'] = -1 + self._settings['video']['width'] = width + self._settings['video']['height'] = height + self._settings['video']['x'] = x + self._settings['video']['y'] = y + self._settings.save() + + def _update_ui(self): + try: + with ts.ignoreStdout(): import cv2 + except ImportError: + cv2 = None + self.deviceComboBox.currentIndexChanged.connect(self._device_changed) + self.selectRegionPushButton.clicked.connect(self._button_clicked) + self._devices = [-1] + screen = QtWidgets.QApplication.primaryScreen() + size = screen.size() + self._frame_max_sizes = [(size.width(), size.height())] + desktop = util_ui.tr("Desktop") + self.deviceComboBox.addItem(desktop) + with ts.ignoreStdout(): + # was range(10) + for i in map(int, ts.get_video_indexes()): + v = cv2.VideoCapture(i) # pylint: disable=no-member + if v.isOpened(): + v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000) # pylint: disable=no-member + v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000) # pylint: disable=no-member + + width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH)) # pylint: disable=no-member + height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT)) # pylint: disable=no-member + del v + self._devices.append(i) + self._frame_max_sizes.append((width, height)) + self.deviceComboBox.addItem(util_ui.tr('Device #') + str(i)) + + if 'device' not in self._settings['video']: + LOG.warn(f"'device' not in self._settings['video']: {self._settings}") + self._settings['video']['device'] = self._devices[-1] + iIndex = self._settings['video']['device'] + try: + index = self._devices.index(iIndex) + self.deviceComboBox.setCurrentIndex(index) + except Exception as e: + # off by one - what's Desktop? + se = f"Video devices index error: index={iIndex} {e}" + LOG.warn(se) + # util_ui.message_box(se, util_ui.tr(f"ERROR: Video devices error")) + self._settings['video']['device'] = self._devices[-1] + + self._retranslate_ui() + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Video settings")) + self.deviceLabel.setText(util_ui.tr("Device:")) + self.selectRegionPushButton.setText(util_ui.tr("Select region")) + + def _button_clicked(self): + self.desktopAreaSelection = DesktopAreaSelectionWindow(self) + + def _device_changed(self): + index = self.deviceComboBox.currentIndex() + self.selectRegionPushButton.setVisible(index == 0) + self.resolutionComboBox.setVisible(True) # index != 0 + width, height = self._frame_max_sizes[index] + self.resolutionComboBox.clear() + dims = [ + (320, 240), + (640, 360), + (640, 480), + (720, 480), + (1280, 720), + (1920, 1080), + (2560, 1440) + ] + for w, h in dims: + if w <= width and h <= height: + self.resolutionComboBox.addItem(str(w) + ' * ' + str(h)) + + +class PluginsSettings(CenteredWidget): + """ + Plugins settings form + """ + + def __init__(self, plugin_loader): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._plugin_loader = plugin_loader + self._window = None + self.initUI() + self.center() + self.retranslateUi() + + def initUI(self): + self.resize(400, 210) + self.setMinimumSize(QtCore.QSize(400, 210)) + self.setMaximumSize(QtCore.QSize(400, 210)) + self.comboBox = QtWidgets.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30)) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(30, 40, 340, 90)) + self.label.setWordWrap(True) + self.button = QtWidgets.QPushButton(self) + self.button.setGeometry(QtCore.QRect(30, 130, 340, 30)) + self.button.clicked.connect(self.button_click) + self.open = QtWidgets.QPushButton(self) + self.open.setGeometry(QtCore.QRect(30, 170, 340, 30)) + self.open.clicked.connect(self.open_plugin) + self.update_list() + self.comboBox.currentIndexChanged.connect(self.show_data) + self.show_data() + + def retranslateUi(self): + self.setWindowTitle(util_ui.tr("Plugins")) + self.open.setText(util_ui.tr("Open selected plugin")) + + def open_plugin(self): + + ind = self.comboBox.currentIndex() + plugin = self.data[ind] # ['SearchPlugin', True, 'Description', 'srch'] + # key in self._plugins and hasattr(self._plugins[key], 'instance'): + window = self._plugin_loader.plugin_window(plugin[-1]) + if window is not None and not hasattr(window, 'show'): + LOG.error(util_ui.tr('ERROR: No show for the plugin: ' +repr(window) +' ' +repr(window))) + util_ui.message_box(util_ui.tr('ERROR: No show for the plugin ' +repr(window)), util_ui.tr('Error')) + elif window is not None: + try: + self._window = window + self._window.show() + except Exception as e: + LOG.error(util_ui.tr('ERROR: Error for the plugin: ' +repr(window) +' ' +str(e))) + util_ui.message_box(util_ui.tr('ERROR: Error for the plugin: ' +repr(window)), util_ui.tr('Error')) + elif window is None: + LOG.warn(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window')) + util_ui.message_box(util_ui.tr('WARN: No GUI found for the plugin: by plugin_loader.plugin_window'), util_ui.tr('Error')) + + def update_list(self): + self.comboBox.clear() + data = self._plugin_loader.get_plugins_list() + self.comboBox.addItems(list(map(lambda x: x[0], data))) + self.data = data + + def show_data(self): + ind = self.comboBox.currentIndex() + if len(self.data): + plugin = self.data[ind] + descr = plugin[2] or util_ui.tr("No description available") + self.label.setText(descr) + if plugin[1]: + self.button.setText(util_ui.tr("Disable plugin")) + else: + self.button.setText(util_ui.tr("Enable plugin")) + else: + self.open.setVisible(False) + self.button.setVisible(False) + self.label.setText(util_ui.tr("No plugins found")) + + def button_click(self): + ind = self.comboBox.currentIndex() + plugin = self.data[ind] + self._plugin_loader.toggle_plugin(plugin[-1]) + plugin[1] = not plugin[1] + if plugin[1]: + self.button.setText(util_ui.tr("Disable plugin")) + else: + self.button.setText(util_ui.tr("Enable plugin")) + + +class UpdateSettings(CenteredWidget): + """ + Updates settings form + """ + + def __init__(self, dsettings, version): + super().__init__() + self._app = QtWidgets.QApplication.instance() + self._settings = dsettings + self._version = version + uic.loadUi(get_views_path('update_settings_screen'), self) + self._update_ui() + self.center() + + def closeEvent(self, event): + self._settings['update'] = self.updateModeComboBox.currentIndex() + self._settings.save() + + def _update_ui(self): + self.updatePushButton.clicked.connect(self._update_client) + self.updateModeComboBox.currentIndexChanged.connect(self._update_mode_changed) + self._retranslate_ui() + self.updateModeComboBox.setCurrentIndex(self._settings['update']) + + def _update_mode_changed(self): + index = self.updateModeComboBox.currentIndex() + self.updatePushButton.setEnabled(index > 0) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Update settings")) + self.updateModeLabel.setText(util_ui.tr("Select update mode:")) + self.updatePushButton.setText(util_ui.tr("Update Toxygen")) + self.updateModeComboBox.addItem(util_ui.tr("Disabled")) + self.updateModeComboBox.addItem(util_ui.tr("Manual")) + self.updateModeComboBox.addItem(util_ui.tr("Auto")) + + def _update_client(self): + if not updater.connection_available(): + util_ui.message_box(util_ui.tr('Problems with internet connection'), util_ui.tr("Error")) + return + if not updater.updater_available(): + util_ui.message_box(util_ui.tr('Updater not found'), util_ui.tr("Error")) + return + version = updater.check_for_updates(self._version, self._settings) + if version is not None: + updater.download(version) + util_ui.close_all_windows() + else: + util_ui.message_box(util_ui.tr('Toxygen is up to date'), util_ui.tr("No updates found")) diff --git a/toxygen/ui/messages_widgets.py b/toxygen/ui/messages_widgets.py new file mode 100644 index 0000000..2c0ddfb --- /dev/null +++ b/toxygen/ui/messages_widgets.py @@ -0,0 +1,463 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import html as h +import re + +from qtpy import QtCore, QtGui, QtWidgets + +from toxygen_wrapper.toxcore_enums_and_consts import * +import ui.widgets as widgets +import utils.util as util +import ui.menu as menu +from ui.widgets import * +from messenger.messages import MESSAGE_AUTHOR +from file_transfers.file_transfers import * + +class MessageBrowser(QtWidgets.QTextBrowser): + + def __init__(self, settings, message_edit, smileys_loader, plugin_loader, text, width, message_type, parent=None): + super().__init__(parent) + self.urls = {} + self._message_edit = message_edit + self._smileys_loader = smileys_loader + self._plugin_loader = plugin_loader + self._add_contact = None + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere) + self.document().setTextWidth(width) + self.setOpenExternalLinks(True) + self.setAcceptRichText(True) + self.setOpenLinks(False) + path = smileys_loader.get_smileys_path() + if path is not None: + self.setSearchPaths([path]) + self.document().setDefaultStyleSheet('a { color: #306EFF; }') + text = self.decoratedText(text) + if message_type != TOX_MESSAGE_TYPE['NORMAL']: + self.setHtml('

' + text + '

') + else: + self.setHtml(text) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPixelSize(settings['message_font_size']) + font.setBold(False) + self.setFont(font) + try: + # was self.resize(width, self.document().size().height()) + # guessing QSize + self.resize(QtCore.QSize(width, int(self.document().size().height()))) + except TypeError as e: + # TypeError: arguments did not match any overloaded call: + # resize(self, a0: QSize): argument 1 has unexpected type 'int' + # resize(self, w: int, h: int): argument 2 has unexpected type 'float' + pass + + self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) + self.anchorClicked.connect(self.on_anchor_clicked) + + def contextMenuEvent(self, event): + menu = widgets.create_menu(self.createStandardContextMenu(event.pos())) + quote = menu.addAction(util_ui.tr('Quote selected text')) + quote.triggered.connect(self.quote_text) + text = self.textCursor().selection().toPlainText() + if not text: + quote.setEnabled(False) + else: + sub_menu = self._plugin_loader.get_message_menu(menu, text) + if len(sub_menu): + plugins_menu = menu.addMenu(util_ui.tr('Plugins')) + plugins_menu.addActions(sub_menu) + menu.popup(event.globalPos()) + menu.exec_(event.globalPos()) + del menu + + def quote_text(self): + text = self.textCursor().selection().toPlainText() + if not text: + return + text = '>' + '\n>'.join(text.split('\n')) + if self._message_edit.toPlainText(): + text = '\n' + text + self._message_edit.appendPlainText(text) + + def on_anchor_clicked(self, url): + text = str(url.toString()) + if text.startswith('tox:'): + self._add_contact = menu.AddContact(text[4:]) + self._add_contact.show() + else: + QtGui.QDesktopServices.openUrl(url) + self.clearFocus() + + def addAnimation(self, url, file_name): + movie = QtGui.QMovie(self) + movie.setFileName(file_name) + self.urls[movie] = url + # Value 'movie.frameChanged' is unsubscriptable + movie.frameChanged().connect(lambda x: self.animate(movie)) + movie.start() + + def animate(self, movie): + self.document().addResource(QtGui.QTextDocument.ImageResource, + self.urls[movie], + movie.currentPixmap()) + self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth()) + + def decoratedText(self, text): + text = h.escape(text) # replace < and > + exp = QtCore.QRegExp( + '(' + '(?:\\b)((www\\.)|(http[s]?|ftp)://)' + '\\w+\\S+)' + '|(?:\\b)(file:///)([\\S| ]*)' + '|(?:\\b)(tox:[a-zA-Z\\d]{76}$)' + '|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)' + '|(?:\\b)(tox:\\S+@\\S+)') + offset = exp.indexIn(text, 0) + while offset != -1: # add links + url = exp.cap() + if exp.cap(2) == 'www.': + html = '{0}'.format(url) + else: + html = '{0}'.format(url) + text = text[:offset] + html + text[offset + len(exp.cap()):] + offset += len(html) + offset = exp.indexIn(text, offset) + arr = text.split('\n') + for i in range(len(arr)): # quotes + if arr[i].startswith('>'): + arr[i] = '' + arr[i][4:] + '' + text = '
'.join(arr) + text = self._smileys_loader.add_smileys_to_text(text, self) + return text + + +class MessageItem(QtWidgets.QWidget): + """ + Message in messages list + """ + def __init__(self, text_message, settings, message_browser_factory_method, delete_action, parent=None): + QtWidgets.QWidget.__init__(self, parent) + self._message = text_message + self._delete_action = delete_action + self.name = widgets.DataLabel(self) + self.name.setGeometry(QtCore.QRect(2, 2, 95, 23)) + self.name.setTextFormat(QtCore.Qt.PlainText) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(11) + font.setBold(True) + if text_message.author is not None: + self.name.setFont(font) + self.name.setText(text_message.author.name) + + self.time = QtWidgets.QLabel(self) + self.time.setGeometry(QtCore.QRect(parent.width() - 60, 0, 50, 25)) + font.setPointSize(10) + font.setBold(False) + self.time.setFont(font) + self._time = text_message.time + if text_message.author and text_message.author.type == MESSAGE_AUTHOR['NOT_SENT']: + movie = QtGui.QMovie(util.join_path(util.get_images_directory(), 'spinner.gif')) + self.time.setMovie(movie) + movie.start() + self.t = True + else: + self.time.setText(util.convert_time(text_message.time)) + self.t = False + + self.message = message_browser_factory_method(text_message.text, parent.width() - 160, + text_message.type, self) + if text_message.type != TOX_MESSAGE_TYPE['NORMAL']: + self.name.setStyleSheet("QLabel { color: #5CB3FF; }") + self.message.setAlignment(QtCore.Qt.AlignCenter) + self.time.setStyleSheet("QLabel { color: #5CB3FF; }") + self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 160, self.message.height())) + self.setFixedHeight(self.message.height()) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x(): + self.listMenu = QtWidgets.QMenu() + delete_item = self.listMenu.addAction(util_ui.tr('Delete message')) + delete_item.triggered.connect(self.delete) + parent_position = self.time.mapToGlobal(QtCore.QPoint(0, 0)) + self.listMenu.move(parent_position) + self.listMenu.show() + + def delete(self): + self._delete_action(self._message) + + def mark_as_sent(self): + if self.t: + self.time.setText(util.convert_time(self._time)) + self.t = False + return True + return False + + def set_avatar(self, pixmap): + self.name.setAlignment(QtCore.Qt.AlignCenter) + self.message.setAlignment(QtCore.Qt.AlignVCenter) + self.setFixedHeight(max(self.height(), 36)) + self.name.setFixedHeight(self.height()) + self.message.setFixedHeight(self.height()) + self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) + + def select_text(self, text): + tmp = self.message.toHtml() + text = h.escape(text) + strings = re.findall(text, tmp, flags=re.IGNORECASE) + for s in strings: + tmp = self.replace_all(tmp, s) + self.message.setHtml(tmp) + + @staticmethod + def replace_all(text, substring): + i, l = 0, len(substring) + while i < len(text) - l + 1: + index = text[i:].find(substring) + if index == -1: + break + i += index + lgt, rgt = text[i:].find('<'), text[i:].find('>') + if rgt < lgt: + i += rgt + 1 + continue + sub = '{}'.format(substring) + text = text[:i] + sub + text[i + l:] + i += len(sub) + return text + + +class FileTransferItem(QtWidgets.QListWidget): + + def __init__(self, transfer_message, file_transfer_handler, settings, width, parent=None): + + QtWidgets.QListWidget.__init__(self, parent) + self._file_transfer_handler = file_transfer_handler + self.resize(QtCore.QSize(width, 34)) + if transfer_message.state == FILE_TRANSFER_STATE['CANCELLED']: + self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') + elif transfer_message.state in PAUSED_FILE_TRANSFERS: + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') + else: + self.setStyleSheet('QListWidget { border: 1px solid green; }') + self.state = transfer_message.state + + self.name = DataLabel(self) + self.name.setGeometry(QtCore.QRect(3, 7, 95, 25)) + self.name.setTextFormat(QtCore.Qt.PlainText) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(11) + font.setBold(True) + self.name.setFont(font) + self.name.setText(transfer_message.author.name) + + self.time = QtWidgets.QLabel(self) + self.time.setGeometry(QtCore.QRect(width - 60, 7, 50, 25)) + font.setPointSize(10) + font.setBold(False) + self.time.setFont(font) + self.time.setText(util.convert_time(transfer_message.time)) + + self.cancel = QtWidgets.QPushButton(self) + self.cancel.setGeometry(QtCore.QRect(width - 125, 2, 30, 30)) + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), 'decline.png')) + icon = QtGui.QIcon(pixmap) + self.cancel.setIcon(icon) + self.cancel.setIconSize(QtCore.QSize(30, 30)) + self.cancel.setVisible(transfer_message.state in ACTIVE_FILE_TRANSFERS or + transfer_message.state == FILE_TRANSFER_STATE['UNSENT']) + self.cancel.clicked.connect( + lambda: self.cancel_transfer(transfer_message.friend_number, transfer_message.file_number)) + self.cancel.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none;}') + + self.accept_or_pause = QtWidgets.QPushButton(self) + self.accept_or_pause.setGeometry(QtCore.QRect(width - 170, 2, 30, 30)) + if transfer_message.state == FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + self.accept_or_pause.setVisible(True) + self.button_update('accept') + elif transfer_message.state in DO_NOT_SHOW_ACCEPT_BUTTON: + self.accept_or_pause.setVisible(False) + elif transfer_message.state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: # setup for continue + self.accept_or_pause.setVisible(True) + self.button_update('resume') + elif transfer_message.state == FILE_TRANSFER_STATE['UNSENT']: + self.accept_or_pause.setVisible(False) + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') + else: # pause + self.accept_or_pause.setVisible(True) + self.button_update('pause') + self.accept_or_pause.clicked.connect( + lambda: self.accept_or_pause_transfer(transfer_message.friend_number, transfer_message.file_number, + transfer_message.size)) + + self.accept_or_pause.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none}') + + self.pb = QtWidgets.QProgressBar(self) + self.pb.setGeometry(QtCore.QRect(100, 7, 100, 20)) + self.pb.setValue(0) + self.pb.setStyleSheet('QProgressBar { background-color: #302F2F; }') + self.pb.setVisible(transfer_message.state in SHOW_PROGRESS_BAR) + + self.file_name = DataLabel(self) + self.file_name.setGeometry(QtCore.QRect(210, 7, width - 420, 20)) + font.setPointSize(12) + self.file_name.setFont(font) + file_size = transfer_message.size // 1024 + if not file_size: + file_size = '{}B'.format(transfer_message.size) + elif file_size >= 1024: + file_size = '{}MB'.format(file_size // 1024) + else: + file_size = '{}KB'.format(file_size) + file_data = '{} {}'.format(file_size, transfer_message.file_name) + self.file_name.setText(file_data) + self.file_name.setToolTip(transfer_message.file_name) + self.saved_name = transfer_message.file_name + self.time_left = QtWidgets.QLabel(self) + self.time_left.setGeometry(QtCore.QRect(width - 92, 7, 30, 20)) + font.setPointSize(10) + self.time_left.setFont(font) + self.time_left.setVisible(transfer_message.state == FILE_TRANSFER_STATE['RUNNING']) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.paused = False + + def cancel_transfer(self, friend_number, file_number): + self._file_transfer_handler.cancel_transfer(friend_number, file_number) + self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') + self.cancel.setVisible(False) + self.accept_or_pause.setVisible(False) + self.pb.setVisible(False) + + def accept_or_pause_transfer(self, friend_number, file_number, size): + if self.state == FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']: + directory = util_ui.directory_dialog(util_ui.tr('Choose folder')) + self.pb.setVisible(True) + if directory: + self._file_transfer_handler.accept_transfer(directory + '/' + self.saved_name, + friend_number, file_number, size) + self.button_update('pause') + elif self.state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume + self.paused = False + self._file_transfer_handler.resume_transfer(friend_number, file_number) + self.button_update('pause') + self.state = FILE_TRANSFER_STATE['RUNNING'] + else: # pause + self.paused = True + self.state = FILE_TRANSFER_STATE['PAUSED_BY_USER'] + self._file_transfer_handler.pause_transfer(friend_number, file_number) + self.button_update('resume') + self.accept_or_pause.clearFocus() + + def button_update(self, path): + pixmap = QtGui.QPixmap(util.join_path(util.get_images_directory(), '{}.png'.format(path))) + icon = QtGui.QIcon(pixmap) + self.accept_or_pause.setIcon(icon) + self.accept_or_pause.setIconSize(QtCore.QSize(30, 30)) + + def update_transfer_state(self, state, progress, time): + self.pb.setValue(int(progress * 100)) + if time + 1: + m, s = divmod(time, 60) + self.time_left.setText('{0:02d}:{1:02d}'.format(m, s)) + if self.state != state and self.state in ACTIVE_FILE_TRANSFERS: + if state == FILE_TRANSFER_STATE['CANCELLED']: + self.setStyleSheet('QListWidget { border: 1px solid #B40404; }') + self.cancel.setVisible(False) + self.accept_or_pause.setVisible(False) + self.pb.setVisible(False) + self.state = state + self.time_left.setVisible(False) + elif state == FILE_TRANSFER_STATE['FINISHED']: + self.accept_or_pause.setVisible(False) + self.pb.setVisible(False) + self.cancel.setVisible(False) + self.setStyleSheet('QListWidget { border: 1px solid green; }') + self.state = state + self.time_left.setVisible(False) + elif state == FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']: + self.accept_or_pause.setVisible(False) + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') + self.state = state + self.time_left.setVisible(False) + elif state == FILE_TRANSFER_STATE['PAUSED_BY_USER']: + self.button_update('resume') # setup button continue + self.setStyleSheet('QListWidget { border: 1px solid green; }') + self.state = state + self.time_left.setVisible(False) + elif state == FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']: + self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }') + self.accept_or_pause.setVisible(False) + self.time_left.setVisible(False) + self.pb.setVisible(False) + elif not self.paused: # active + self.pb.setVisible(True) + self.accept_or_pause.setVisible(True) # setup to pause + self.button_update('pause') + self.setStyleSheet('QListWidget { border: 1px solid green; }') + self.state = state + self.time_left.setVisible(True) + + +class UnsentFileItem(FileTransferItem): + + def __init__(self, transfer_message, file_transfer_handler, settings, width, parent=None): + super().__init__(transfer_message, file_transfer_handler, settings, width, parent) + self._time = time + movie = QtGui.QMovie(util.join_path(util.get_images_directory(), 'spinner.gif')) + self.time.setMovie(movie) + movie.start() + self._message_id = transfer_message.message_id + self._friend_number = transfer_message.friend_number + + def cancel_transfer(self, *args): + self._file_transfer_handler.cancel_not_started_transfer(self._friend_number, self._message_id) + + +class InlineImageItem(QtWidgets.QScrollArea): + + def __init__(self, data, width, elem, parent=None): + + QtWidgets.QScrollArea.__init__(self, parent) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self._elem = elem + self._image_label = QtWidgets.QLabel(self) + self._image_label.raise_() + self.setWidget(self._image_label) + self._image_label.setScaledContents(False) + self._pixmap = QtGui.QPixmap() + self._pixmap.loadFromData(data, 'PNG') + self._max_size = width - 30 + self._resize_needed = not (self._pixmap.width() <= self._max_size) + self._full_size = not self._resize_needed + if not self._resize_needed: + self._image_label.setPixmap(self._pixmap) + self.resize(QtCore.QSize(self._max_size + 5, self._pixmap.height() + 5)) + self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) + else: + pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) + self._image_label.setPixmap(pixmap) + self.resize(QtCore.QSize(self._max_size + 5, pixmap.height())) + self._image_label.setGeometry(5, 0, self._max_size + 5, pixmap.height()) + self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton and self._resize_needed: # scale inline + if self._full_size: + pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio) + self._image_label.setPixmap(pixmap) + self.resize(QtCore.QSize(self._max_size, pixmap.height())) + self._image_label.setGeometry(5, 0, pixmap.width(), pixmap.height()) + else: + self._image_label.setPixmap(self._pixmap) + self.resize(QtCore.QSize(self._max_size, self._pixmap.height() + 17)) + self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height()) + self._full_size = not self._full_size + self._elem.setSizeHint(QtCore.QSize(self.width(), self.height())) + elif event.button() == QtCore.Qt.RightButton: # save inline + directory = util_ui.directory_dialog(util_ui.tr('Choose folder')) + if directory: + fl = QtCore.QFile(directory + '/toxygen_inline_' + util.curr_time().replace(':', '_') + '.png') + self._pixmap.save(fl, 'PNG') diff --git a/toxygen/passwordscreen.py b/toxygen/ui/password_screen.py similarity index 56% rename from toxygen/passwordscreen.py rename to toxygen/ui/password_screen.py index dcd9d05..57f7b95 100644 --- a/toxygen/passwordscreen.py +++ b/toxygen/ui/password_screen.py @@ -1,28 +1,33 @@ -from widgets import CenteredWidget, LineEdit -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui +# -*- 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 +import utils.ui as util_ui + +global LOG +LOG = logging.getLogger('app.'+__name__) class PasswordArea(LineEdit): def __init__(self, parent): - super(PasswordArea, self).__init__(parent) - self.parent = parent - self.setEchoMode(QtGui.QLineEdit.EchoMode.Password) + super().__init__(parent) + self._parent = parent + self.setEchoMode(QtWidgets.QLineEdit.Password) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Return: - self.parent.button_click() + self._parent.button_click() else: - super(PasswordArea, self).keyPressEvent(event) + super().keyPressEvent(event) -class PasswordScreenBase(CenteredWidget): +class PasswordScreenBase(CenteredWidget, DialogWithResult): def __init__(self, encrypt): - super(PasswordScreenBase, self).__init__() + CenteredWidget.__init__(self) + DialogWithResult.__init__(self) self._encrypt = encrypt self.initUI() @@ -31,18 +36,18 @@ class PasswordScreenBase(CenteredWidget): self.setMinimumSize(QtCore.QSize(360, 170)) self.setMaximumSize(QtCore.QSize(360, 170)) - self.enter_pass = QtGui.QLabel(self) + self.enter_pass = QtWidgets.QLabel(self) self.enter_pass.setGeometry(QtCore.QRect(30, 10, 300, 30)) self.password = PasswordArea(self) self.password.setGeometry(QtCore.QRect(30, 50, 300, 30)) - self.button = QtGui.QPushButton(self) + self.button = QtWidgets.QPushButton(self) self.button.setGeometry(QtCore.QRect(30, 90, 300, 30)) - self.button.setText('OK') + self.button.setText(util_ui.tr('OK')) self.button.clicked.connect(self.button_click) - self.warning = QtGui.QLabel(self) + self.warning = QtWidgets.QLabel(self) self.warning.setGeometry(QtCore.QRect(30, 130, 300, 30)) self.warning.setStyleSheet('QLabel { color: #F70D1A; }') self.warning.setVisible(False) @@ -61,28 +66,28 @@ class PasswordScreenBase(CenteredWidget): super(PasswordScreenBase, self).keyPressEvent(event) def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("pass", "Enter password", None, QtGui.QApplication.UnicodeUTF8)) - self.enter_pass.setText(QtGui.QApplication.translate("pass", "Password:", None, QtGui.QApplication.UnicodeUTF8)) - self.warning.setText(QtGui.QApplication.translate("pass", "Incorrect password", None, QtGui.QApplication.UnicodeUTF8)) + self.setWindowTitle(util_ui.tr('Enter password')) + self.enter_pass.setText(util_ui.tr('Password:')) + self.warning.setText(util_ui.tr('Incorrect password')) class PasswordScreen(PasswordScreenBase): def __init__(self, encrypt, data): - super(PasswordScreen, self).__init__(encrypt) + super().__init__(encrypt) self._data = data def button_click(self): if self.password.text(): try: self._encrypt.set_password(self.password.text()) - new_data = self._encrypt.pass_decrypt(self._data[0]) + new_data = self._encrypt.pass_decrypt(self._data) except Exception as ex: self.warning.setVisible(True) + LOG.error(f"Decryption error: {ex}") print('Decryption error:', ex) else: - self._data[0] = new_data - self.close() + self.close_with_result(new_data) class UnlockAppScreen(PasswordScreenBase): @@ -116,37 +121,31 @@ class SetProfilePasswordScreen(CenteredWidget): self.setMaximumSize(QtCore.QSize(700, 200)) self.password = LineEdit(self) self.password.setGeometry(QtCore.QRect(40, 10, 300, 30)) - self.password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) + self.password.setEchoMode(QtWidgets.QLineEdit.Password) self.confirm_password = LineEdit(self) self.confirm_password.setGeometry(QtCore.QRect(40, 50, 300, 30)) - self.confirm_password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) - self.set_password = QtGui.QPushButton(self) + self.confirm_password.setEchoMode(QtWidgets.QLineEdit.Password) + self.set_password = QtWidgets.QPushButton(self) self.set_password.setGeometry(QtCore.QRect(40, 100, 300, 30)) self.set_password.clicked.connect(self.new_password) - self.not_match = QtGui.QLabel(self) + self.not_match = QtWidgets.QLabel(self) self.not_match.setGeometry(QtCore.QRect(350, 50, 300, 30)) self.not_match.setVisible(False) self.not_match.setStyleSheet('QLabel { color: #BC1C1C; }') - self.warning = QtGui.QLabel(self) + self.warning = QtWidgets.QLabel(self) self.warning.setGeometry(QtCore.QRect(40, 160, 500, 30)) self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') def retranslateUi(self): - self.setWindowTitle(QtGui.QApplication.translate("PasswordScreen", "Profile password", None, - QtGui.QApplication.UnicodeUTF8)) + self.setWindowTitle(util_ui.tr('Profile password')) self.password.setPlaceholderText( - QtGui.QApplication.translate("PasswordScreen", "Password (at least 8 symbols)", None, - QtGui.QApplication.UnicodeUTF8)) + util_ui.tr('Password (at least 8 symbols)')) self.confirm_password.setPlaceholderText( - QtGui.QApplication.translate("PasswordScreen", "Confirm password", None, - QtGui.QApplication.UnicodeUTF8)) + util_ui.tr('Confirm password')) self.set_password.setText( - QtGui.QApplication.translate("PasswordScreen", "Set password", None, QtGui.QApplication.UnicodeUTF8)) - self.not_match.setText(QtGui.QApplication.translate("PasswordScreen", "Passwords do not match", None, - QtGui.QApplication.UnicodeUTF8)) - self.warning.setText( - QtGui.QApplication.translate("PasswordScreen", "There is no way to recover lost passwords", None, - QtGui.QApplication.UnicodeUTF8)) + util_ui.tr('Set password')) + self.not_match.setText(util_ui.tr('Passwords do not match')) + self.warning.setText(util_ui.tr('There is no way to recover lost passwords')) def new_password(self): if self.password.text() == self.confirm_password.text(): @@ -154,11 +153,8 @@ class SetProfilePasswordScreen(CenteredWidget): self._encrypt.set_password(self.password.text()) self.close() else: - self.not_match.setText( - QtGui.QApplication.translate("PasswordScreen", "Password must be at least 8 symbols", None, - QtGui.QApplication.UnicodeUTF8)) + self.not_match.setText(util_ui.tr('Password must be at least 8 symbols')) self.not_match.setVisible(True) else: - self.not_match.setText(QtGui.QApplication.translate("PasswordScreen", "Passwords do not match", None, - QtGui.QApplication.UnicodeUTF8)) + self.not_match.setText(util_ui.tr('Passwords do not match')) self.not_match.setVisible(True) diff --git a/toxygen/ui/peer_screen.py b/toxygen/ui/peer_screen.py new file mode 100644 index 0000000..6bca903 --- /dev/null +++ b/toxygen/ui/peer_screen.py @@ -0,0 +1,116 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import uic + +from ui.widgets import CenteredWidget +import utils.util as util +import utils.ui as util_ui +from ui.contact_items import * +import toxygen_wrapper.toxcore_enums_and_consts as consts + + +class PeerScreen(CenteredWidget): + + def __init__(self, contacts_manager, groups_service, group, peer_id): + super().__init__() + self._contacts_manager = contacts_manager + self._groups_service = groups_service + self._group = group + self._peer = group.get_peer_by_id(peer_id) + + self._roles = { + TOX_GROUP_ROLE['FOUNDER']: util_ui.tr('Administrator'), + TOX_GROUP_ROLE['MODERATOR']: util_ui.tr('Moderator'), + TOX_GROUP_ROLE['USER']: util_ui.tr('User'), + TOX_GROUP_ROLE['OBSERVER']: util_ui.tr('Observer') + } + + uic.loadUi(util.get_views_path('peer_screen'), self) + self._update_ui() + + def _update_ui(self): + self.statusCircle = StatusCircle(self) + self.statusCircle.setGeometry(50, 15, 30, 30) + + if self._peer: + self.statusCircle.update(self._peer.status) + self.peerNameLabel.setText(self._peer.name) + self.ignorePeerCheckBox.setChecked(self._peer.is_muted) + + self.ignorePeerCheckBox.clicked.connect(self._toggle_ignore) + self.sendPrivateMessagePushButton.clicked.connect(self._send_private_message) + self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) + self.roleNameLabel.setText(self._get_role_name()) + can_change_role_or_ban = self._can_change_role_or_ban() + self.rolesComboBox.setVisible(can_change_role_or_ban) + self.roleNameLabel.setVisible(not can_change_role_or_ban) + self.banGroupBox.setEnabled(can_change_role_or_ban) +# self.banPushButton.clicked.connect(self._ban_peer) + self.kickPushButton.clicked.connect(self._kick_peer) + + self._retranslate_ui() + + self.rolesComboBox.currentIndexChanged.connect(self._role_set) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Peer details')) + self.ignorePeerCheckBox.setText(util_ui.tr('Ignore peer')) + self.roleLabel.setText(util_ui.tr('Role:')) + self.copyPublicKeyPushButton.setText(util_ui.tr('Copy public key')) + self.sendPrivateMessagePushButton.setText(util_ui.tr('Send private message')) +# self.banPushButton.setText(util_ui.tr('Ban peer')) + self.kickPushButton.setText(util_ui.tr('Kick peer')) + self.banGroupBox.setTitle(util_ui.tr('Ban peer')) + self.ipBanRadioButton.setText(util_ui.tr('IP')) + self.nickBanRadioButton.setText(util_ui.tr('Nickname')) + self.pkBanRadioButton.setText(util_ui.tr('Public key')) + + self.rolesComboBox.clear() + index = self._group.get_self_peer().role + roles = list(self._roles.values()) + for role in roles[index + 1:]: + self.rolesComboBox.addItem(role) + self.rolesComboBox.setCurrentIndex(self._peer.role - index - 1) + + def _can_change_role_or_ban(self): + self_peer = self._group.get_self_peer() + if self_peer.role > TOX_GROUP_ROLE['MODERATOR']: + return False + + return self_peer.role < self._peer.role + + def _role_set(self): + index = self.rolesComboBox.currentIndex() + all_roles_count = len(self._roles) + diff = all_roles_count - self.rolesComboBox.count() + self._groups_service.set_new_peer_role(self._group, self._peer, index + diff) + + def _get_role_name(self): + return self._roles[self._peer.role] + + def _toggle_ignore(self): + ignore = self.ignorePeerCheckBox.isChecked() + self._groups_service.toggle_ignore_peer(self._group, self._peer, ignore) + + def _send_private_message(self): + self._contacts_manager.add_group_peer(self._group, self._peer) + self.close() + + def _copy_public_key(self): + util_ui.copy_to_clipboard(self._peer.public_key) + + def _ban_peer(self): + ban_type = self._get_ban_type() + self._groups_service.ban_peer(self._group, self._peer.id, ban_type) + self.close() + + def _kick_peer(self): + self._groups_service.kick_peer(self._group, self._peer.id) + self.close() + + def _get_ban_type(self): + if self.ipBanRadioButton.isChecked(): + return consts.TOX_GROUP_BAN_TYPE['IP_PORT'] + elif self.nickBanRadioButton.isChecked(): + return consts.TOX_GROUP_BAN_TYPE['NICK'] + return consts.TOX_GROUP_BAN_TYPE['PUBLIC_KEY'] diff --git a/toxygen/ui/profile_settings_screen.py b/toxygen/ui/profile_settings_screen.py new file mode 100644 index 0000000..5b4658f --- /dev/null +++ b/toxygen/ui/profile_settings_screen.py @@ -0,0 +1,159 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import QtGui, QtCore, uic + +from ui.widgets import CenteredWidget +import utils.ui as util_ui +from utils.util import join_path, get_images_directory, get_views_path +from user_data.settings import Settings + +class ProfileSettings(CenteredWidget): + """Form with profile settings such as name, status, TOX ID""" + def __init__(self, profile, profile_manager, settings, toxes): + super().__init__() + self._profile = profile + self._profile_manager = profile_manager + self._settings = settings + self._toxes = toxes + self._auto = False + + uic.loadUi(get_views_path('profile_settings_screen'), self) + + self._init_ui() + self.center() + + def closeEvent(self, event): + self._profile.set_name(self.nameLineEdit.text()) + self._profile.set_status_message(self.statusMessageLineEdit.text()) + self._profile.set_status(self.statusComboBox.currentIndex()) + + def _init_ui(self): + self._auto = Settings.get_auto_profile() == self._profile_manager.get_path() + self.toxIdLabel.setText(self._profile.tox_id) + self.nameLineEdit.setText(self._profile.name) + self.statusMessageLineEdit.setText(str(self._profile.status_message)) + self.defaultProfilePushButton.clicked.connect(self._toggle_auto_profile) + self.copyToxIdPushButton.clicked.connect(self._copy_tox_id) + self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) + self.changePasswordPushButton.clicked.connect(self._save_password) + self.exportProfilePushButton.clicked.connect(self._export_profile) + self.newNoSpamPushButton.clicked.connect(self._set_new_no_spam) + self.newAvatarPushButton.clicked.connect(self._set_avatar) + self.resetAvatarPushButton.clicked.connect(self._reset_avatar) + + self.invalidPasswordsLabel.setVisible(False) + + self._retranslate_ui() + + if self._profile.status is not None: + self.statusComboBox.setCurrentIndex(self._profile.status) + else: + self.statusComboBox.setVisible(False) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr("Profile settings")) + + self.exportProfilePushButton.setText(util_ui.tr("Export profile")) + self.nameLabel.setText(util_ui.tr("Name:")) + self.statusLabel.setText(util_ui.tr("Status:")) + self.toxIdTitleLabel.setText(util_ui.tr("TOX ID:")) + self.copyToxIdPushButton.setText(util_ui.tr("Copy TOX ID")) + self.newAvatarPushButton.setText(util_ui.tr("New avatar")) + self.resetAvatarPushButton.setText(util_ui.tr("Reset avatar")) + self.newNoSpamPushButton.setText(util_ui.tr("New NoSpam")) + self.profilePasswordLabel.setText(util_ui.tr("Profile password")) + self.passwordLineEdit.setPlaceholderText(util_ui.tr("Password (at least 8 symbols)")) + self.confirmPasswordLineEdit.setPlaceholderText(util_ui.tr("Confirm password")) + self.changePasswordPushButton.setText(util_ui.tr("Set password")) + self.invalidPasswordsLabel.setText(util_ui.tr("Passwords do not match")) + self.emptyPasswordLabel.setText(util_ui.tr("Leaving blank will reset current password")) + self.warningLabel.setText(util_ui.tr("There is no way to recover lost passwords")) + self.statusComboBox.addItem(util_ui.tr("Online")) + self.statusComboBox.addItem(util_ui.tr("Away")) + self.statusComboBox.addItem(util_ui.tr("Busy")) + self.copyPublicKeyPushButton.setText(util_ui.tr("Copy public key" +' (64)')) + + self._set_default_profile_button_text() + + def _toggle_auto_profile(self): + if self._auto: + Settings.reset_auto_profile() + else: + Settings.set_auto_profile(self._profile_manager.get_path()) + self._auto = not self._auto + self._set_default_profile_button_text() + + def _set_default_profile_button_text(self): + if self._auto: + self.defaultProfilePushButton.setText(util_ui.tr("Mark as not default profile")) + else: + self.defaultProfilePushButton.setText(util_ui.tr("Mark as default profile")) + + def _save_password(self): + password = self.passwordLineEdit.text() + confirm_password = self.confirmPasswordLineEdit.text() + if password == confirm_password: + if not len(password) or len(password) >= 8: + self._toxes.set_password(password) + self.close() + else: + self.invalidPasswordsLabel.setText( + util_ui.tr("Password must be at least 8 symbols")) + self.invalidPasswordsLabel.setVisible(True) + else: + self.invalidPasswordsLabel.setText(util_ui.tr("Passwords do not match")) + self.invalidPasswordsLabel.setVisible(True) + + def _copy_tox_id(self): + util_ui.copy_to_clipboard(self._profile.tox_id) + + icon = self._get_accept_icon() + self.copyToxIdPushButton.setIcon(icon) + self.copyToxIdPushButton.setIconSize(QtCore.QSize(10, 10)) + + def _copy_public_key(self): + util_ui.copy_to_clipboard(self._profile.tox_id[:64]) + + icon = self._get_accept_icon() + self.copyPublicKeyPushButton.setIcon(icon) + self.copyPublicKeyPushButton.setIconSize(QtCore.QSize(10, 10)) + + def _set_new_no_spam(self): + self.toxIdLabel.setText(self._profile.set_new_nospam()) + + def _reset_avatar(self): + self._profile.reset_avatar(self._settings['identicons']) + + def _set_avatar(self): + choose = util_ui.tr("Choose avatar") + name = util_ui.file_dialog(choose, 'Images (*.png)') + if not name[0]: + return + bitmap = QtGui.QPixmap(name[0]) + bitmap.scaled(QtCore.QSize(128, 128), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + + byte_array = QtCore.QByteArray() + buffer = QtCore.QBuffer(byte_array) + buffer.open(QtCore.QIODevice.WriteOnly) + bitmap.save(buffer, 'PNG') + + self._profile.set_avatar(bytes(byte_array.data())) + + def _export_profile(self): + directory = util_ui.directory_dialog() + if not directory: + return + + reply = util_ui.question(util_ui.tr('Do you want to move your profile to this location?'), + util_ui.tr('Use new path')) + + self._settings.export(directory) + self._profile.export_db(directory) + self._profile_manager.export_profile(self._settings, directory, reply) + + @staticmethod + def _get_accept_icon(): + pixmap = QtGui.QPixmap(join_path(get_images_directory(), 'accept.png')) + + return QtGui.QIcon(pixmap) + diff --git a/toxygen/ui/self_peer_screen.py b/toxygen/ui/self_peer_screen.py new file mode 100644 index 0000000..7f30653 --- /dev/null +++ b/toxygen/ui/self_peer_screen.py @@ -0,0 +1,69 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import uic + +from ui.widgets import CenteredWidget, LineEdit +import utils.util as util +import utils.ui as util_ui +from ui.contact_items import * + + +class SelfPeerScreen(CenteredWidget): + + def __init__(self, contacts_manager, groups_service, group): + super().__init__() + self._contacts_manager = contacts_manager + self._groups_service = groups_service + self._group = group + self._peer = group.get_self_peer() + self._roles = { + TOX_GROUP_ROLE['FOUNDER']: util_ui.tr('Administrator'), + TOX_GROUP_ROLE['MODERATOR']: util_ui.tr('Moderator'), + TOX_GROUP_ROLE['USER']: util_ui.tr('User'), + TOX_GROUP_ROLE['OBSERVER']: util_ui.tr('Observer') + } + + uic.loadUi(util.get_views_path('self_peer_screen'), self) + self._update_ui() + + def _update_ui(self): + self.lineEdit = LineEdit(self) + self.lineEdit.setGeometry(140, 40, 400, 30) + self.lineEdit.setText(self._peer.name) + self.lineEdit.textChanged.connect(self._nick_changed) + + self.savePushButton.clicked.connect(self._save) + self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key) + + self._retranslate_ui() + + self.statusComboBox.setCurrentIndex(self._peer.status) + + def _retranslate_ui(self): + self.setWindowTitle(util_ui.tr('Change credentials in group')) + self.lineEdit.setPlaceholderText(util_ui.tr('Your nickname in group')) + self.nameLabel.setText(util_ui.tr('Name:')) + self.roleLabel.setText(util_ui.tr('Role:')) + self.statusLabel.setText(util_ui.tr('Status:')) + self.copyPublicKeyPushButton.setText(util_ui.tr('Copy public key')) + self.savePushButton.setText(util_ui.tr('Save')) + self.roleNameLabel.setText(self._get_role_name()) + self.statusComboBox.addItem(util_ui.tr('Online')) + self.statusComboBox.addItem(util_ui.tr('Away')) + self.statusComboBox.addItem(util_ui.tr('Busy')) + + def _get_role_name(self): + return self._roles[self._peer.role] + + def _nick_changed(self): + nick = self.lineEdit.text() + self.savePushButton.setEnabled(bool(nick)) + + def _save(self): + nick = self.lineEdit.text() + status = self.statusComboBox.currentIndex() + self._groups_service.set_self_info(self._group, nick, status) + self.close() + + def _copy_public_key(self): + util_ui.copy_to_clipboard(self._peer.public_key) diff --git a/toxygen/ui/tray.py b/toxygen/ui/tray.py new file mode 100644 index 0000000..c838a0d --- /dev/null +++ b/toxygen/ui/tray.py @@ -0,0 +1,115 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import QtWidgets, QtGui, QtCore +# from PyQt5.QtCore import pyqtSignal as Signal +from qtpy.QtCore import Signal + +from utils.ui import tr +from utils.util import * +from ui.password_screen import UnlockAppScreen +import os.path + +class SystemTrayIcon(QtWidgets.QSystemTrayIcon): + # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' + leftClicked = Signal() + + def __init__(self, icon, parent=None): + super().__init__(icon, parent) + self.activated.connect(self.icon_activated) + + def icon_activated(self, reason): + if reason == QtWidgets.QSystemTrayIcon.Trigger: + self.leftClicked.emit() + + +class Menu(QtWidgets.QMenu): + + def __init__(self, settings, profile, *args): + super().__init__(*args) + self._settings = settings + self._profile = profile + + def new_status(self, status): + if not self._settings.locked: + self._profile.set_status(status) + self.about_to_show_handler() + self.hide() + + def about_to_show_handler(self): + status = self._profile.status + act = self.act + if status is None or self._settings.locked: + self.actions()[1].setVisible(False) + else: + self.actions()[1].setVisible(True) + act.actions()[0].setChecked(False) + act.actions()[1].setChecked(False) + act.actions()[2].setChecked(False) + act.actions()[status].setChecked(True) + self.actions()[2].setVisible(not self._settings.locked) + + def languageChange(self, *args, **kwargs): + self.actions()[0].setText(tr('Open Toxygen')) + self.actions()[1].setText(tr('Set status')) + self.actions()[2].setText(tr('Exit')) + self.act.actions()[0].setText(tr('Online')) + self.act.actions()[1].setText(tr('Away')) + self.act.actions()[2].setText(tr('Busy')) + + +def init_tray(profile, settings, main_screen, toxes): + icon = os.path.join(get_images_directory(), 'icon.png') + tray = SystemTrayIcon(QtGui.QIcon(icon)) + + menu = Menu(settings, profile) + show = menu.addAction(tr('Open Toxygen')) + sub = menu.addMenu(tr('Set status')) + online = sub.addAction(tr('Online')) + away = sub.addAction(tr('Away')) + busy = sub.addAction(tr('Busy')) + online.setCheckable(True) + away.setCheckable(True) + busy.setCheckable(True) + menu.act = sub + exit = menu.addAction(tr('Exit')) + + def show_window(): + def show(): + if not main_screen.isActiveWindow(): + main_screen.setWindowState( + main_screen.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + main_screen.activateWindow() + main_screen.show() + if not settings.locked: + show() + else: + def correct_pass(): + show() + settings.locked = False + settings.unlockScreen = False + if not settings.unlockScreen: + settings.unlockScreen = True + show_window.screen = UnlockAppScreen(toxes, correct_pass) + show_window.screen.show() + + def tray_activated(reason): + if reason == QtWidgets.QSystemTrayIcon.DoubleClick: + show_window() + + def close_app(): + if not settings.locked: + settings.closing = True + main_screen.close() + + show.triggered.connect(show_window) + exit.triggered.connect(close_app) + menu.aboutToShow.connect(menu.about_to_show_handler) + online.triggered.connect(lambda: menu.new_status(0)) + away.triggered.connect(lambda: menu.new_status(1)) + busy.triggered.connect(lambda: menu.new_status(2)) + + tray.setContextMenu(menu) + tray.show() + tray.activated.connect(tray_activated) + + return tray diff --git a/toxygen/ui/views/add_bootstrap_screen.ui b/toxygen/ui/views/add_bootstrap_screen.ui 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/add_contact_screen.ui b/toxygen/ui/views/add_contact_screen.ui new file mode 100644 index 0000000..0f26a25 --- /dev/null +++ b/toxygen/ui/views/add_contact_screen.ui @@ -0,0 +1,99 @@ + + + Form + + + + 0 + 0 + 560 + 320 + + + + + 560 + 320 + + + + + 560 + 320 + + + + Form + + + + + 50 + 10 + 150 + 20 + + + + TextLabel + + + + + + 50 + 70 + 150 + 30 + + + + TextLabel + + + + + + 50 + 110 + 460 + 150 + + + + + + + 50 + 270 + 460 + 30 + + + + PushButton + + + + + true + + + + 220 + 10 + 321 + 31 + + + + Qt::NoContextMenu + + + + + + + + + diff --git a/toxygen/ui/views/audio_settings_screen.ui b/toxygen/ui/views/audio_settings_screen.ui new file mode 100644 index 0000000..a404592 --- /dev/null +++ b/toxygen/ui/views/audio_settings_screen.ui @@ -0,0 +1,87 @@ + + + Form + + + + 0 + 0 + 315 + 218 + + + + + 315 + 218 + + + + + 315 + 218 + + + + Form + + + + + 30 + 10 + 261 + 30 + + + + + 16 + + + + TextLabel + + + + + + 30 + 100 + 261 + 30 + + + + + 16 + + + + TextLabel + + + + + + 30 + 50 + 255 + 41 + + + + + + + 30 + 140 + 255 + 41 + + + + + + + diff --git a/toxygen/ui/views/bans_list_screen.ui b/toxygen/ui/views/bans_list_screen.ui new file mode 100644 index 0000000..16339d8 --- /dev/null +++ b/toxygen/ui/views/bans_list_screen.ui @@ -0,0 +1,29 @@ + + + Form + + + + 0 + 0 + 500 + 375 + + + + Form + + + + + 0 + 0 + 500 + 375 + + + + + + + diff --git a/toxygen/ui/views/create_group_screen.ui b/toxygen/ui/views/create_group_screen.ui new file mode 100644 index 0000000..3a3358a --- /dev/null +++ b/toxygen/ui/views/create_group_screen.ui @@ -0,0 +1,127 @@ + + + Form + + + + 0 + 0 + 640 + 300 + + + + Form + + + + false + + + + 20 + 250 + 601 + 41 + + + + + + + + + + 150 + 20 + 470 + 35 + + + + + + + 150 + 80 + 470 + 35 + + + + + + + 20 + 20 + 121 + 31 + + + + TextLabel + + + + + + 20 + 80 + 121 + 31 + + + + TextLabel + + + + + + 20 + 200 + 111 + 17 + + + + TextLabel + + + + + + 20 + 150 + 111 + 17 + + + + TextLabel + + + + + + 150 + 140 + 470 + 35 + + + + + + + 150 + 190 + 470 + 35 + + + + + + + diff --git a/toxygen/ui/views/create_profile_screen.ui b/toxygen/ui/views/create_profile_screen.ui new file mode 100644 index 0000000..bfffee5 --- /dev/null +++ b/toxygen/ui/views/create_profile_screen.ui @@ -0,0 +1,128 @@ + + + Form + + + + 0 + 0 + 400 + 340 + + + + + 400 + 340 + + + + + 400 + 340 + + + + Form + + + + + 30 + 270 + 341 + 51 + + + + PushButton + + + + + + 30 + 170 + 341 + 41 + + + + QLineEdit::Password + + + + + + 30 + 120 + 341 + 41 + + + + QLineEdit::Password + + + + + + 30 + 80 + 330 + 20 + + + + TextLabel + + + + + + 30 + 10 + 330 + 23 + + + + RadioButton + + + true + + + + + + 30 + 40 + 330 + 23 + + + + RadioButton + + + + + + 30 + 220 + 341 + 30 + + + + + + + Qt::AlignCenter + + + + + + diff --git a/toxygen/ui/views/gc_ban_item.ui b/toxygen/ui/views/gc_ban_item.ui new file mode 100644 index 0000000..a57d0e1 --- /dev/null +++ b/toxygen/ui/views/gc_ban_item.ui @@ -0,0 +1,58 @@ + + + Form + + + + 0 + 0 + 500 + 100 + + + + Form + + + + + 330 + 30 + 161 + 41 + + + + PushButton + + + + + + 15 + 20 + 305 + 20 + + + + TextLabel + + + + + + 15 + 50 + 305 + 20 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/gc_invite_item.ui b/toxygen/ui/views/gc_invite_item.ui new file mode 100644 index 0000000..6eddbeb --- /dev/null +++ b/toxygen/ui/views/gc_invite_item.ui @@ -0,0 +1,71 @@ + + + Form + + + + 0 + 0 + 600 + 150 + + + + Form + + + + + 250 + 30 + 300 + 21 + + + + TextLabel + + + + + + 250 + 70 + 300 + 21 + + + + TextLabel + + + + + + 140 + 30 + 60 + 60 + + + + TextLabel + + + + + + 40 + 50 + 20 + 23 + + + + + + + + + + diff --git a/toxygen/ui/views/gc_settings_screen.ui b/toxygen/ui/views/gc_settings_screen.ui new file mode 100644 index 0000000..526c156 --- /dev/null +++ b/toxygen/ui/views/gc_settings_screen.ui @@ -0,0 +1,83 @@ + + + Form + + + + 0 + 0 + 400 + 220 + + + + + 400 + 220 + + + + + 400 + 220 + + + + Form + + + + + 10 + 20 + 380 + 20 + + + + TextLabel + + + + + + 10 + 60 + 380 + 40 + + + + PushButton + + + + + + 10 + 120 + 380 + 20 + + + + TextLabel + + + + + + 10 + 160 + 380 + 20 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/group_invites_screen.ui b/toxygen/ui/views/group_invites_screen.ui new file mode 100644 index 0000000..183f801 --- /dev/null +++ b/toxygen/ui/views/group_invites_screen.ui @@ -0,0 +1,113 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 0 + 150 + 600 + 25 + + + + TextLabel + + + Qt::AlignCenter + + + + + + 0 + 0 + 600 + 341 + + + + + + + 10 + 360 + 350 + 35 + + + + + + + 10 + 410 + 350 + 35 + + + + + + + 390 + 390 + 200 + 35 + + + + + + + 40 + 460 + 201 + 31 + + + + PushButton + + + + + + 360 + 460 + 201 + 31 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/group_management_screen.ui b/toxygen/ui/views/group_management_screen.ui new file mode 100644 index 0000000..de7c21e --- /dev/null +++ b/toxygen/ui/views/group_management_screen.ui @@ -0,0 +1,123 @@ + + + Form + + + + 0 + 0 + 658 + 283 + + + + Form + + + + + 180 + 20 + 450 + 41 + + + + + + + 20 + 30 + 145 + 20 + + + + TextLabel + + + + + + 20 + 80 + 145 + 20 + + + + TextLabel + + + + + + 180 + 70 + 450 + 40 + + + + 2 + + + 9999 + + + 512 + + + + + + 20 + 130 + 145 + 20 + + + + TextLabel + + + + + + 180 + 120 + 450 + 40 + + + + + + + 20 + 180 + 300 + 41 + + + + PushButton + + + + + + 20 + 220 + 611 + 41 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/interface_settings_screen.ui b/toxygen/ui/views/interface_settings_screen.ui new file mode 100644 index 0000000..b762903 --- /dev/null +++ b/toxygen/ui/views/interface_settings_screen.ui @@ -0,0 +1,255 @@ + + + Form + + + + 0 + 0 + 552 + 847 + + + + Form + + + + + + Qt::ScrollBarAsNeeded + + + true + + + + + 0 + 0 + 532 + 827 + + + + + + 30 + 140 + 67 + 17 + + + + TextLabel + + + + + + 20 + 180 + 471 + 31 + + + + + + + 20 + 60 + 471 + 31 + + + + + + + 30 + 20 + 67 + 17 + + + + TextLabel + + + + + + + 30 + 280 + 461 + 221 + + + + GroupBox + + + + + 30 + 40 + 92 + 23 + + + + CheckBox + + + + + + 30 + 80 + 411 + 17 + + + + TextLabel + + + + + + 30 + 120 + 411 + 31 + + + + + + + + 30 + 250 + 461 + 23 + + + + CheckBox + + + + + + 30 + 750 + 471 + 40 + + + + PushButton + + + + + + 30 + 690 + 471 + 40 + + + + PushButton + + + + + + 30 + 520 + 461 + 23 + + + + CheckBox + + + + + + 30 + 550 + 471 + 131 + + + + GroupBox + + + + + 30 + 30 + 421 + 23 + + + + RadioButton + + + + + + 30 + 60 + 431 + 23 + + + + RadioButton + + + + + + 30 + 90 + 421 + 23 + + + + RadioButton + + + + + + + + + + + diff --git a/toxygen/ui/views/join_group_screen.ui b/toxygen/ui/views/join_group_screen.ui new file mode 100644 index 0000000..077a332 --- /dev/null +++ b/toxygen/ui/views/join_group_screen.ui @@ -0,0 +1,139 @@ + + + Form + + + + 0 + 0 + 740 + 320 + + + + + 740 + 320 + + + + + 740 + 320 + + + + Form + + + + + 30 + 30 + 67 + 17 + + + + TextLabel + + + + + + 30 + 90 + 67 + 17 + + + + TextLabel + + + + + false + + + + 30 + 260 + 680 + 51 + + + + + + + + + + 190 + 20 + 520 + 41 + + + + + + + 190 + 80 + 520 + 41 + + + + + + + 30 + 150 + 67 + 17 + + + + TextLabel + + + + + + 30 + 210 + 67 + 17 + + + + TextLabel + + + + + + 190 + 140 + 520 + 41 + + + + + + + 190 + 200 + 520 + 41 + + + + + + + diff --git a/toxygen/ui/views/login_screen.ui b/toxygen/ui/views/login_screen.ui new file mode 100644 index 0000000..d100803 --- /dev/null +++ b/toxygen/ui/views/login_screen.ui @@ -0,0 +1,135 @@ + + + loginScreen + + + + 0 + 0 + 400 + 200 + + + + + 400 + 200 + + + + + 400 + 200 + + + + Form + + + + + 0 + 5 + 401 + 30 + + + + + 16 + 75 + true + + + + Toxygen + + + Qt::AlignCenter + + + + + + 10 + 40 + 180 + 150 + + + + GroupBox + + + Qt::AlignCenter + + + + + 10 + 110 + 160 + 27 + + + + PushButton + + + + + + + 210 + 40 + 180 + 150 + + + + GroupBox + + + Qt::AlignCenter + + + + + 10 + 40 + 160 + 27 + + + + + + + 10 + 75 + 160 + 27 + + + + CheckBox + + + + + + 10 + 110 + 160 + 27 + + + + PushButton + + + + + + + diff --git a/toxygen/ui/views/ms_left_column.ui b/toxygen/ui/views/ms_left_column.ui new file mode 100644 index 0000000..ffbff71 --- /dev/null +++ b/toxygen/ui/views/ms_left_column.ui @@ -0,0 +1,94 @@ + + + Form + + + + 0 + 0 + 270 + 500 + + + + PointingHandCursor + + + Form + + + + + 5 + 5 + 64 + 64 + + + + PointingHandCursor + + + TextLabel + + + + + + 0 + 75 + 150 + 25 + + + + + + + 150 + 75 + 120 + 25 + + + + + + + 0 + 77 + 20 + 20 + + + + TextLabel + + + + + + 0 + 100 + 270 + 400 + + + + + + + 0 + 100 + 270 + 30 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/network_settings_screen.ui b/toxygen/ui/views/network_settings_screen.ui new file mode 100644 index 0000000..f6e2960 --- /dev/null +++ b/toxygen/ui/views/network_settings_screen.ui @@ -0,0 +1,196 @@ + + + Form + + + + 0 + 0 + 400 + 545 + + + + + 400 + 545 + + + + + 400 + 545 + + + + Form + + + + + 30 + 20 + 150 + 30 + + + + CheckBox + + + + + + 210 + 20 + 150 + 30 + + + + CheckBox + + + + + + 30 + 140 + 150 + 30 + + + + CheckBox + + + + + + 30 + 190 + 150 + 25 + + + + RadioButton + + + + + + 30 + 230 + 150 + 25 + + + + RadioButton + + + + + + 30 + 100 + 150 + 30 + + + + CheckBox + + + + + + 30 + 280 + 60 + 20 + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 330 + 60 + 20 + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 380 + 60 + 20 + + + + Chat Url + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 430 + 340 + 40 + + + + PushButton + + + + + + 30 + 60 + 340 + 30 + + + + CheckBox + + + + + + 30 + 480 + 340 + 65 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/notifications_settings_screen.ui b/toxygen/ui/views/notifications_settings_screen.ui new file mode 100644 index 0000000..67e2dc6 --- /dev/null +++ b/toxygen/ui/views/notifications_settings_screen.ui @@ -0,0 +1,71 @@ + + + Form + + + + 0 + 0 + 320 + 201 + + + + Form + + + + + 20 + 20 + 271 + 41 + + + + CheckBox + + + + + + 20 + 60 + 271 + 41 + + + + CheckBox + + + + + + 20 + 100 + 271 + 41 + + + + CheckBox + + + + + + 20 + 140 + 271 + 41 + + + + CheckBox + + + + + + diff --git a/toxygen/ui/views/peer_screen.ui b/toxygen/ui/views/peer_screen.ui new file mode 100644 index 0000000..086dd18 --- /dev/null +++ b/toxygen/ui/views/peer_screen.ui @@ -0,0 +1,202 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 110 + 10 + 431 + 40 + + + + TextLabel + + + + + + 50 + 140 + 500 + 50 + + + + PushButton + + + + + + 50 + 100 + 500 + 23 + + + + CheckBox + + + + + + 50 + 300 + 500 + 161 + + + + GroupBox + + + + + + 40 + 40 + 251 + 23 + + + + RadioButton + + + true + + + + + + 40 + 80 + 251 + 23 + + + + RadioButton + + + + + + 40 + 120 + 251 + 23 + + + + RadioButton + + + + + + 380 + 100 + 101 + 41 + + + + PushButton + + + + + + + 50 + 60 + 67 + 20 + + + + TextLabel + + + + + + 130 + 60 + 411 + 20 + + + + TextLabel + + + + + + 50 + 210 + 500 + 50 + + + + PushButton + + + + + + 130 + 55 + 291 + 30 + + + + + + + diff --git a/toxygen/ui/views/profile_settings_screen.ui b/toxygen/ui/views/profile_settings_screen.ui new file mode 100644 index 0000000..1c899ab --- /dev/null +++ b/toxygen/ui/views/profile_settings_screen.ui @@ -0,0 +1,280 @@ + + + Form + + + + 0 + 0 + 900 + 680 + + + + Form + + + + + 30 + 10 + 161 + 30 + + + + TextLabel + + + + + + 30 + 90 + 161 + 30 + + + + TextLabel + + + + + + 30 + 50 + 421 + 30 + + + + + + + 30 + 130 + 421 + 30 + + + + + + + 520 + 30 + 311 + 30 + + + + + + + 40 + 180 + 131 + 20 + + + + TextLabel + + + + + + 40 + 210 + 831 + 60 + + + + TextLabel + + + true + + + + + + 40 + 280 + 371 + 30 + + + + PushButton + + + + + + 440 + 280 + 371 + 30 + + + + PushButton + + + + + + 520 + 80 + 321 + 34 + + + + PushButton + + + + + + 520 + 130 + 321 + 34 + + + + PushButton + + + + + + 60 + 380 + 161 + 30 + + + + TextLabel + + + + + + 50 + 420 + 421 + 30 + + + + + + + 50 + 470 + 421 + 30 + + + + + + + 500 + 420 + 381 + 20 + + + + TextLabel + + + + + + 60 + 580 + 381 + 20 + + + + TextLabel + + + + + + 40 + 630 + 831 + 34 + + + + PushButton + + + + + + 50 + 520 + 421 + 34 + + + + PushButton + + + + + + 500 + 470 + 381 + 20 + + + + TextLabel + + + + + + 40 + 330 + 371 + 34 + + + + PushButton + + + + + + 440 + 330 + 371 + 34 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/self_peer_screen.ui b/toxygen/ui/views/self_peer_screen.ui new file mode 100644 index 0000000..38e1f88 --- /dev/null +++ b/toxygen/ui/views/self_peer_screen.ui @@ -0,0 +1,119 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 50 + 120 + 67 + 20 + + + + TextLabel + + + + + + 50 + 250 + 500 + 50 + + + + PushButton + + + + + + 140 + 110 + 400 + 40 + + + + + + + 50 + 40 + 67 + 20 + + + + TextLabel + + + + + + 50 + 190 + 67 + 20 + + + + TextLabel + + + + + + 140 + 190 + 411 + 20 + + + + TextLabel + + + + + + 50 + 330 + 500 + 50 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/update_settings_screen.ui b/toxygen/ui/views/update_settings_screen.ui new file mode 100644 index 0000000..76e7c57 --- /dev/null +++ b/toxygen/ui/views/update_settings_screen.ui @@ -0,0 +1,67 @@ + + + Form + + + + 0 + 0 + 400 + 120 + + + + + 400 + 120 + + + + + 400 + 120 + + + + Form + + + + + 25 + 5 + 350 + 20 + + + + TextLabel + + + + + + 25 + 30 + 350 + 30 + + + + + + + 25 + 70 + 350 + 30 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/video_settings_screen.ui b/toxygen/ui/views/video_settings_screen.ui new file mode 100644 index 0000000..cfa36fb --- /dev/null +++ b/toxygen/ui/views/video_settings_screen.ui @@ -0,0 +1,77 @@ + + + Form + + + + 0 + 0 + 400 + 120 + + + + + 400 + 120 + + + + + 400 + 120 + + + + Form + + + + + 25 + 5 + 350 + 20 + + + + TextLabel + + + + + + 25 + 30 + 350 + 30 + + + + + + + 25 + 70 + 350 + 30 + + + + PushButton + + + + + + 25 + 70 + 350 + 30 + + + + + + + diff --git a/toxygen/ui/widgets.py b/toxygen/ui/widgets.py new file mode 100644 index 0000000..78e9a0a --- /dev/null +++ b/toxygen/ui/widgets.py @@ -0,0 +1,213 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from qtpy import QtCore, QtGui, QtWidgets +# from PyQt5.QtCore import pyqtSignal as Signal +from qtpy.QtCore import Signal + +import utils.ui as util_ui +import logging + +global LOG +LOG = logging.getLogger('app') + +class DataLabel(QtWidgets.QLabel): + """ + Label with elided text + """ + def setText(self, text): + try: + text = ''.join('\u25AF' if len(bytes(str(c), 'utf-8')) >= 4 else c for c in str(text)) + except Exception as e: + LOG.error(f"DataLabel::setText: {e}") + return + + try: + metrics = QtGui.QFontMetrics(self.font()) + text = metrics.elidedText(str(text), QtCore.Qt.ElideRight, self.width()) + except Exception as e: + # RuntimeError: wrapped C/C++ object of type DataLabel has been deleted + text = str(text) + + super().setText(text) + +class ComboBox(QtWidgets.QComboBox): + + def __init__(self, *args): + super().__init__(*args) + self.view().setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) + + +class CenteredWidget(QtWidgets.QWidget): + + def __init__(self): + super().__init__() + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.center() + + def center(self): + qr = self.frameGeometry() + cp = QtWidgets.QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + +class DialogWithResult(QtWidgets.QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self._result = None + + def get_result(self): + return self._result + + result = property(get_result) + + def close_with_result(self, result): + self._result = result + self.close() + + +class LineEdit(QtWidgets.QLineEdit): + + def __init__(self, parent=None): + super().__init__(parent) + + def contextMenuEvent(self, event): + menu = create_menu(self.createStandardContextMenu()) + menu.exec_(event.globalPos()) + del menu + + +class QRightClickButton(QtWidgets.QPushButton): + """ + Button with right click support + """ + # FixMe: AttributeError: module 'qtpy.QtCore' has no attribute 'pyqtSignal' + rightClicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.rightClicked.emit() + else: + super().mousePressEvent(event) + + +class RubberBand(QtWidgets.QRubberBand): + + def __init__(self): + super().__init__(QtWidgets.QRubberBand.Rectangle, None) + self.setPalette(QtGui.QPalette(QtCore.Qt.transparent)) + self.pen = QtGui.QPen(QtCore.Qt.blue, 4) + self.pen.setStyle(QtCore.Qt.SolidLine) + self.painter = QtGui.QPainter() + + def paintEvent(self, event): + + self.painter.begin(self) + self.painter.setPen(self.pen) + self.painter.drawRect(event.rect()) + self.painter.end() + + +class RubberBandWindow(QtWidgets.QWidget): + + def __init__(self, parent): + super().__init__() + self.parent = parent + self.setMouseTracking(True) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) + self.showFullScreen() + self.setWindowOpacity(0.5) + self.rubberband = RubberBand() + self.rubberband.setWindowFlags(self.rubberband.windowFlags() | QtCore.Qt.FramelessWindowHint) + self.rubberband.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + def mousePressEvent(self, event): + self.origin = event.pos() + self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize())) + self.rubberband.show() + QtWidgets.QWidget.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + if self.rubberband.isVisible(): + self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized()) + left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height())) + right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height())) + top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y()) + bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height()) + self.setMask(left + right + top + bottom) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + self.rubberband.setHidden(True) + self.close() + else: + super().keyPressEvent(event) + + +def create_menu(menu): + """ + :return translated menu + """ + for action in menu.actions(): + text = action.text() + if 'Link Location' in text: + text = text.replace('Copy &Link Location', + util_ui.tr("Copy link location")) + elif '&Copy' in text: + text = text.replace('&Copy', util_ui.tr("Copy")) + elif 'All' in text: + text = text.replace('Select All', util_ui.tr("Select all")) + elif 'Delete' in text: + text = text.replace('Delete', util_ui.tr("Delete")) + elif '&Paste' in text: + text = text.replace('&Paste', util_ui.tr("Paste")) + elif 'Cu&t' in text: + text = text.replace('Cu&t', util_ui.tr("Cut")) + elif '&Undo' in text: + text = text.replace('&Undo', util_ui.tr("Undo")) + elif '&Redo' in text: + text = text.replace('&Redo', util_ui.tr("Redo")) + else: + menu.removeAction(action) + continue + action.setText(text) + return menu + + +class MultilineEdit(CenteredWidget): + + def __init__(self, title, text, save): + super(MultilineEdit, self).__init__() + self.resize(350, 200) + self.setMinimumSize(QtCore.QSize(350, 200)) + self.setMaximumSize(QtCore.QSize(350, 200)) + self.setWindowTitle(title) + self.edit = QtWidgets.QTextEdit(self) + self.edit.setGeometry(QtCore.QRect(0, 0, 350, 150)) + self.edit.setText(text) + self.button = QtWidgets.QPushButton(self) + self.button.setGeometry(QtCore.QRect(0, 150, 350, 50)) + self.button.setText(util_ui.tr("Save")) + self.button.clicked.connect(self.button_click) + self.center() + self.save = save + + def button_click(self): + self.save(self.edit.toPlainText()) + self.close() + + +class LineEditWithEnterSupport(LineEdit): + + def __init__(self, enter_action, parent=None): + super().__init__(parent) + self._action = enter_action + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Return: + self._action() + else: + super().keyPressEvent(event) diff --git a/toxygen/ui/widgets_factory.py b/toxygen/ui/widgets_factory.py new file mode 100644 index 0000000..08861a4 --- /dev/null +++ b/toxygen/ui/widgets_factory.py @@ -0,0 +1,102 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +from ui.main_screen_widgets import * +from ui.menu import * +from ui.groups_widgets import * +from ui.peer_screen import * +from ui.self_peer_screen import * +from ui.group_invites_widgets import * +from ui.group_settings_widgets import * +from ui.group_bans_widgets import * +from ui.profile_settings_screen import ProfileSettings + +class WidgetsFactory: + + def __init__(self, settings, profile, profile_manager, contacts_manager, file_transfer_handler, smiley_loader, + plugin_loader, toxes, version, groups_service, history, contacts_provider): + self._settings = settings + self._profile = profile + self._profile_manager = profile_manager + self._contacts_manager = contacts_manager + self._file_transfer_handler = file_transfer_handler + self._smiley_loader = smiley_loader + self._plugin_loader = plugin_loader + self._toxes = toxes + self._version = version + self._groups_service = groups_service + self._history = history + self._contacts_provider = contacts_provider + + def create_screenshot_window(self, *args): + return ScreenShotWindow(self._file_transfer_handler, self._contacts_manager, *args) + + def create_welcome_window(self): + return WelcomeScreen(self._settings) + + def create_profile_settings_window(self): + return ProfileSettings(self._profile, self._profile_manager, self._settings, self._toxes) + + def create_network_settings_window(self): + return NetworkSettings(self._settings, self._profile.restart) + + def create_audio_settings_window(self): + return AudioSettings(self._settings) + + def create_video_settings_window(self): + try: + with ts.ignoreStdout(): import cv2 + except ImportError: + cv2 = None + if cv2 is None: return None + return VideoSettings(self._settings) + + def create_update_settings_window(self): + return UpdateSettings(self._settings, self._version) + + def create_plugins_settings_window(self): + return PluginsSettings(self._plugin_loader) + + def create_add_contact_window(self, tox_id): + return AddContact(self._settings, self._contacts_manager, tox_id) + + def create_privacy_settings_window(self): + return PrivacySettings(self._contacts_manager, self._settings) + + def create_interface_settings_window(self): + return InterfaceSettings(self._settings, self._smiley_loader) + + def create_notification_settings_window(self): + return NotificationsSettings(self._settings) + + def create_smiley_window(self, parent): + return SmileyWindow(parent, self._smiley_loader) + + def create_sticker_window(self): + return StickerWindow(self._file_transfer_handler, self._contacts_manager) + + def create_group_screen_window(self): + return CreateGroupScreen(self._groups_service, self._profile) + + def create_join_group_screen_window(self): + return JoinGroupScreen(self._groups_service, self._profile) + + def create_search_screen(self, messages): + return SearchScreen(self._contacts_manager, self._history, messages, messages.parent()) + + def create_peer_screen_window(self, group, peer_id): + return PeerScreen(self._contacts_manager, self._groups_service, group, peer_id) + + def create_self_peer_screen_window(self, group): + return SelfPeerScreen(self._contacts_manager, self._groups_service, group) + + def create_group_invites_window(self): + return GroupInvitesScreen(self._groups_service, self._profile, self._contacts_provider) + + def create_group_management_screen(self, group): + return GroupManagementScreen(self._groups_service, group) + + @staticmethod + def create_group_settings_screen(group): + return GroupSettingsScreen(group) + + def create_groups_bans_screen(self, group): + return GroupBansScreen(self._groups_service, group) diff --git a/toxygen/updater/__init__.py b/toxygen/updater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/updater/updater.py b/toxygen/updater/updater.py new file mode 100644 index 0000000..0eb81f3 --- /dev/null +++ b/toxygen/updater/updater.py @@ -0,0 +1,129 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import utils.util as util +import utils.ui as util_ui +import os +import platform +import urllib +from qtpy import QtNetwork, QtCore +import subprocess + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) + +TIMEOUT=10 + +def connection_available(): + return False + try: + urllib.request.urlopen('http://216.58.192.142', timeout=TIMEOUT) # google.com + return True + except: + return False + +def updater_available(): + if is_from_sources(): + return os.path.exists(util.curr_directory() + '/toxygen_updater.py') + elif platform.system() == 'Windows': + return os.path.exists(util.curr_directory() + '/toxygen_updater.exe') + else: + return os.path.exists(util.curr_directory() + '/toxygen_updater') + + +def check_for_updates(current_version, settings): + major, minor, patch = list(map(lambda x: int(x), current_version.split('.'))) + versions = generate_versions(major, minor, patch) + for version in versions: + if send_request(version, settings): + return version + return None # no new version was found + + +def is_from_sources(): + return __file__.endswith('.py') + + +def test_url(version): + return 'https://github.com/toxygen-project/toxygen/releases/tag/v' + version + + +def get_url(version): + if is_from_sources(): + return 'https://github.com/toxygen-project/toxygen/archive/v' + version + '.zip' + else: + if platform.system() == 'Windows': + name = 'toxygen_windows.zip' + elif util.is_64_bit(): + name = 'toxygen_linux_64.tar.gz' + else: + name = 'toxygen_linux.tar.gz' + return 'https://github.com/toxygen-project/toxygen/releases/download/v{}/{}'.format(version, name) + + +def get_params(url, version): + if is_from_sources(): + if platform.system() == 'Windows': + return ['python', 'toxygen_updater.py', url, version] + else: + return ['python3', 'toxygen_updater.py', url, version] + elif platform.system() == 'Windows': + return [util.curr_directory() + '/toxygen_updater.exe', url, version] + else: + return ['./toxygen_updater', url, version] + + +def download(version): + os.chdir(util.curr_directory()) + url = get_url(version) + params = get_params(url, version) + LOG.info('Updating Toxygen') + try: + subprocess.Popen(params) + except Exception as ex: + LOG.error('running updater failed with ' + str(ex)) + + +def send_request(version, settings): + netman = QtNetwork.QNetworkAccessManager() + proxy = QtNetwork.QNetworkProxy() + if settings['proxy_type']: + proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy if settings['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) + proxy.setHostName(settings['proxy_host']) + proxy.setPort(settings['proxy_port']) + netman.setProxy(proxy) + url = test_url(version) + try: + request = QtNetwork.QNetworkRequest() + request.setUrl(QtCore.QUrl(url)) + reply = netman.get(request) + while not reply.isFinished(): + QtCore.QThread.msleep(1) + QtCore.QCoreApplication.processEvents() + attr = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute) + return attr is not None and 200 <= attr < 300 + except Exception as ex: + LOG.error('TOXYGEN UPDATER ' + str(ex)) + return False + + +def generate_versions(major, minor, patch): + new_major = '.'.join([str(major + 1), '0', '0']) + new_minor = '.'.join([str(major), str(minor + 1), '0']) + new_patch = '.'.join([str(major), str(minor), str(patch + 1)]) + return new_major, new_minor, new_patch + + +def start_update_if_needed(version, settings): + updating = False + if settings['update'] and updater_available() and connection_available(): # auto update + version = check_for_updates(version, settings) + if version is not None: + if settings['update'] == 2: + download(version) + updating = True + else: + reply = util_ui.question(util_ui.tr('Update for Toxygen was found. Download and install it?')) + if reply: + download(version) + updating = True + return updating diff --git a/toxygen/user_data/__init__.py b/toxygen/user_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/user_data/backup_service.py b/toxygen/user_data/backup_service.py new file mode 100644 index 0000000..9f3a051 --- /dev/null +++ b/toxygen/user_data/backup_service.py @@ -0,0 +1,42 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os.path + +from utils.util import get_profile_name_from_path, join_path + +class BackupService: + + def __init__(self, settings, profile_manager): + self._settings = settings + self._profile_name = get_profile_name_from_path(profile_manager.get_path()) + + settings.settings_saved_event.add_callback(self._settings_saved) + profile_manager.profile_saved_event.add_callback(self._profile_saved) + + def _settings_saved(self, data): + if not self._check_if_should_save_backup(): + return + + file_path = join_path(self._get_backup_directory(), self._profile_name + '.json') + + with open(file_path, 'wt') as fl: + fl.write(data) + + def _profile_saved(self, data): + if not self._check_if_should_save_backup(): + return + + file_path = join_path(self._get_backup_directory(), self._profile_name + '.tox') + + with open(file_path, 'wb') as fl: + fl.write(data) + + def _check_if_should_save_backup(self): + backup_directory = self._get_backup_directory() + if backup_directory is None: + return False + + return os.path.exists(backup_directory) and os.path.isdir(backup_directory) + + def _get_backup_directory(self): + return self._settings['backup_directory'] diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py new file mode 100644 index 0000000..a6f5df0 --- /dev/null +++ b/toxygen/user_data/profile_manager.py @@ -0,0 +1,107 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import utils.util as util +import os + +from user_data.settings import Settings +from common.event import Event +from user_data.settings import get_user_config_path + +global LOG +import logging +LOG = logging.getLogger('app.'+__name__) +from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE + +class ProfileManager: + """ + Class with methods for search, load and save profiles + """ + def __init__(self, toxes, path, app=None): + assert path + self._toxes = toxes + self._path = path + assert path + self._app = app + self._directory = os.path.dirname(path) + self._profile_saved_event = Event() + # create /avatars if not exists: + avatars_directory = util.join_path(self._directory, 'avatars') + if not os.path.exists(avatars_directory): + os.makedirs(avatars_directory) + + # Properties + + def get_profile_saved_event(self): + return self._profile_saved_event + + profile_saved_event = property(get_profile_saved_event) + + # Public methods + + def open_profile(self): + with open(self._path, 'rb') as fl: + data = fl.read() + if data: + return data + else: + raise IOError('Save file has zero size!') + + def get_dir(self): + return self._directory + + def get_path(self): + return self._path + + def save_profile(self, data): + if self._toxes.has_password(): + data = self._toxes.pass_encrypt(data) + profile_path = self._path.replace('.json', '.tox') + try: + suf = f"{os.getpid()}" + with open(profile_path+suf, 'wb') as fl: + fl.write(data) + stat = os.stat(profile_path+suf) + if hasattr(stat, 'st_blocks'): + assert stat.st_blocks > 0, f"Zero length file {profile_path+suf}" + os.rename(profile_path+suf,profile_path) + LOG_INFO('Profile saved successfully to' +profile_path) + except Exception as e: + LOG_WARN(f"Profile save failed to {profile_path}\n{e}") + + self._profile_saved_event(data) + + def export_profile(self, settings, new_path, use_new_path): + profile_path = self._path.replace('.json', '.tox') + with open(profile_path, 'rb') as fin: + data = fin.read() + path = new_path + os.path.basename(profile_path) + with open(path, 'wb') as fout: + fout.write(data) + LOG.info('Profile exported successfully to ' +path) + util.copy(os.path.join(self._directory, 'avatars'), + os.path.join(new_path, 'avatars')) + if use_new_path: + profile_path = os.path.join(new_path, os.path.basename(profile_path)) + self._directory = new_path + settings.update_path(new_path) + + @staticmethod + def find_profiles(): + """ + Find available tox profiles + """ + path = get_user_config_path() + result = [] + # check default path + if not os.path.exists(path): + os.makedirs(path) + for fl in os.listdir(path): + if fl.endswith('.tox'): + name = fl[:-4] + result.append((path, name)) + path = util.get_base_directory(__file__) + # check current directory + for fl in os.listdir(path): + if fl.endswith('.tox'): + name = fl[:-4] + result.append((path + '/', name)) + return result diff --git a/toxygen/user_data/settings.py b/toxygen/user_data/settings.py new file mode 100644 index 0000000..c87eec3 --- /dev/null +++ b/toxygen/user_data/settings.py @@ -0,0 +1,428 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os +from platform import system +import json + +from utils import util +from utils.util import log, join_path +from common.event import Event +import utils.ui as util_ui +import utils.util as util_utils +import user_data +from toxygen_wrapper.tests import support_testing as ts + +global LOG +import logging +LOG = logging.getLogger('settings') + +def merge_args_into_settings(args, settings): + if args: + if not hasattr(args, 'audio'): + LOG.warn('No audio ' +repr(args)) + settings['audio'] = getattr(args, 'audio') + if not hasattr(args, 'video'): + LOG.warn('No video ' +repr(args)) + settings['video'] = getattr(args, 'video') + for key in settings.keys(): + # proxy_type proxy_port proxy_host + not_key = 'not_' +key + if hasattr(args, key): + val = getattr(args, key) + if type(val) == bytes: + # proxy_host - ascii? + # filenames - ascii? + val = str(val, 'UTF-8') + settings[key] = val + elif hasattr(args, not_key): + val = not getattr(args, not_key) + settings[key] = val + clean_settings(settings) + return + +def clean_settings(self): + # failsafe to ensure C tox is bytes and Py settings is str + + # overrides + self['mirror_mode'] = False + # REQUIRED!! + if not os.path.exists('/proc/sys/net/ipv6'): + LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist') + self['ipv6_enabled'] = False + + if 'proxy_type' in self and self['proxy_type'] == 0: + self['proxy_host'] = '' + self['proxy_port'] = 0 + + if 'proxy_type' in self and self['proxy_type'] != 0 and \ + 'proxy_host' in self and self['proxy_host'] != '' and \ + 'proxy_port' in self and self['proxy_port'] != 0: + if 'udp_enabled' in self and self['udp_enabled']: + # We don't currently support UDP over proxy. + LOG.info("UDP enabled and proxy set: disabling UDP") + self['udp_enabled'] = False + if 'local_discovery_enabled' in self and self['local_discovery_enabled']: + LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled") + self['local_discovery_enabled'] = False + if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']: + LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled") + self['dht_announcements_enabled'] = False + + if 'auto_accept_path' in self and \ + type(self['auto_accept_path']) == bytes: + self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8') + + for key in Settings.get_default_settings(): + if key not in self: continue + if type(self[key]) == bytes: + LOG.warn('bytes setting in: ' +key \ + +' ' + repr(self[key])) + # ascii? + # self[key] = str(self[key], 'utf-8') + LOG.debug("Cleaned settings") + +def get_user_config_path(): + system = util_utils.get_platform() + if system == 'Windows': + return os.path.join(os.getenv('APPDATA'), 'Tox/') + elif system == 'Darwin': + return os.path.join(os.getenv('HOME'), 'Library/Application Support/Tox/') + else: + return os.path.join(os.getenv('HOME'), '.config/tox/') + +def supported_languages(): + return { + 'English': 'en_EN', + 'French': 'fr_FR', + 'Russian': 'ru_RU', + 'Ukrainian': 'uk_UA' + } + +def built_in_themes(): + return { + 'dark': 'dark_style.qss', + 'default': 'style.qss' + } + +#def get_global_settings_path(): +# return os.path.join(get_base_directory(), 'toxygen.json') + +def is_active_profile(profile_path): + sFile = profile_path + '.lock' + if not os.path.isfile(sFile): + return False + try: + import psutil + except Exception as e: + return True + with open(sFile, 'rb') as iFd: + sPid = iFd.read() + if sPid and int(sPid.strip()) in psutil.pids(): + return True + LOG.debug('Unlinking stale lock file ' +sFile) + try: + os.unlink(sFile) + except: + pass + return False + +class Settings(dict): + """ + Settings of current profile + global app settings + """ + + def __init__(self, toxes, json_path, app): + self._toxes = toxes + self._app = app + self._path = app._path + self._args = app._args + self._oArgs = app._args + self._log = lambda l: LOG.log(self._oArgs.loglevel, l) + self._profile_path = json_path.replace('.json', '.tox') + + self._settings_saved_event = Event() + path = json_path.replace('.tox', '.json') + if path and os.path.isfile(path): + try: + with open(path, 'rb') as fl: + data = fl.read() + if self._toxes.is_data_encrypted(data): + data = self._toxes.pass_decrypt(data) + info = json.loads(str(data, 'utf-8')) + LOG.debug('Parsed settings from: ' + str(path)) + except Exception as ex: + title = f"Error opening/parsing settings file:" + text = title +f"\n{path}\n" + LOG.error(text +str(ex)) + util_ui.message_box(text, title) + info = Settings.get_default_settings(app._args) + user_data.settings.clean_settings(info) + else: + LOG.debug('get_default_settings for: ' + repr(path)) + info = Settings.get_default_settings(app._args) + + if not path or not os.path.exists(path): + merge_args_into_settings(app._args, info) + else: + aC = self._changed(app._args, info) + if aC: + title = 'Override profile with commandline - ' + if path: + title += os.path.basename(path) + text = 'Override profile with command-line settings? \n' + # text += '\n'.join([str(key) +'=' +str(val) for + # key,val in self._changed(app._args).items()]) + text += repr(aC) + reply = util_ui.question(text, title) + if reply: + merge_args_into_settings(app._args, info) + info['audio'] = getattr(app._args, 'audio') + info['video'] = getattr(app._args, 'video') + if getattr(app._args, 'trace_enabled'): + info['trace_enabled'] = getattr(app._args, 'trace_enabled') + else: + LOG.warn("app._args, 'trace_enabled") + info['trace_enabled'] = False + super().__init__(info) + self._upgrade() + + LOG.info('Parsed settings from: ' + str(path)) + ex = f"self=id(self) {self}" + LOG.debug(ex) + + self.save() + self.locked = False + self.closing = False + self.unlockScreen = False + + # Properties + + def get_settings_saved_event(self): + return self._settings_saved_event + + settings_saved_event = property(get_settings_saved_event) + + # Public methods + + def save(self): + text = json.dumps(self) + if self._toxes.has_password(): + text = bytes(self._toxes.pass_encrypt(bytes(text, 'utf-8'))) + else: + text = bytes(text, 'utf-8') + json_path = os.path.join(get_user_config_path(), 'toxygen.json') + tmp = json_path + str(os.getpid()) + try: + with open(tmp, 'wb') as fl: + fl.write(text) + if os.path.exists(json_path+'.bak'): + os.remove(json_path+'.bak') + os.rename(json_path, json_path+'.bak') + os.rename(tmp, json_path) + except Exception as e: + LOG.warn(f'Error saving to {json_path} ' +str(e)) + else: + self._settings_saved_event(text) + + def close(self): + path = self._profile_path + '.lock' + if os.path.isfile(path): + os.remove(path) + + def set_active_profile(self, profile_path): + """ + Mark current profile as active + """ + if not profile_path: + profile_path = self.get_auto_profile() + + path = profile_path + '.lock' + try: + import shutil + except: + pass + else: + shutil.copy2(profile_path, path) + # need to open this with the same perms as _profile_path + # copy profile_path and then write? + with open(path, 'wb') as fl: + fl.write(bytes(str(os.getpid()), 'ascii')) + + def export(self, path): + text = json.dumps(self) + name = os.path.basename(self._path) + with open(join_path(path, str(name)), 'w') as fl: + fl.write(text) + + def update_path(self, new_path): + self._path = new_path + self.save() + + # Static methods + + @staticmethod + def get_auto_profile(appdir=None): + if appdir is None: + appdir = ts.get_user_config_path() + # self._path = + p = os.path.join(appdir, 'toxygen.json') + if not os.path.isfile(p): + return None + try: + with open(p, 'rb') as fl: + data = fl.read() + if self._toxes.is_data_encrypted(data): + data = self._toxes.pass_decrypt(data) + except Exception as ex: + LOG.warn(f"fl.read {p}: {ex}") + return None + try: + auto = json.loads(str(data, 'utf-8')) + except Exception as ex: + LOG.warn(f"json.loads {p}: {ex}") + auto = {} + if 'profile_path' in auto: + path = str(auto['profile_path']) + if not os.path.isabs(path): + path = join_path(path, os.path.dirname(os.path.realpath(__file__))) + if os.path.isfile(path): + return path + return None + + @staticmethod + def supported_languages(): + # backwards + return supported_languages() + + @staticmethod + def set_auto_profile(path): + p = os.path.join(os.path.dirname(path), 'toxygen.json') + if os.path.isfile(p): + with open(p) as fl: + data = fl.read() + data = json.loads(data) + else: + data = {} + data['profile_path'] = str(path) + with open(p, 'w') as fl: + fl.write(json.dumps(data)) + + @staticmethod + def reset_auto_profile(): + appdir = ts.get_user_config_path() + p = os.path.join(appdir, 'toxygen.json') + if os.path.isfile(p): + with open(p) as fl: + data = fl.read() + data = json.loads(data) + else: + data = {} + if 'profile_path' in data: + del data['profile_path'] + with open(p, 'w') as fl: + fl.write(json.dumps(data)) + + @staticmethod + def get_default_settings(args=None): + """ + Default profile settings + """ + retval = { + # FixMe: match? /var/local/src/c-toxcore/toxcore/tox.h + 'ipv6_enabled': True, + 'udp_enabled': True, + 'trace_enabled': False, + 'local_discovery_enabled': True, + 'dht_announcements_enabled': True, + 'proxy_type': 0, + 'proxy_host': '', + 'proxy_port': 0, + 'start_port': 0, + 'end_port': 0, + 'tcp_port': 0, + 'local_discovery_enabled': True, + 'hole_punching_enabled': False, + # tox_log_cb *log_callback; + 'experimental_thread_safety': False, + # operating_system + + 'theme': 'default', + 'notifications': False, + 'sound_notifications': False, + 'language': 'English', + 'calls_sound': False, # was True + + 'save_history': True, + 'save_unsent_only': False, + 'allow_inline': True, + 'allow_auto_accept': True, + 'auto_accept_path': None, + 'sorting': 0, + 'auto_accept_from_friends': [], + 'paused_file_transfers': {}, + 'resend_files': True, + 'friends_aliases': [], + 'show_avatars': False, + 'typing_notifications': False, + 'blocked': [], + 'plugins': [], + 'notes': {}, + 'smileys': True, + 'smiley_pack': 'default', + 'mirror_mode': False, + 'width': 920, + 'height': 500, + 'x': 400, + 'y': 400, + 'message_font_size': 14, + 'unread_color': 'red', + 'compact_mode': False, + 'identicons': True, + 'show_welcome_screen': False, + 'close_app': 0, + 'font': 'Times New Roman', + 'update': 0, + 'group_notifications': True, + 'download_nodes_list': False, # + 'download_nodes_url': 'https://nodes.tox.chat/json', + 'notify_all_gc': False, + 'backup_directory': None, + + 'audio': {'input': -1, + 'output': -1, + 'enabled': True}, + 'video': {'device': -1, + 'width': 320, + 'height': 240, + 'x': 0, + 'y': 0}, + 'current_nodes': None, + 'network': 'new', + 'tray_icon': False, + } + return retval + + # Private methods + + def _upgrade(self): + default = Settings.get_default_settings() + for key in default: + if key not in self: + print(key) + self[key] = default[key] + + def _changed(self, aArgs, info): + aRet = dict() + default = Settings.get_default_settings() + for key in default: + if key in ['audio', 'video']: continue + if key not in aArgs.__dict__: continue + val = aArgs.__dict__[key] + if val in ['0.0.0.0']: continue + if key in aArgs.__dict__ and key not in info: + # dunno = network + continue + if key in aArgs.__dict__ and info[key] != val: + aRet[key] = val + return aRet + diff --git a/toxygen/user_data/toxes.py b/toxygen/user_data/toxes.py new file mode 100644 index 0000000..84b8636 --- /dev/null +++ b/toxygen/user_data/toxes.py @@ -0,0 +1,25 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +class ToxES: + + def __init__(self, tox_encrypt_save): + self._tox_encrypt_save = tox_encrypt_save + self._password = None + + def set_password(self, password): + self._password = password + + def has_password(self): + return bool(self._password) + + def is_password(self, password): + return self._password == password + + def is_data_encrypted(self, data): + return len(data) > 0 and self._tox_encrypt_save.is_data_encrypted(data) + + def pass_encrypt(self, data): + return self._tox_encrypt_save.pass_encrypt(data, self._password) + + def pass_decrypt(self, data): + return self._tox_encrypt_save.pass_decrypt(data, self._password) diff --git a/toxygen/util.py b/toxygen/util.py deleted file mode 100644 index 471e850..0000000 --- a/toxygen/util.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import time -import shutil - -program_version = '0.2.2' - - -def log(data): - with open(curr_directory() + '/logs.log', 'a') as fl: - fl.write(str(data) + '\n') - - -def curr_directory(): - return os.path.dirname(os.path.realpath(__file__)) - - -def curr_time(): - return time.strftime('%H:%M') - - -def copy(src, dest): - if not os.path.exists(dest): - os.makedirs(dest) - src_files = os.listdir(src) - for file_name in src_files: - full_file_name = os.path.join(src, file_name) - if os.path.isfile(full_file_name): - shutil.copy(full_file_name, dest) - else: - copy(full_file_name, os.path.join(dest, file_name)) - - -def convert_time(t): - sec = int(t) - time.timezone - m, s = divmod(sec, 60) - h, m = divmod(m, 60) - d, h = divmod(h, 24) - return '%02d:%02d' % (h, m) - - -class Singleton: - _instance = None - - def __init__(self): - self.__class__._instance = self - - @classmethod - def get_instance(cls): - return cls._instance diff --git a/toxygen/utils/__init__.py b/toxygen/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/utils/ui.py b/toxygen/utils/ui.py new file mode 100644 index 0000000..f7d20e8 --- /dev/null +++ b/toxygen/utils/ui.py @@ -0,0 +1,56 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from qtpy import QtWidgets + +import utils.util as util + +def tr(s): + return QtWidgets.QApplication.translate('Toxygen', s) + + +def question(text, title=None): + reply = QtWidgets.QMessageBox.question(None, title or 'Toxygen', text, + QtWidgets.QMessageBox.Yes, + QtWidgets.QMessageBox.No) + return reply == QtWidgets.QMessageBox.Yes + + +def message_box(text, title=None): + m_box = QtWidgets.QMessageBox() + m_box.setText(tr(text)) + m_box.setWindowTitle(title or 'Toxygen') + m_box.exec_() + + +def text_dialog(text, title='', default_value=''): + text, ok = QtWidgets.QInputDialog.getText(None, title, text, QtWidgets.QLineEdit.Normal, default_value) + + return text, ok + + +def directory_dialog(caption=''): + return QtWidgets.QFileDialog.getExistingDirectory(None, caption, util.curr_directory(), + QtWidgets.QFileDialog.DontUseNativeDialog) + + +def file_dialog(caption, file_filter=None): + return QtWidgets.QFileDialog.getOpenFileName(None, caption, util.curr_directory(), file_filter, + options=QtWidgets.QFileDialog.DontUseNativeDialog) + + +def save_file_dialog(caption, file_filter=None): + return QtWidgets.QFileDialog.getSaveFileName(None, caption, util.curr_directory(), + filter=file_filter, + options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) + + +def close_all_windows(): + QtWidgets.QApplication.closeAllWindows() + + +def copy_to_clipboard(text): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(text) + + +# TODO: all dialogs diff --git a/toxygen/utils/util.py b/toxygen/utils/util.py new file mode 100644 index 0000000..70cc7ff --- /dev/null +++ b/toxygen/utils/util.py @@ -0,0 +1,190 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import datetime +import os +import platform +import re +import shutil +import sys +import time + + +def cached(func): + saved_result = None + + def wrapped_func(): + nonlocal saved_result + if saved_result is None: + saved_result = func() + + return saved_result + + return wrapped_func + +oFD=None +def log(data=None): + global oFD + if not oFD: + if 'TMPDIR' in os.environ: + logdir = os.environ['TMPDIR'] + else: + logdir = '/tmp' + try: + oFD = open(join_path(logdir, 'toxygen.log'), 'a') + except Exception as ex: + oFD = None + print(f"ERROR: opening toxygen.log: {ex}") + return '' + if data is None: return oFD + try: + oFD.write(str(data) +'\n') + except Exception as ex: + print(f"ERROR: writing to toxygen.log: {ex}") + return data + +def curr_directory(current_file=None): + return os.path.dirname(os.path.realpath(current_file or __file__)) + + +def get_base_directory(current_file=None): + return os.path.dirname(curr_directory(current_file or __file__)) + + +@cached +def get_images_directory(): + return get_app_directory('images') + + +@cached +def get_styles_directory(): + return get_app_directory('styles') + + +@cached +def get_sounds_directory(): + return get_app_directory('sounds') + + +@cached +def get_stickers_directory(): + return get_app_directory('stickers') + + +@cached +def get_smileys_directory(): + return get_app_directory('smileys') + + +@cached +def get_translations_directory(): + return get_app_directory('translations') + + +@cached +def get_plugins_directory(): + return get_app_directory('plugins') + + +@cached +def get_libs_directory(): + return get_app_directory('libs') + + +def get_app_directory(directory_name): + return os.path.join(get_base_directory(), directory_name) + + +def get_profile_name_from_path(path): + return os.path.basename(path)[:-4] + + +def get_views_path(view_name): + ui_folder = os.path.join(get_base_directory(), 'ui') + views_folder = os.path.join(ui_folder, 'views') + + return os.path.join(views_folder, view_name + '.ui') + + +def curr_time(): + return time.strftime('%H:%M') + + +def get_unix_time(): + return int(time.time()) + + +def join_path(a, b): + return os.path.join(a, b) + + +def file_exists(file_path): + return os.path.exists(file_path) + + +def copy(src, dest): + if not os.path.exists(dest): + os.makedirs(dest) + src_files = os.listdir(src) + for file_name in src_files: + full_file_name = os.path.join(src, file_name) + if os.path.isfile(full_file_name): + shutil.copy(full_file_name, dest) + else: + copy(full_file_name, os.path.join(dest, file_name)) + + +def remove(folder): + if os.path.isdir(folder): + shutil.rmtree(folder) + + +def convert_time(t): + offset = time.timezone + time_offset() * 60 + sec = int(t) - offset + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + return '%02d:%02d' % (h, m) + + +@cached +def time_offset(): + hours = int(time.strftime('%H')) + minutes = int(time.strftime('%M')) + sec = int(time.time()) - time.timezone + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + result = hours * 60 + minutes - h * 60 - m + return result + +def unix_time_to_long_str(unix_time): + date_time = datetime.datetime.utcfromtimestamp(unix_time) + + return date_time.strftime('%Y-%m-%d %H:%M:%S') + + +@cached +def is_64_bit(): + return sys.maxsize > 2 ** 32 + + +def is_re_valid(regex): + try: + re.compile(regex) + except re.error: + return False + else: + return True + + +@cached +def get_platform(): + return platform.system() + +def get_user_config_path(): + if get_platform() == 'Windows': + return os.getenv('APPDATA') + '/Tox/' + elif get_platform() == 'Darwin': + return os.getenv('HOME') + '/Library/Application Support/Tox/' + else: + return os.getenv('HOME') + '/.config/tox/' diff --git a/toxygen/widgets.py b/toxygen/widgets.py deleted file mode 100644 index 1e5cfe8..0000000 --- a/toxygen/widgets.py +++ /dev/null @@ -1,133 +0,0 @@ -try: - from PySide import QtCore, QtGui -except ImportError: - from PyQt4 import QtCore, QtGui - - -class DataLabel(QtGui.QLabel): - """ - Label with elided text - """ - def setText(self, text): - text = ''.join(c if c <= '\u10FFFF' else '\u25AF' for c in text) - metrics = QtGui.QFontMetrics(self.font()) - text = metrics.elidedText(text, QtCore.Qt.ElideRight, self.width()) - super().setText(text) - - -class CenteredWidget(QtGui.QWidget): - - def __init__(self): - super(CenteredWidget, self).__init__() - self.center() - - def center(self): - qr = self.frameGeometry() - cp = QtGui.QDesktopWidget().availableGeometry().center() - qr.moveCenter(cp) - self.move(qr.topLeft()) - - -class LineEdit(QtGui.QLineEdit): - - def __init__(self, parent=None): - super(LineEdit, self).__init__(parent) - - def contextMenuEvent(self, event): - menu = create_menu(self.createStandardContextMenu()) - menu.exec_(event.globalPos()) - del menu - - -class QRightClickButton(QtGui.QPushButton): - """ - Button with right click support - """ - - def __init__(self, parent): - super(QRightClickButton, self).__init__(parent) - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - self.emit(QtCore.SIGNAL("rightClicked()")) - else: - super(QRightClickButton, self).mousePressEvent(event) - - -class RubberBand(QtGui.QRubberBand): - - def __init__(self): - super(RubberBand, self).__init__(QtGui.QRubberBand.Rectangle, None) - self.setPalette(QtGui.QPalette(QtCore.Qt.transparent)) - self.pen = QtGui.QPen(QtCore.Qt.blue, 4) - self.pen.setStyle(QtCore.Qt.SolidLine) - self.painter = QtGui.QPainter() - - def paintEvent(self, event): - - self.painter.begin(self) - self.painter.setPen(self.pen) - self.painter.drawRect(event.rect()) - self.painter.end() - - -def create_menu(menu): - """ - :return translated menu - """ - for action in menu.actions(): - text = action.text() - if 'Link Location' in text: - text = text.replace('Copy &Link Location', - QtGui.QApplication.translate("MainWindow", "Copy link location", None, - QtGui.QApplication.UnicodeUTF8)) - elif '&Copy' in text: - text = text.replace('&Copy', QtGui.QApplication.translate("MainWindow", "Copy", None, - QtGui.QApplication.UnicodeUTF8)) - elif 'All' in text: - text = text.replace('Select All', QtGui.QApplication.translate("MainWindow", "Select all", None, - QtGui.QApplication.UnicodeUTF8)) - elif 'Delete' in text: - text = text.replace('Delete', QtGui.QApplication.translate("MainWindow", "Delete", None, - QtGui.QApplication.UnicodeUTF8)) - elif '&Paste' in text: - text = text.replace('&Paste', QtGui.QApplication.translate("MainWindow", "Paste", None, - QtGui.QApplication.UnicodeUTF8)) - elif 'Cu&t' in text: - text = text.replace('Cu&t', QtGui.QApplication.translate("MainWindow", "Cut", None, - QtGui.QApplication.UnicodeUTF8)) - elif '&Undo' in text: - text = text.replace('&Undo', QtGui.QApplication.translate("MainWindow", "Undo", None, - QtGui.QApplication.UnicodeUTF8)) - elif '&Redo' in text: - text = text.replace('&Redo', QtGui.QApplication.translate("MainWindow", "Redo", None, - QtGui.QApplication.UnicodeUTF8)) - else: - menu.removeAction(action) - continue - action.setText(text) - return menu - - -class MultilineEdit(CenteredWidget): - - def __init__(self, title, text, save): - super(MultilineEdit, self).__init__() - self.resize(350, 200) - self.setMinimumSize(QtCore.QSize(350, 200)) - self.setMaximumSize(QtCore.QSize(350, 200)) - self.setWindowTitle(title) - self.edit = QtGui.QTextEdit(self) - self.edit.setGeometry(QtCore.QRect(0, 0, 350, 150)) - self.edit.setText(text) - self.button = QtGui.QPushButton(self) - self.button.setGeometry(QtCore.QRect(0, 150, 350, 50)) - self.button.setText(QtGui.QApplication.translate("MainWindow", "Save", None, QtGui.QApplication.UnicodeUTF8)) - self.button.clicked.connect(self.button_click) - self.center() - self.save = save - - def button_click(self): - self.save(self.edit.toPlainText()) - self.close() -