22 Commits

Author SHA1 Message Date
098f295cb0 Fix group join by chat id 2018-01-08 22:40:52 +03:00
2fc90880b8 api update 2017-01-27 21:09:15 +03:00
1ab59ee422 group leave fix 2016-07-15 13:03:44 -07:00
735c88d5bf notifications fix, reconnect added 2016-07-15 03:10:41 -07:00
2745caa531 invite fix 2016-07-15 12:12:06 +03:00
4abd72e278 more callbacks, fixes, peers 2016-07-14 22:23:56 +03:00
f775203f4c some fixes 2016-07-13 16:12:15 +03:00
8a53fc8727 menu update + group leaving 2016-07-12 23:31:32 +03:00
bb4e80ca09 fixes, gc loading on start 2016-07-12 22:53:02 +03:00
3602b3433e contact.py fixes and right click menu update 2016-07-12 17:53:38 +03:00
dc96f66d8c updates and fixes 2016-07-12 15:47:17 +03:00
bae87c8d72 some fixes and image 2016-07-12 13:48:59 +03:00
4f42098d56 incoming messages 2016-07-11 22:47:39 +03:00
f13274882a group creation, invites and message sending (untested) 2016-07-11 22:19:35 +03:00
c8bdb32e86 rebase 2016-07-11 17:52:03 +03:00
3ad7d20827 history improvements, ui for creation finished, some updates 2016-07-11 17:39:45 +03:00
3aba7dffd2 ui: chat creation window 2016-07-11 17:39:44 +03:00
9b0c6e63ce bug fixes 2016-07-11 17:31:16 +03:00
6703cbd301 basecontact created 2016-07-11 17:31:16 +03:00
40d0b03227 icon and gc.py 2016-07-11 17:28:38 +03:00
c0601444d9 generated wrapper without callbacks docs and errors check 2016-07-11 17:28:38 +03:00
c767ebe530 enums and consts 2016-07-11 17:28:38 +03:00
1343 changed files with 9373 additions and 19738 deletions

9
.gitignore vendored
View File

@ -1,11 +1,11 @@
*.pyc
*.pyo
*.ui
toxygen/toxcore
tests/tests
tests/libs
tests/.cache
tests/__pycache__
tests/avatars
toxygen/libs
.idea
*~
@ -15,14 +15,9 @@ toxygen/libs
toxygen/build
toxygen/dist
*.spec
dist
dist/
toxygen/avatars
toxygen/__pycache__
/*.egg-info
/*.egg
html
Toxygen.egg-info
*.tox
.cache
*.db

View File

@ -1,53 +0,0 @@
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

View File

@ -10,10 +10,9 @@ include toxygen/smileys/animated/config.json
include toxygen/smileys/starwars/*.gif
include toxygen/smileys/starwars/*.png
include toxygen/smileys/starwars/config.json
include toxygen/smileys/ksk/*.png
include toxygen/smileys/ksk/config.json
include toxygen/styles/*.qss
include toxygen/styles/style.qss
include toxygen/translations/*.qm
include toxygen/libs/libtox.dll
include toxygen/libs/libsodium.a
include toxygen/bootstrap/nodes.json
include toxygen/libs/libtox64.dll
include toxygen/libs/libsodium64.a

100
README.md
View File

@ -1,66 +1,60 @@
# Toxygen
Toxygen is cross-platform [Tox](https://tox.chat/) client written in Python3
Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pure Python3.
[![Release](https://img.shields.io/github/release/xveduk/toxygen.svg?style=flat)](https://github.com/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)
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md)
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md)
### Supported OS: Linux and Windows
### Supported OS:
- Windows
- Linux
### Features:
###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
- 1v1 messages
- File transfers
- Audio calls
- Video calls
- Group chats
- Plugins support
- Desktop sharing
- Chat history
- Emoticons
- Stickers
- Screenshots
- Name lookups (toxme.io support)
- Save file encryption
- Profile import and export
- Faux offline messaging
- Faux offline file transfers
- Inline images
- Message splitting
- Proxy support
- Avatars
- Multiprofile
- Multilingual
- Sound notifications
- Contact aliases
- Contact blocking
- Typing notifications
- Changing nospam
- File resuming
- Read receipts
- NGC groups
###Downloads
[Releases](https://github.com/xveduk/toxygen/releases)
### Screenshots
[Download last stable version](https://github.com/xveduk/toxygen/archive/master.zip)
[Download develop version](https://github.com/xveduk/toxygen/archive/develop.zip)
###Screenshots
*Toxygen on Ubuntu and Windows*
![Ubuntu](/docs/ubuntu.png)
![Windows](/docs/windows.png)
## Forked
This hard-forked from https://github.com/toxygen-project/toxygen
```next_gen``` branch.
###Docs
[Check /docs/ for more info](/docs/)
https://git.plastiras.org/emdee/toxygen_wrapper needs packaging
is making a dependency. Just download it and copy the two directories
```wrapper``` and ```wrapper_tests``` into ```toxygen/toxygen```.
See ToDo.md to the current ToDo list.
If you install https://github.com/weechat/qweechat
you can have IRC and jabber in a window too. Start
[weechat](https://github.com/weechat/weechat) and
```
/relay weechat 9000 password
```
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!

52
ToDo.md
View File

@ -1,52 +0,0 @@
# Toxygen ToDo List
## Bugs
1. There is an agravating bug where new messages are not put in the
current window, and a messages waiting indicator appears. You have
to focus out of the window and then back in the window.
## Fix history
The code is in there but it's not working.
## 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.
## 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.

View File

@ -1,13 +0,0 @@
FROM ubuntu:16.04
RUN apt-get update && \
apt-get install build-essential libtool autotools-dev automake checkinstall cmake check git yasm libsodium-dev libopus-dev libvpx-dev pkg-config -y && \
git clone https://github.com/ingvar1995/toxcore.git --branch=ngc_rebase && \
cd toxcore && mkdir _build && cd _build && \
cmake .. && make && make install
RUN apt-get install portaudio19-dev python3-pyqt5 python3-pyaudio python3-pip -y && \
pip3 install numpy pydenticon opencv-python pyinstaller
RUN useradd -ms /bin/bash toxygen
USER toxygen

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bash
cd ~
git clone https://github.com/toxygen-project/toxygen.git --branch=next_gen
cd toxygen/toxygen
pyinstaller --windowed --icon=images/icon.ico main.py
cp -r styles dist/main/
find . -type f ! -name '*.qss' -delete
cp -r plugins dist/main/
mkdir -p dist/main/ui/views
cp -r ui/views dist/main/ui/
cp -r sounds dist/main/
cp -r smileys dist/main/
cp -r stickers dist/main/
cp -r bootstrap dist/main/
find . -type f ! -name '*.json' -delete
cp -r images dist/main/
cp -r translations dist/main/
find . -name "*.ts" -type f -delete
cd dist
mv main toxygen
cd toxygen
mv main toxygen
wget -O updater https://github.com/toxygen-project/toxygen_updater/releases/download/v0.1/toxygen_updater_linux_64
echo "[Paths]" >> qt.conf
echo "Prefix = PyQt5/Qt" >> qt.conf
cd ..
tar -zcvf toxygen_linux_64.tar.gz toxygen > /dev/null
rm -rf toxygen

View File

@ -1,19 +1,10 @@
# Compile Toxygen
#Compile Toxygen
You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/)
Use Dockerfile and build script from `build` directory:
Install PyInstaller:
``pip3 install pyinstaller``
1. Build image:
```
docker build -t toxygen .
```
``pyinstaller --windowed --icon images/icon.ico main.py``
2. Run container:
```
docker run -it toxygen bash
```
3. Execute `build.sh` script:
```./build.sh```
Don't forget to copy /images/, /sounds/, /translations/, /styles/, /smileys/, /stickers/ (and /libs/libtox.dll on Windows) to /dist/main/

View File

@ -1,6 +0,0 @@
# Contact us:
1) https://git.plastiras.org/emdee/toxygen/issues
2) Use Toxygen Tox Group (NGC) -
ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA

View File

@ -1,25 +1,20 @@
# Issues
#Issues
Help us find all bugs in Toxygen! Please provide following info:
- OS
- Toxygen version
- Toxygen executable info - python executable (.py), precompiled binary, from package etc.
- Toxygen executable info - .py or precompiled binary
- Steps to reproduce the bug
Want to see new feature in Toxygen?
[Ask for it!](https://git.plastiras.org/emdee/toxygen/issues)
Want to see new feature in Toxygen? [Ask for it!](https://github.com/xveduk/toxygen/issues)
# Pull requests
#Pull requests
Developer? Feel free to open pull request. Our dev team is small so we glad to get help.
Don't know what to do? Improve UI, fix
[issues](https://git.plastiras.org/emdee/toxygen/issues)
or implement features from our TODO list.
Don't know what to do? Improve UI, fix [issues](https://github.com/xveduk/toxygen/issues) or implement features from our TODO list.
You can find our TODO's in code, issues list and [here](/README.md). Also you can implement [plugins](/docs/plugins.md) for Toxygen.
Note that we have a lot of branches for different purposes. Master branch is for stable versions (releases) only, so I recommend to open PR's to develop branch. Development of next Toxygen version usually goes there. Other branches used for implementing different tasks such as file transfers improvements or audio calls implementation etc.
#Translations
# Translations
Help us translate Toxygen! Translation can be created using pylupdate (``pylupdate5 toxygen.pro``) and QT Linguist.
Help us translate Toxygen! Translation can be created using pyside-lupdate (``pyside-lupdate toxygen.pro``) and QT Linguist.

View File

@ -1,44 +1,59 @@
# How to install Toxygen
### Linux
## Use precompiled binary:
[Check our releases page](https://github.com/xveduk/toxygen/releases)
1. Install [c-toxcore](https://github.com/TokTok/c-toxcore/)
##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/)
2. Install PortAudio:
``sudo apt-get install portaudio19-dev``
3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5``
4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python``
5. Install [toxygen](https://git.plastiras.org/emdee/toxygen/)
6. Run toxygen using ``toxygen`` command.
3. Install toxygen:
``sudo pip3.4 install toxygen``
4 Run toxygen using ``toxygen`` command.
## From source code (recommended for developers)
### Windows
Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly recommended to use 64-bit Python.
1. [Download and install latest Python 3.4](https://www.python.org/downloads/windows/)
2. [Install PySide](https://pypi.python.org/pypi/PySide/1.2.4) (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)
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. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip)
7. Unpack archive
8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\
9. Run \toxygen\main.py.
### Linux
1. Install latest Python3:
``sudo apt-get install python3``
2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` or ``sudo pip3 install pyqt5``
3. Install [toxcore](https://github.com/TokTok/c-toxcore) with toxav support)
4. Install PyAudio:
``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``sudo pip3 install pyaudio``)
5. Install NumPy: ``sudo pip3 install numpy``
6. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python``
7. [Download toxygen](https://git.plastiras.org/emdee/toxygen/)
8. Unpack archive
9. Run app:
``python3 main.py``
Dependencies:
Optional: install toxygen using setup.py: ``python3 setup.py install``
1. Install latest Python3.4:
``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``
## Compile Toxygen
Check [compile.md](/docs/compile.md) for more info

View File

@ -1,6 +1,6 @@
# Plugins API
#Plugins API
In Toxygen plugin is single python module (.py file) and directory with data associated with it.
In Toxygen plugin is single python (supported Python 3.0 - 3.4) module (.py file) and directory with data associated with it.
Every module must contain one class derived from PluginSuperClass defined in [plugin_super_class.py](/src/plugins/plugin_super_class.py). Instance of this class will be created by PluginLoader class (defined in [plugin_support.py](/src/plugin_support.py) ). This class can enable/disable plugins and send data to it.
Every plugin has its own full name and unique short name (1-5 symbols). Main app can get it using special methods.
@ -12,13 +12,12 @@ 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 (ToxES instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save.
- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxEncryptSave instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save.
Plugin can override following methods:
- get_description - this method should return plugin description.
@ -45,13 +44,13 @@ Import statement will not work in case you import module that wasn't previously
About GUI:
GUI is available via PyQt5. Plugin can have no GUI at all.
It's strictly recommended to support both PySide and PyQt4 in GUI. Plugin can have no GUI at all.
Exceptions:
Plugin's methods MUST NOT raise exceptions.
# Examples
#Examples
You can find examples in [official repo](https://github.com/toxygen-project/toxygen_plugins)
You can find examples in [official repo](https://github.com/ingvar1995/toxygen_plugins)

View File

@ -1,22 +1,22 @@
# Plugins
#Plugins
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.5 - 3.6 module (.py file) and directory with plugin's data which provide some additional functionality.
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 module (.py file) and directory with plugin's data which provide some additional functionality.
# How to write plugin
#How to write plugin
Check [Plugin API](/docs/plugin_api.md) for more info
# How to install plugin
#How to install plugin
Toxygen comes without preinstalled plugins.
1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins => Import plugin)
2. Restart Toxygen or choose Plugins => Reload plugins in menu.
1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins -> Import plugin)
2. Restart Toxygen
## Note: /src/plugins/ should contain plugin_super_class.py and __init__.py
##Note: /src/plugins/ should contain plugin_super_class.py and __init__.py
# Plugins list
#Plugins list
WARNING: It is unsecure to install plugin not from this list!
[Main repo](https://github.com/toxygen-project/toxygen_plugins)
[Main repo](https://github.com/ingvar1995/toxygen_plugins)

View File

@ -1,4 +1,4 @@
# Smileys
#Smileys
Toxygen support smileys. Smiley is small picture which replaces some symbol or combination of symbols. If you want to create your own smiley pack, create directory in src/smileys/. This directory must contain images with smileys and config.json. Example of config.json:
@ -6,8 +6,8 @@ Toxygen support smileys. Smiley is small picture which replaces some symbol or c
Animated smileys (.gif) are supported too.
# Stickers
#Stickers
Sticker is inline image. If you want to create your own sticker pack, create directory in src/stickers/ and place your stickers there.
Sticker is inline image. If you want to create your own smiley pack, create directory in src/stickers/ and place your stickers there.
Users can import smileys and stickers using menu: Settings -> Interface
Users can import plugins and stickers packs using menu: Settings -> Interface

BIN
docs/ubuntu.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 109 KiB

BIN
docs/windows.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -2,92 +2,50 @@ from setuptools import setup
from setuptools.command.install import install
from platform import system
from subprocess import call
import main
import sys
import os
from utils.util import curr_directory, join_path
from toxygen.util import program_version
version = main.__version__ + '.0'
version = program_version + '.0'
MODULES = ['PyAudio']
if system() == 'Windows':
MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python', 'pydenticon', 'cv2']
else:
MODULES = ['pydenticon']
MODULES.append('PyQt5')
try:
import pyaudio
except ImportError:
MODULES.append('PyAudio')
try:
import numpy
except ImportError:
MODULES.append('numpy')
try:
import cv2
except ImportError:
MODULES.append('opencv-python')
try:
import coloredlogs
except ImportError:
MODULES.append('coloredlogs')
try:
import pyqtconsole
except ImportError:
MODULES.append('pyqtconsole')
MODULES.append('PySide')
def get_packages():
directory = join_path(curr_directory(__file__), 'toxygen')
for root, dirs, files in os.walk(directory):
packages = map(lambda d: 'toxygen.' + d, dirs)
packages = ['toxygen'] + list(packages)
return packages
class InstallScript(install):
"""This class configures Toxygen after installation"""
def run(self):
install.run(self)
try:
if system() != 'Windows':
call(["toxygen", "--clean"])
except:
try:
params = list(filter(lambda x: x.startswith('--prefix='), sys.argv))
if params:
path = params[0][len('--prefix='):]
if path[-1] not in ('/', '\\'):
path += '/'
path += 'bin/toxygen'
if system() != 'Windows':
call([path, "--clean"])
except:
pass
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://git.plastiras.org/emdee/toxygen/',
keywords='toxygen Tox messenger',
url='https://github.com/xveduk/toxygen/',
keywords='toxygen tox messenger',
author='Ingvar',
maintainer='',
maintainer='Ingvar',
license='GPL3',
packages=get_packages(),
packages=['toxygen', 'toxygen.plugins', 'toxygen.styles'],
install_requires=MODULES,
include_package_data=True,
classifiers=[
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
],
entry_points={
'console_scripts': ['toxygen=toxygen.main:main']
'console_scripts': ['toxygen=toxygen.main:main'],
},
cmdclass={
'install': InstallScript
'install': InstallScript,
},
zip_safe=False
)

View File

@ -1,18 +1,70 @@
from toxygen.middleware.tox_factory import *
from toxygen.bootstrap import node_generator
from toxygen.profile import *
from toxygen.settings import ProfileHelper
from toxygen.tox_dns import tox_dns
import toxygen.toxencryptsave as encr
# TODO: add new tests
class TestProfile:
def test_search(self):
arr = ProfileHelper.find_profiles()
assert len(arr) >= 2
def test_open(self):
data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile()
assert data
class TestTox:
def test_loading(self):
data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile()
settings = Settings.get_default_settings()
tox = tox_factory(data, settings)
for data in node_generator():
tox.bootstrap(*data)
del tox
def test_creation(self):
name = 'Toxygen User'
status_message = 'Toxing on Toxygen'
name = b'Toxygen User'
status_message = b'Toxing on Toxygen'
tox = tox_factory()
tox.self_set_name(name)
tox.self_set_status_message(status_message)
data = tox.get_savedata()
del tox
tox = tox_factory(data)
assert tox.self_get_name() == name
assert tox.self_get_status_message() == status_message
assert tox.self_get_name() == str(name, 'utf-8')
assert tox.self_get_status_message() == str(status_message, 'utf-8')
def test_friend_list(self):
data = ProfileHelper(Settings.get_default_path(), 'bob').open_profile()
settings = Settings.get_default_settings()
tox = tox_factory(data, settings)
s = tox.self_get_friend_list()
size = tox.self_get_friend_list_size()
assert size <= 2
assert len(s) <= 2
del tox
class TestDNS:
def test_dns(self):
bot_id = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5'
tox_id = tox_dns('groupbot@toxme.io')
assert tox_id == bot_id
class TestEncryption:
def test_encr_decr(self):
with open(settings.Settings.get_default_path() + '/alice.tox', 'rb') as fl:
data = fl.read()
lib = encr.ToxEncryptSave()
lib.set_password('easypassword')
copy_data = data[:]
data = lib.pass_encrypt(data)
data = lib.pass_decrypt(data)
assert copy_data == data

View File

@ -1,4 +0,0 @@
class TestToxygen:
def test_main(self):
import toxygen.main # check for syntax errors

File diff suppressed because it is too large Load Diff

View File

View File

@ -1,58 +0,0 @@
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)

View File

@ -1,532 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import pyaudio
import time
import threading
import itertools
from wrapper.toxav_enums import *
from av import screen_sharing
from av.call import Call
import common.tox_save
from utils import ui as util_ui
import wrapper_tests.support_testing as ts
from middleware.threads import invoke_in_main_thread
from main import sleep
from middleware.threads import BaseThread
global LOG
import logging
LOG = logging.getLogger('app.'+__name__)
# callbacks can be called in any thread so were being careful
def LOG_ERROR(l): print('EROR< '+l)
def LOG_WARN(l): print('WARN< '+l)
def LOG_INFO(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20-1
if bIsVerbose: print('INFO< '+l)
def LOG_DEBUG(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 10-1
if bIsVerbose: print('DBUG< '+l)
def LOG_TRACE(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel < 10-1
pass # print('TRACE+ '+l)
TIMER_TIMEOUT = 30.0
bSTREAM_CALLBACK = False
iFPS = 25
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!r}" )
elif 'device' not in s['video']:
LOG.warn("AV.__init__ 'device' not in s.video" )
LOG.debug(f"AV.__init__ {s['video']!r}" )
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):
self._running = False
self.stop_audio_thread()
self.stop_video_thread()
def __contains__(self, friend_number):
return friend_number in self._calls
# -----------------------------------------------------------------------------------------------------------------
# Calls
# -----------------------------------------------------------------------------------------------------------------
def __call__(self, friend_number, audio, video):
"""Call friend with specified number"""
if friend_number in self._calls:
LOG.warn(f"__call__ already has {friend_number}")
return
if self._audio_krate_tox_audio not in ts.lToxSampleratesK:
LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}")
try:
self._toxav.call(friend_number,
self._audio_krate_tox_audio if audio else 0,
self._audio_krate_tox_video if video else 0)
except ArgumentError 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
return call_accept_call(self, friend_number, audio_enabled, video_enabled)
def call_accept_call(self, friend_number, audio_enabled, video_enabled):
LOG.debug(f"call_accept_call from {friend_number} {self._running}" +
f"{audio_enabled} {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 ArgumentError as e:
LOG.debug(f"AV accept_call error from {friend_number} {self._running}" +
f"{e}")
raise
if audio_enabled:
# may raise
self.start_audio_thread()
if video_enabled:
# may raise
self.start_video_thread()
def finish_call(self, friend_number, by_friend=False):
LOG.debug(f"finish_call {friend_number}")
if not by_friend:
self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL'])
if friend_number in self._calls:
del self._calls[friend_number]
try:
# AttributeError: 'int' object has no attribute 'out_audio'
if not len(list(filter(lambda c: c.out_audio, self._calls))):
self.stop_audio_thread()
if not len(list(filter(lambda c: c.out_video, self._calls))):
self.stop_video_thread()
except Exception as e:
LOG.error(f"finish_call FixMe: {e}")
# dunno
self.stop_audio_thread()
self.stop_video_thread()
def finish_not_started_call(self, friend_number):
if friend_number in self:
call = self._calls[friend_number]
if not call.is_active:
self.finish_call(friend_number)
def toxav_call_state_cb(self, friend_number, state):
"""
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):
return number in self and self._calls[number].in_video
# -----------------------------------------------------------------------------------------------------------------
# Threads
# -----------------------------------------------------------------------------------------------------------------
def start_audio_thread(self):
"""
Start audio sending
from a callback
"""
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 supported sample rates for device: audio[input]={iInput!r}"
LOG_ERROR(f"start_audio_thread {e}")
#?? dunno - cancel call?
return
if not self._audio_rate_pa in lPaSamplerates:
LOG_WARN(f"{self._audio_rate_pa} not in {lPaSamplerates!r}")
if False:
self._audio_rate_pa = oPYA.get_device_info_by_index(iInput)['defaultSampleRate']
else:
LOG_WARN(f"Setting audio_rate to: {lPaSamplerates[0]}")
self._audio_rate_pa = lPaSamplerates[0]
try:
LOG_DEBUG( f"start_audio_thread framerate: {self._audio_rate_pa}" \
+f" device: {iInput}"
+f" supported: {lPaSamplerates!r}")
if self._audio_rate_pa not in lPaSamplerates:
LOG_WARN(f"PAudio sampling rate was {self._audio_rate_pa} changed to {lPaSamplerates[0]}")
self._audio_rate_pa = lPaSamplerates[0]
if bSTREAM_CALLBACK:
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:
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 = BaseThread(target=self.send_audio,
name='_audio_thread')
self._audio_thread.start()
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? not if from a callback
# calls_manager._call.toxav_call_state_cb(friend_number, mask)
# raise RuntimeError(e)
return
else:
LOG_DEBUG(f"start_audio_thread {self._audio_stream!r}")
def stop_audio_thread(self):
if self._audio_thread is None:
return
self._audio_running = False
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):
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!r}" )
raise RuntimeError("start_video_thread not 'video' in s)" )
elif 'device' not in s['video']:
LOG.error("start_video_thread not 'device' in s['video']" )
LOG.debug(f"start_video_thread {s['video']!r}" )
raise RuntimeError("start_video_thread not 'device' ins s['video']" )
self._video_width = s['video']['width']
self._video_height = s['video']['height']
# dunno
if True or 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 = BaseThread(target=self.send_video,
name='_video_thread')
self._video_thread.start()
def stop_video_thread(self):
if self._video_thread is None:
return
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:
# AttributeError: 'NoneType' object has no attribute 'join'
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):
"""
Incoming chunk
"""
if self._out_stream is None:
# was iOutput = self._settings._args.audio['output']
iOutput = self._settings['audio']['output']
if not rate in self.lPaSampleratesO:
LOG.warn(f"{rate} not in {self.lPaSampleratesO!r}")
if False:
rate = oPYA.get_device_info_by_index(iOutput)['defaultSampleRate']
LOG.warn(f"Setting audio_rate to: {self.lPaSampleratesO[0]}")
rate = self.lPaSampleratesO[0]
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 {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']
LOG.debug(f"audio_chunk output_device_index={iOutput} rate={rate} channels={channels_count}")
self._out_stream.write(samples)
# -----------------------------------------------------------------------------------------------------------------
# AV sending
# -----------------------------------------------------------------------------------------------------------------
def send_audio_data(self, data, count, *largs, **kwargs):
pcm = data
# :param sampling_rate: Audio sampling rate used in this frame.
if self._toxav is None:
raise RuntimeError("_toxav not initialized")
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:
try:
# app.av.calls ERROR Error send_audio: 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
# 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 audio_send_frame: {e}")
LOG.debug(f"send_audio self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}")
# invoke_in_main_thread(util_ui.message_box,
# str(e),
# util_ui.tr("Error send_audio audio_send_frame"))
pass
def send_audio(self):
"""
This method sends audio to friends
"""
i=0
count = self._audio_sample_count_tox
LOG.debug(f"send_audio stream={self._audio_stream}")
while self._audio_running:
try:
pcm = self._audio_stream.read(count, exception_on_overflow=False)
if not pcm:
sleep(0.1)
else:
self.send_audio_data(pcm, count)
except:
LOG_DEBUG(f"error send_audio {i}")
else:
LOG_TRACE(f"send_audio {i}")
i += 1
sleep(0.01)
def send_video(self):
"""
This method sends video to friends
"""
LOG.debug(f"send_video thread={threading.current_thread().name}"
+f" self._video_running={self._video_running}"
+f" device: {self._settings['video']['device']}" )
while self._video_running:
try:
result, frame = self._video.read()
if not result:
LOG.warn(f"send_video video_send_frame _video.read result={result}")
break
if frame is None:
LOG.warn(f"send_video video_send_frame _video.read result={result} frame={frame}")
continue
else:
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.debug(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):
"""
: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
u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2]
u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:]
u = list(itertools.chain.from_iterable(u))
v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2]
v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:]
v = list(itertools.chain.from_iterable(v))
return bytes(y), bytes(u), bytes(v)

View File

@ -1,167 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import sys
import threading
import av.calls
from messenger.messages import *
from ui import av_widgets
import common.event as event
import utils.ui as util_ui
global LOG
import logging
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):
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):
"""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):
"""
Incoming call from friend.
"""
LOG.debug(__name__ +f" 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):
"""
Accept incoming call with audio or video
Called from a thread
"""
LOG.debug(f"CM accept_call from {friend_number} {audio} {video}")
sys.stdout.flush()
try:
self._callav.call_accept_call(friend_number, audio, video)
except Exception as e:
LOG.error(f"accept_call _call.accept_call ERROR for {friend_number} {e}")
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!r}")
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!r}")
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}'))
else:
self._main_screen.active_call()
finally:
# does not terminate call - just the av_widget
if friend_number in self._incoming_calls:
self._incoming_calls.remove(friend_number)
try:
self._call_widgets[friend_number].close()
del self._call_widgets[friend_number]
except:
# RuntimeError: wrapped C/C++ object of type IncomingCallWidget has been deleted
pass
LOG.debug(f" closed self._call_widgets[{friend_number}]")
def stop_call(self, friend_number, by_friend):
"""
Stop call with friend
"""
LOG.debug(__name__+f" stop_call {friend_number}")
if friend_number in self._incoming_calls:
self._incoming_calls.remove(friend_number)
is_declined = True
else:
is_declined = False
self._main_screen.call_finished()
self._callav.finish_call(friend_number, by_friend) # finish or decline call
if friend_number in self._call_widgets:
self._call_widgets[friend_number].close()
del self._call_widgets[friend_number]
def destroy_window():
#??? FixMed
is_video = self._callav.is_video_call(friend_number)
if is_video:
import cv2
cv2.destroyWindow(str(friend_number))
threading.Timer(2.0, destroy_window).start()
self._call_finished_event(friend_number, is_declined)
def friend_exit(self, friend_number):
if friend_number in self._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)

View File

@ -1,22 +0,0 @@
from PyQt5 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):
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

139
toxygen/avwidgets.py Normal file
View File

@ -0,0 +1,139 @@
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
import widgets
import profile
import util
import pyaudio
import wave
import settings
from util import curr_directory
class IncomingCallWidget(widgets.CenteredWidget):
def __init__(self, friend_number, text, name):
super(IncomingCallWidget, self).__init__()
self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint)
self.resize(QtCore.QSize(500, 270))
self.avatar_label = QtGui.QLabel(self)
self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64))
self.avatar_label.setScaledContents(False)
self.name = widgets.DataLabel(self)
self.name.setGeometry(QtCore.QRect(90, 20, 300, 25))
font = QtGui.QFont()
font.setFamily("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()

114
toxygen/basecontact.py Normal file
View File

@ -0,0 +1,114 @@
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 BaseContact:
"""
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 or group topic
# -----------------------------------------------------------------------------------------------------------------
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, default_path='avatar.png'):
"""
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 = default_path
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()

83
toxygen/bootstrap.py Normal file
View File

@ -0,0 +1,83 @@
import random
class Node:
def __init__(self, ip, port, tox_key, rand):
self._ip, self._port, self._tox_key, self.rand = ip, port, tox_key, rand
def get_data(self):
return bytes(self._ip, 'utf-8'), self._port, self._tox_key
def node_generator():
nodes = []
ips = [
"144.76.60.215", "23.226.230.47", "195.154.119.113", "biribiri.org",
"46.38.239.179", "178.62.250.138", "130.133.110.14", "104.167.101.29",
"205.185.116.116", "198.98.51.198", "80.232.246.79", "108.61.165.198",
"212.71.252.109", "194.249.212.109", "185.25.116.107", "192.99.168.140",
"46.101.197.175", "95.215.46.114", "5.189.176.217", "148.251.23.146",
"104.223.122.15", "78.47.114.252", "d4rk4.ru", "81.4.110.149",
"95.31.20.151", "104.233.104.126", "51.254.84.212", "home.vikingmakt.com.br",
"5.135.59.163", "185.58.206.164", "188.244.38.183", "mrflibble.c4.ee",
"82.211.31.116", "128.199.199.197", "103.230.156.174", "91.121.66.124",
"92.54.84.70", "tox1.privacydragon.me"
]
ports = [
33445, 33445, 33445, 33445,
33445, 33445, 33445, 33445,
33445, 33445, 33445, 33445,
33445, 33445, 33445, 33445,
443, 33445, 5190, 2306,
33445, 33445, 1813, 33445,
33445, 33445, 33445, 33445,
33445, 33445, 33445, 33445,
33445, 33445, 33445, 33445,
33445, 33445
]
ids = [
"04119E835DF3E78BACF0F84235B300546AF8B936F035185E2A8E9E0A67C8924F",
"A09162D68618E742FFBCA1C2C70385E6679604B2D80EA6E84AD0996A1AC8A074",
"E398A69646B8CEACA9F0B84F553726C1C49270558C57DF5F3C368F05A7D71354",
"F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67",
"F5A1A38EFB6BD3C2C8AF8B10D85F0F89E931704D349F1D0720C3C4059AF2440A",
"788236D34978D1D5BD822F0A5BEBD2C53C64CC31CD3149350EE27D4D9A2F9B6B",
"461FA3776EF0FA655F1A05477DF1B3B614F7D6B124F7DB1DD4FE3C08B03B640F",
"5918AC3C06955962A75AD7DF4F80A5D7C34F7DB9E1498D2E0495DE35B3FE8A57",
"A179B09749AC826FF01F37A9613F6B57118AE014D4196A0E1105A98F93A54702",
"1D5A5F2F5D6233058BF0259B09622FB40B482E4FA0931EB8FD3AB8E7BF7DAF6F",
"CF6CECA0A14A31717CC8501DA51BE27742B70746956E6676FF423A529F91ED5D",
"8E7D0B859922EF569298B4D261A8CCB5FEA14FB91ED412A7603A585A25698832",
"C4CEB8C7AC607C6B374E2E782B3C00EA3A63B80D4910B8649CCACDD19F260819",
"3CEE1F054081E7A011234883BC4FC39F661A55B73637A5AC293DDF1251D9432B",
"DA4E4ED4B697F2E9B000EEFE3A34B554ACD3F45F5C96EAEA2516DD7FF9AF7B43",
"6A4D0607A296838434A6A7DDF99F50EF9D60A2C510BBF31FE538A25CB6B4652F",
"CD133B521159541FB1D326DE9850F5E56A6C724B5B8E5EB5CD8D950408E95707",
"5823FB947FF24CF83DDFAC3F3BAA18F96EA2018B16CC08429CB97FA502F40C23",
"2B2137E094F743AC8BD44652C55F41DFACC502F125E99E4FE24D40537489E32F",
"7AED21F94D82B05774F697B209628CD5A9AD17E0C073D9329076A4C28ED28147",
"0FB96EEBFB1650DDB52E70CF773DDFCABE25A95CC3BB50FC251082E4B63EF82A",
"1C5293AEF2114717547B39DA8EA6F1E331E5E358B35F9B6B5F19317911C5F976",
"53737F6D47FA6BD2808F378E339AF45BF86F39B64E79D6D491C53A1D522E7039",
"9E7BD4793FFECA7F32238FA2361040C09025ED3333744483CA6F3039BFF0211E",
"9CA69BB74DE7C056D1CC6B16AB8A0A38725C0349D187D8996766958584D39340",
"EDEE8F2E839A57820DE3DA4156D88350E53D4161447068A3457EE8F59F362414",
"AEC204B9A4501412D5F0BB67D9C81B5DB3EE6ADA64122D32A3E9B093D544327D",
"188E072676404ED833A4E947DC1D223DF8EFEBE5F5258573A236573688FB9761",
"2D320F971EF2CA18004416C2AAE7BA52BF7949DB34EA8E2E21AF67BD367BE211",
"24156472041E5F220D1FA11D9DF32F7AD697D59845701CDD7BE7D1785EB9DB39",
"15A0F9684E2423F9F46CFA5A50B562AE42525580D840CC50E518192BF333EE38",
"FAAB17014F42F7F20949F61E55F66A73C230876812A9737F5F6D2DCE4D9E4207",
"AF97B76392A6474AF2FD269220FDCF4127D86A42EF3A242DF53A40A268A2CD7C",
"B05C8869DBB4EDDD308F43C1A974A20A725A36EACCA123862FDE9945BF9D3E09",
"5C4C7A60183D668E5BD8B3780D1288203E2F1BAE4EEF03278019E21F86174C1D",
"4E3F7D37295664BBD0741B6DBCB6431D6CD77FC4105338C2FC31567BF5C8224A",
"5625A62618CB4FCA70E147A71B29695F38CC65FF0CBD68AD46254585BE564802",
"31910C0497D347FF160D6F3A6C0E317BAFA71E8E03BC4CBB2A185C9D4FB8B31E"
]
for i in range(len(ips)):
nodes.append(Node(ips[i], ports[i], ids[i], random.randint(0, 1000000)))
arr = sorted(nodes, key=lambda x: x.rand)[:4]
for elem in arr:
yield elem.get_data()

View File

@ -1,48 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import random
import urllib.request
from utils.util import *
from PyQt5 import QtNetwork
from PyQt5 import QtCore
try:
import certifi
from io import BytesIO
except ImportError:
certifi = None
from user_data.settings import get_user_config_path
from wrapper_tests.support_testing import _get_nodes_path
from wrapper_tests.support_http import download_url
import wrapper_tests.support_testing as ts
global LOG
import logging
LOG = logging.getLogger('app.'+'bootstrap')
def download_nodes_list(settings, oArgs):
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):
if not nodes:
return
with open(_get_nodes_path(oArgs=app._args), 'wb') as fl:
LOG.info("Saving nodes to " +_get_nodes_path())
fl.write(nodes)

View File

@ -1 +0,0 @@
{"nodes":[{"ipv4":"80.211.19.83","ipv6":"-","port":33445,"public_key":"A2D7BF17C10A12C339B9F4E8DD77DEEE8457D580535A6F0D0F9AF04B8B4C4420","status_udp":true,"status_tcp":true}]}

373
toxygen/callbacks.py Normal file
View File

@ -0,0 +1,373 @@
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 - group chats
# -----------------------------------------------------------------------------------------------------------------
def group_message(window, tray, tox):
"""
New message in group chat
"""
def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data):
profile = Profile.get_instance()
settings = Settings.get_instance()
message = str(message[:length], 'utf-8')
invoke_in_main_thread(profile.new_message, group_number, message_type, message, True, peer_id)
if not window.isActiveWindow():
bl = settings['notify_all_gc'] or profile.name in message
name = tox.group_peer_get_name(group_number, peer_id)
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and (not settings.locked) and bl:
invoke_in_main_thread(tray_notification, name, message, tray, window)
if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']:
sound_notification(SOUND_NOTIFICATION['MESSAGE'])
invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png'))
return wrapped
def group_invite(tox, friend_number, invite_data, length, user_data):
invoke_in_main_thread(Profile.get_instance().process_group_invite,
friend_number,
bytes(invite_data[:length]))
def group_self_join(tox, group_number, user_data):
pr = Profile.get_instance()
gc = pr.get_gc_by_number(group_number)
invoke_in_main_thread(gc.set_status, TOX_USER_STATUS['NONE'])
if not pr.is_active_a_friend() and pr.get_active_number() == group_number:
invoke_in_main_thread(pr.set_active)
def group_peer_join(tox, group_number, peer_id, user_data):
gc = Profile.get_instance().get_gc_by_number(group_number)
gc.add_peer(peer_id)
# -----------------------------------------------------------------------------------------------------------------
# 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)
tox.callback_group_message(group_message(window, tray, tox), 0)
tox.callback_group_invite(group_invite, 0)
tox.callback_group_self_join(group_self_join, 0)
tox.callback_group_peer_join(group_peer_join, 0)

144
toxygen/calls.py Normal file
View File

@ -0,0 +1,144 @@
import pyaudio
import time
import threading
import settings
from toxav_enums import *
# TODO: play sound until outgoing call will be started or cancelled and add timeout
# TODO: add widget for call
CALL_TYPE = {
'NONE': 0,
'AUDIO': 1,
'VIDEO': 2
}
class AV:
def __init__(self, toxav):
self._toxav = toxav
self._running = True
self._calls = {} # dict: key - friend number, value - call type
self._audio = None
self._audio_stream = None
self._audio_thread = None
self._audio_running = False
self._out_stream = None
self._audio_rate = 8000
self._audio_channels = 1
self._audio_duration = 60
self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000
def __contains__(self, friend_number):
return friend_number in self._calls
def __call__(self, friend_number, audio, video):
"""Call friend with specified number"""
self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0)
self._calls[friend_number] = CALL_TYPE['AUDIO']
self.start_audio_thread()
def finish_call(self, friend_number, by_friend=False):
if not by_friend:
self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL'])
if friend_number in self._calls:
del self._calls[friend_number]
if not len(self._calls):
self.stop_audio_thread()
def stop(self):
self._running = False
self.stop_audio_thread()
def start_audio_thread(self):
"""
Start audio sending
"""
if self._audio_thread is not None:
return
self._audio_running = True
self._audio = pyaudio.PyAudio()
self._audio_stream = self._audio.open(format=pyaudio.paInt16,
rate=self._audio_rate,
channels=self._audio_channels,
input=True,
input_device_index=settings.Settings.get_instance().audio['input'],
frames_per_buffer=self._audio_sample_count * 10)
self._audio_thread = threading.Thread(target=self.send_audio)
self._audio_thread.start()
def stop_audio_thread(self):
if self._audio_thread is None:
return
self._audio_running = False
self._audio_thread.join()
self._audio_thread = None
self._audio_stream = None
self._audio = None
if self._out_stream is not None:
self._out_stream.stop_stream()
self._out_stream.close()
self._out_stream = None
def chunk(self, samples, channels_count, rate):
"""
Incoming chunk
"""
if self._out_stream is None:
self._out_stream = self._audio.open(format=pyaudio.paInt16,
channels=channels_count,
rate=rate,
output_device_index=settings.Settings.get_instance().audio['output'],
output=True)
self._out_stream.write(samples)
def send_audio(self):
"""
This method sends audio to friends
"""
while self._audio_running:
try:
pcm = self._audio_stream.read(self._audio_sample_count)
if pcm:
for friend in self._calls:
if self._calls[friend] & 1:
try:
self._toxav.audio_send_frame(friend, pcm, self._audio_sample_count,
self._audio_channels, self._audio_rate)
except:
pass
except:
pass
time.sleep(0.01)
def accept_call(self, friend_number, audio_enabled, video_enabled):
if self._running:
self._calls[friend_number] = int(video_enabled) * 2 + int(audio_enabled)
self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0)
self.start_audio_thread()
def toxav_call_state_cb(self, friend_number, state):
"""
New call state
"""
if self._running:
if state & TOXAV_FRIEND_CALL_STATE['ACCEPTING_A']:
self._calls[friend_number] |= 1

View File

@ -1,26 +0,0 @@
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)

View File

@ -1,13 +0,0 @@
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

View File

@ -1,18 +0,0 @@
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

212
toxygen/contact.py Normal file
View File

@ -0,0 +1,212 @@
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
import basecontact
from messages import *
from history import *
import file_transfers as ft
import util
class Contact(basecontact.BaseContact):
"""
Class encapsulating TOX contact
Properties: name (alias of contact or name), status_message, status (connection status)
widget - widget for update
"""
def __init__(self, number, message_getter, 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
"""
super().__init__(name, status_message, widget, tox_id)
self._message_getter = message_getter
self._new_messages = False
self._visible = True
self._alias = False
self._number = number
self._corr = []
self._unsaved_messages = 0
self._history_loaded = self._new_actions = False
self._curr_text = ''
def __del__(self):
self.set_visibility(False)
del self._widget
if hasattr(self, '_message_getter'):
del self._message_getter
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 delete_old_messages(self):
old = filter(lambda x: x.get_type() in (2, 3) and (x.get_status() >= 2 or x.get_status() is None),
self._corr[:-SAVE_MESSAGES])
old = list(old)
l = max(len(self._corr) - SAVE_MESSAGES, 0) - len(old)
self._unsaved_messages -= l
self._corr = old + self._corr[-SAVE_MESSAGES:]
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)
# -----------------------------------------------------------------------------------------------------------------
# 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 actions
# -----------------------------------------------------------------------------------------------------------------
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)
# -----------------------------------------------------------------------------------------------------------------
# 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)
# -----------------------------------------------------------------------------------------------------------------
# Alias support
# -----------------------------------------------------------------------------------------------------------------
def set_name(self, value):
"""
Set new name or ignore if alias exists
:param value: new name
"""
if not self._alias:
super(Contact, self).set_name(value)
def set_alias(self, alias):
self._alias = bool(alias)

View File

@ -1,186 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from user_data.settings import *
from PyQt5 import QtCore, QtGui
from wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE
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
"""
avatar_path = self.get_avatar_path()
width = self._widget.avatar_label.width()
pixmap = QtGui.QPixmap(avatar_path)
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
self._widget.avatar_label.repaint()
self._avatar_changed_event(avatar_path)
def reset_avatar(self, generate_new):
avatar_path = self.get_avatar_path()
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):
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')

View File

@ -1,50 +0,0 @@
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):
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

View File

@ -1,343 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from history.database import TIMEOUT, \
SAVE_MESSAGES, MESSAGE_AUTHOR
from contacts import basecontact, common
from messenger.messages import *
from contacts.contact_menu import *
from file_transfers import file_transfers as ft
import re
# LOG=util.log
global LOG
import logging
LOG = logging.getLogger('app.'+__name__)
class Contact(basecontact.BaseContact):
"""
Class encapsulating TOX contact
Properties: number, message getter, history etc. Base class for friend and gc classes
"""
def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id):
"""
:param message_getter: gets messages from db
:param number: number of friend.
"""
super().__init__(profile_manager, name, status_message, widget, tox_id)
self._number = number
self._new_messages = False
self._visible = True
self._alias = False
self._message_getter = message_getter
self._corr = []
self._unsaved_messages = 0
self._history_loaded = self._new_actions = False
self._curr_text = self._search_string = ''
self._search_index = 0
def __del__(self):
self.set_visibility(False)
del self._widget
if hasattr(self, '_message_getter'):
del self._message_getter
# -----------------------------------------------------------------------------------------------------------------
# History support
# -----------------------------------------------------------------------------------------------------------------
def load_corr(self, first_time=True):
"""
:param first_time: friend became active, load first part of messages
"""
try:
if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')):
return
if self._message_getter is None:
return
data = list(self._message_getter.get(PAGE_SIZE))
if data is not None and len(data):
data.reverse()
else:
return
data = list(map(lambda p: self._get_text_message(p), data))
self._corr = data + self._corr
except:
pass
finally:
self._history_loaded = True
def load_all_corr(self):
"""
Get all chat history from db for current friend
"""
if self._message_getter is None:
return
data = list(self._message_getter.get_all())
if data is not None and len(data):
data.reverse()
data = list(map(lambda p: self._get_text_message(p), data))
self._corr = data + self._corr
self._history_loaded = True
def get_corr_for_saving(self):
"""
Get data to save in db
:return: list of unsaved messages or []
"""
messages = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']), self._corr))
return messages[-self._unsaved_messages:] if self._unsaved_messages else []
def get_corr(self):
return self._corr[:]
def append_message(self, message):
"""
:param message: text or file transfer message
"""
self._corr.append(message)
if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']):
self._unsaved_messages += 1
def get_last_message_text(self):
messages = list(filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION'])
and m.author.type != MESSAGE_AUTHOR['FRIEND'], self._corr))
if messages:
return messages[-1].text
else:
return ''
def remove_messages_widgets(self):
for message in self._corr:
message.remove_widget()
def get_message(self, _filter):
return list(filter(lambda m: _filter(m), self._corr))[0]
@staticmethod
def _get_text_message(params):
(message, author_type, author_name, unix_time, message_type, unique_id) = params
author = MessageAuthor(author_name, author_type)
return TextMessage(message, author, unix_time, message_type, unique_id)
# -----------------------------------------------------------------------------------------------------------------
# Unsent messages
# -----------------------------------------------------------------------------------------------------------------
def get_unsent_messages(self):
"""
:return list of unsent messages
"""
messages = filter(lambda m: m.author is not None and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr)
return list(messages)
def get_unsent_messages_for_saving(self):
"""
:return list of unsent messages for saving
"""
messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION'])
and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr)
return list(messages)
def mark_as_sent(self, tox_message_id):
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]
try:
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!s}")
# -----------------------------------------------------------------------------------------------------------------
# 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()

View File

@ -1,237 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from PyQt5 import QtWidgets
import utils.ui as util_ui
from 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

View File

@ -1,181 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import common.tox_save as tox_save
global LOG
import logging
LOG = logging.getLogger(__name__)
# callbacks can be called in any thread so were being careful
def LOG_ERROR(l): print('EROR< '+l)
def LOG_WARN(l): print('WARN< '+l)
def LOG_INFO(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20-1
if bIsVerbose: print('INFO< '+l)
def LOG_DEBUG(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 10-1
if bIsVerbose: print('DBUG< '+l)
def LOG_TRACE(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel < 10-1
pass # print('TRACE+ '+l)
class ContactProvider(tox_save.ToxSave):
def __init__(self, tox, friend_factory, group_factory, group_peer_factory):
super().__init__(tox)
self._friend_factory = friend_factory
self._group_factory = group_factory
self._group_peer_factory = group_peer_factory
self._cache = {} # key - contact's public key, value - contact instance
# -----------------------------------------------------------------------------------------------------------------
# Friends
# -----------------------------------------------------------------------------------------------------------------
def get_friend_by_number(self, friend_number):
try:
public_key = self._tox.friend_get_public_key(friend_number)
except Exception as e:
LOG_WARN(f"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)
self._add_to_cache(public_key, friend)
LOG_INFO(f"get_friend_by_public_key ADDED {friend} ")
return friend
def get_all_friends(self):
try:
friend_numbers = self._tox.self_get_friend_list()
except Exception as e:
LOG_WARN(f"get_all_friends NO {friend_numbers} {e} ")
return None
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"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_INFO(f"group_get_number {group_number} ")
# original code
chat_id = self._tox.group_get_chat_id(group_number)
if not chat_id:
LOG_ERROR(f"get_group_by_number NULL number ({group_number})")
else:
LOG_INFO(f"group_get_number {group_number} {chat_id}")
group = self.get_group_by_chat_id(chat_id)
if not group:
LOG_ERROR(f"get_group_by_number NULL group ({chat_id})")
if group is None:
LOG_WARN(f"get_group_by_number leaving ({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"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_ERROR(f"get_group_by_public_key NULL group public_key={get_group_by_chat_id}")
else:
self._add_to_cache(public_key, group)
return group
# -----------------------------------------------------------------------------------------------------------------
# Group peers
# -----------------------------------------------------------------------------------------------------------------
def get_all_group_peers(self):
return list()
def get_group_peer_by_id(self, group, peer_id):
peer = group.get_peer_by_id(peer_id)
if peer:
return self._get_group_peer(group, peer)
def get_group_peer_by_public_key(self, group, public_key):
peer = group.get_peer_by_public_key(public_key)
return self._get_group_peer(group, peer)
# -----------------------------------------------------------------------------------------------------------------
# All contacts
# -----------------------------------------------------------------------------------------------------------------
def get_all(self):
return self.get_all_friends() + self.get_all_groups() + self.get_all_group_peers()
# -----------------------------------------------------------------------------------------------------------------
# Caching
# -----------------------------------------------------------------------------------------------------------------
def clear_cache(self):
self._cache.clear()
def remove_contact_from_cache(self, contact_public_key):
if contact_public_key in self._cache:
del self._cache[contact_public_key]
# -----------------------------------------------------------------------------------------------------------------
# Private methods
# -----------------------------------------------------------------------------------------------------------------
def _get_contact_from_cache(self, public_key):
return self._cache[public_key] if public_key in self._cache else None
def _add_to_cache(self, public_key, contact):
self._cache[public_key] = contact
def _get_group_peer(self, group, peer):
return self._group_peer_factory.create_group_peer(group, peer)

View File

@ -1,685 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import traceback
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
# LOG=util.log
global LOG
import logging
LOG = logging.getLogger('app.'+__name__)
def LOG_ERROR(l): print('ERROR_: '+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('TRACE+ '+l)
UINT32_MAX = 2 ** 32 -1
def set_contact_kind(contact):
bInvite = len(contact.name) == 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):
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):
data = self._tox.get_savedata()
self._profile_manager.save_profile(data)
def is_friend_active(self, friend_number):
if not self.is_active_a_friend():
return False
return self.get_curr_contact().number == friend_number
def is_group_active(self, group_number):
if self.is_active_a_friend():
return False
return self.get_curr_contact().number == group_number
def is_contact_active(self, contact):
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
return self._contacts[self._active_contact].tox_id == contact.tox_id
# -----------------------------------------------------------------------------------------------------------------
# Reconnection support
# -----------------------------------------------------------------------------------------------------------------
def reset_contacts_statuses(self):
for contact in self._contacts:
contact.status = None
# -----------------------------------------------------------------------------------------------------------------
# Work with active friend
# -----------------------------------------------------------------------------------------------------------------
def get_active(self):
return self._active_contact
def set_active(self, value):
"""
Change current active friend or update info
:param value: number of new active friend in friend's list
"""
if value is None and self._active_contact == -1: # nothing to update
return
if value == -1: # all friends were deleted
self._screen.account_name.setText('')
self._screen.account_status.setText('')
self._screen.account_status.setToolTip('')
self._active_contact = -1
self._screen.account_avatar.setHidden(True)
self._messages.clear()
self._screen.messageEdit.clear()
return
try:
self._screen.typing.setVisible(False)
current_contact = self.get_curr_contact()
if current_contact is not None:
# TODO: send when needed
current_contact.typing_notification_handler.send(self._tox, False)
current_contact.remove_messages_widgets() # TODO: if required
self._unsubscribe_from_events(current_contact)
if self._active_contact >= 0 and self._active_contact != value:
try:
current_contact.curr_text = self._screen.messageEdit.toPlainText()
except:
pass
# IndexError: list index out of range
contact = self._contacts[value]
self._subscribe_to_events(contact)
contact.remove_invalid_unsent_files()
if self._active_contact != value:
self._screen.messageEdit.setPlainText(contact.curr_text)
self._active_contact = value
contact.reset_messages()
if not self._settings['save_history']:
contact.delete_old_messages()
self._messages.clear()
contact.load_corr()
corr = contact.get_corr()[-PAGE_SIZE:]
for message in corr:
if message.type == MESSAGE_TYPE['FILE_TRANSFER']:
self._messages_items_factory.create_file_transfer_item(message)
elif message.type == MESSAGE_TYPE['INLINE']:
self._messages_items_factory.create_inline_item(message)
else:
self._messages_items_factory.create_message_item(message)
self._messages.scrollToBottom()
# if value in self._call:
# self._screen.active_call()
# elif value in self._incoming_calls:
# self._screen.incoming_call()
# else:
# self._screen.call_finished()
self._set_current_contact_data(contact)
self._active_contact_changed(contact)
except Exception as ex: # no friend found. ignore
LOG.warn(f"no friend found. Friend value: {value!s}")
LOG.error('in set active: ' + str(ex))
# 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)
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: # broken?
if not hasattr(peer, 'public_key') or not peer.public_key:
LOG.error(f'no peer public_key ' + repr(dir(peer)))
else:
if not self.check_if_contact_exists(peer.public_key):
self.add_group_peer(group, peer)
return self.get_contact_by_tox_id(peer.public_key)
else:
LOG.warn(f'no peer group_number={group_number} peer_id={peer_id}')
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[:TOX_PUBLIC_KEY_SIZE * 2]
if tox_id == self._tox.self_get_address()[:TOX_PUBLIC_KEY_SIZE * 2]:
return
if tox_id not in self._settings['blocked']:
self._settings['blocked'].append(tox_id)
self._settings.save()
try:
num = self._tox.friend_by_public_key(tox_id)
self.delete_friend(num)
self.save_profile()
except: # not in friend list
pass
def unblock_user(self, tox_id, add_to_friend_list):
"""
Unblock user
:param tox_id: tox id of contact
:param add_to_friend_list: add this contact to friend list or not
"""
self._settings['blocked'].remove(tox_id)
self._settings.save()
if add_to_friend_list:
self.add_friend(tox_id)
self.save_profile()
# -----------------------------------------------------------------------------------------------------------------
# Groups support
# -----------------------------------------------------------------------------------------------------------------
def get_group_chats(self):
return list(filter(lambda c: type(c) is GroupChat, self._contacts))
def add_group(self, group_number):
index = len(self._contacts)
group = self._contact_provider.get_group_by_number(group_number)
# group num >= 0?
if group is None:
LOG.warn(f"CM.add_group: NO group {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._kind = 'grouppeer'
self._contacts.append(contact)
contact.reset_avatar(self._settings['identicons'])
self._save_profile()
def remove_group_peer_by_id(self, group, peer_id):
peer = group.get_peer_by_id(peer_id)
if 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) == 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[: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:
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()

View File

@ -1,74 +0,0 @@
from contacts import contact, common
from messenger.messages import *
import os
from contacts.contact_menu import *
class Friend(contact.Contact):
"""
Friend in list of friends.
"""
def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id):
super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id)
self._receipts = 0
self._typing_notification_handler = common.FriendTypingNotificationHandler(number)
# -----------------------------------------------------------------------------------------------------------------
# File transfers support
# -----------------------------------------------------------------------------------------------------------------
def insert_inline(self, before_message_id, inline):
"""
Update status of active transfer and load inline if needed
"""
try:
tr = list(filter(lambda m: m.message_id == before_message_id, self._corr))[0]
i = self._corr.index(tr)
if inline: # inline was loaded
self._corr.insert(i, inline)
return i - len(self._corr)
except:
pass
def get_unsent_files(self):
messages = filter(lambda m: type(m) is UnsentFileMessage, self._corr)
return list(messages)
def clear_unsent_files(self):
self._corr = list(filter(lambda m: type(m) is not UnsentFileMessage, self._corr))
def remove_invalid_unsent_files(self):
def is_valid(message):
if type(message) is not UnsentFileMessage:
return True
if message.data is not None:
return True
return os.path.exists(message.path)
self._corr = list(filter(is_valid, self._corr))
def delete_one_unsent_file(self, message_id):
self._corr = list(filter(lambda m: not (type(m) is UnsentFileMessage and m.message_id == message_id),
self._corr))
# -----------------------------------------------------------------------------------------------------------------
# Full status
# -----------------------------------------------------------------------------------------------------------------
def get_full_status(self):
return self._status_message
# -----------------------------------------------------------------------------------------------------------------
# Typing notifications
# -----------------------------------------------------------------------------------------------------------------
def get_typing_notification_handler(self):
return self._typing_notification_handler
# -----------------------------------------------------------------------------------------------------------------
# Context menu support
# -----------------------------------------------------------------------------------------------------------------
def get_context_menu_generator(self):
return FriendMenuGenerator(self)

View File

@ -1,44 +0,0 @@
from common.tox_save import ToxSave
from contacts.friend import Friend
class FriendFactory(ToxSave):
def __init__(self, profile_manager, settings, tox, db, items_factory):
super().__init__(tox)
self._profile_manager = profile_manager
self._settings = settings
self._db = db
self._items_factory = items_factory
def create_friend_by_public_key(self, public_key):
friend_number = self._tox.friend_by_public_key(public_key)
return self.create_friend_by_number(friend_number)
def create_friend_by_number(self, friend_number):
aliases = self._settings['friends_aliases']
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()

View File

@ -1,169 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from contacts import contact
from contacts.contact_menu import GroupMenuGenerator
import utils.util as util
from groups.group_peer import GroupChatPeer
from 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__)
def LOG_ERROR(l): print('ERROR_: '+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('TRACE+ '+l)
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
LOG_TRACE(f"add_peer id={peer_id}")
peer = GroupChatPeer(peer_id,
self._tox.group_peer_get_name(self._number, peer_id),
self._tox.group_peer_get_status(self._number, peer_id),
self._tox.group_peer_get_role(self._number, peer_id),
self._tox.group_peer_get_public_key(self._number, peer_id),
is_current_user)
self._peers.append(peer)
def remove_peer(self, peer_id):
if peer_id == self.get_self_peer().id: # we were kicked or banned
self.remove_all_peers_except_self()
else:
peer = self.get_peer_by_id(peer_id)
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 []
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 []
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)

View File

@ -1,61 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from contacts.group_chat import GroupChat
from common.tox_save import ToxSave
import 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()):
if self._tox.group_get_chat_id(i) == chat_id:
return i
return -1

View File

@ -1,20 +0,0 @@
import contacts.contact
from contacts.contact_menu import GroupPeerMenuGenerator
class GroupPeerContact(contacts.contact.Contact):
def __init__(self, profile_manager, message_getter, peer_number, name, widget, tox_id, group_pk):
super().__init__(profile_manager, message_getter, peer_number, name, str(), widget, tox_id)
self._group_pk = group_pk
def get_group_pk(self):
return self._group_pk
group_pk = property(get_group_pk)
def remove_invalid_unsent_files(self):
pass
def get_context_menu_generator(self):
return GroupPeerMenuGenerator(self)

View File

@ -1,23 +0,0 @@
from common.tox_save import ToxSave
from contacts.group_peer_contact import GroupPeerContact
class GroupPeerFactory(ToxSave):
def __init__(self, tox, profile_manager, db, items_factory):
super().__init__(tox)
self._profile_manager = profile_manager
self._db = db
self._items_factory = items_factory
def create_group_peer(self, group, peer):
item = self._create_group_peer_item()
message_getter = self._db.messages_getter(peer.public_key)
group_peer_contact = GroupPeerContact(self._profile_manager, message_getter, peer.id, peer.name,
item, peer.public_key, group.tox_id)
group_peer_contact.status = peer.status
return group_peer_contact
def _create_group_peer_item(self):
return self._items_factory.create_contact_item()

View File

@ -1,94 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from contacts import basecontact
import random
import threading
import common.tox_save as tox_save
from middleware.threads import invoke_in_main_thread
iUMAXINT = 4294967295
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):
"""
: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
# -----------------------------------------------------------------------------------------------------------------
# Edit current user's data
# -----------------------------------------------------------------------------------------------------------------
def change_status(self):
"""
Changes status of user (online, away, busy)
"""
if self._status is not None:
self.set_status((self._status + 1) % 3)
def set_status(self, status):
super().set_status(status)
if status is not None:
self._tox.self_set_status(status)
elif not self._waiting_for_reconnection:
self._waiting_for_reconnection = True
self._timer = threading.Timer(50, self._reconnect)
self._timer.start()
def set_name(self, value):
if self.name == value:
return
super().set_name(value)
self._tox.self_set_name(self._name)
def set_status_message(self, value):
super().set_status_message(value)
self._tox.self_set_status_message(self._status_message)
def set_new_nospam(self):
"""Sets new nospam part of tox id"""
self._tox.self_set_nospam(random.randint(0, 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):
"""
Recreate tox instance
"""
self.status = None
invoke_in_main_thread(self._reset_action)
def _reconnect(self):
self._waiting_for_reconnection = False
contacts = self._contacts_provider.get_all_friends()
all_friends_offline = all(list(map(lambda x: x.status is None, contacts)))
if self.status is None or (all_friends_offline and len(contacts)):
self._waiting_for_reconnection = True
self.restart()
self._timer = threading.Timer(50, self._reconnect)
self._timer.start()

View File

@ -1,21 +1,25 @@
from os import chdir, remove, rename
from os.path import basename, dirname, exists, getsize
from time import time
from toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL
from os.path import basename, getsize, exists, dirname
from os import remove, rename, chdir
from time import time, sleep
from tox import Tox
import settings
try:
from PySide import QtCore
except ImportError:
from PyQt4 import QtCore
from common.event import Event
from middleware.threads import invoke_in_main_thread
from wrapper.tox import Tox
from wrapper.toxcore_enums_and_consts import TOX_FILE_CONTROL, TOX_FILE_KIND
# TODO: threads!
FILE_TRANSFER_STATE = {
TOX_FILE_TRANSFER_STATE = {
'RUNNING': 0,
'PAUSED_BY_USER': 1,
'CANCELLED': 2,
'FINISHED': 3,
'PAUSED_BY_FRIEND': 4,
'INCOMING_NOT_STARTED': 5,
'OUTGOING_NOT_STARTED': 6,
'UNSENT': 7
'OUTGOING_NOT_STARTED': 6
}
ACTIVE_FILE_TRANSFERS = (0, 1, 4, 5, 6)
@ -26,105 +30,80 @@ DO_NOT_SHOW_ACCEPT_BUTTON = (2, 3, 4, 6)
SHOW_PROGRESS_BAR = (0, 1, 4)
def is_inline(file_name):
allowed_inlines = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png')
return file_name in allowed_inlines or file_name.startswith('qTox_Image_')
ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png')
class FileTransfer:
class StateSignal(QtCore.QObject):
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):
"""
Superclass for file transfers
"""
def __init__(self, path, tox, friend_number, size, file_number=None):
QtCore.QObject.__init__(self)
self._path = path
self._tox = tox
self._friend_number = friend_number
self._state = FILE_TRANSFER_STATE['RUNNING']
self.state = TOX_FILE_TRANSFER_STATE['RUNNING']
self._file_number = file_number
self._creation_time = None
self._size = float(size)
self._done = 0
self._state_changed_event = Event()
self._finished_event = Event()
self._file_id = self._file = None
self._state_changed = StateSignal()
def set_tox(self, tox):
self._tox = tox
def set_state_changed_handler(self, handler):
self._state_changed_event += lambda *args: invoke_in_main_thread(handler, *args)
self._state_changed.signal.connect(handler)
def set_transfer_finished_handler(self, handler):
self._finished_event += lambda *args: invoke_in_main_thread(handler, *args)
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
file_id = property(get_file_id)
def get_path(self):
return self._path
path = property(get_path)
def get_size(self):
return self._size
size = property(get_size)
def cancel(self):
self.send_control(TOX_FILE_CONTROL['CANCEL'])
if self._file is not None:
self._file.close()
self._signal()
def cancelled(self):
if self._file is not None:
self._file.close()
self.set_state(FILE_TRANSFER_STATE['CANCELLED'])
def pause(self, by_friend):
if not by_friend:
self.send_control(TOX_FILE_CONTROL['PAUSE'])
else:
self.set_state(FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'])
def send_control(self, control):
if self._tox.file_control(self._friend_number, self._file_number, control):
self.set_state(control)
def get_file_id(self):
return self._tox.file_get_file_id(self._friend_number, self._file_number)
def _signal(self):
def signal(self):
percentage = self._done / self._size if self._size else 0
if self._creation_time is None or not percentage:
t = -1
else:
t = ((time() - self._creation_time) / percentage) * (1 - percentage)
self._state_changed_event(self.state, percentage, int(t))
self._state_changed.signal.emit(self.state, percentage, int(t))
def _finished(self):
self._finished_event(self._friend_number, self._file_number)
def get_file_number(self):
return self._file_number
def get_friend_number(self):
return self._friend_number
def cancel(self):
self.send_control(TOX_FILE_CONTROL['CANCEL'])
if hasattr(self, '_file'):
self._file.close()
self.signal()
def cancelled(self):
if hasattr(self, '_file'):
sleep(0.1)
self._file.close()
self.state = TOX_FILE_TRANSFER_STATE['CANCELLED']
self.signal()
def pause(self, by_friend):
if not by_friend:
self.send_control(TOX_FILE_CONTROL['PAUSE'])
else:
self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']
self.signal()
def send_control(self, control):
if self._tox.file_control(self._friend_number, self._file_number, control):
self.state = control
self.signal()
def get_file_id(self):
return self._tox.file_get_file_id(self._friend_number, self._file_number)
# -----------------------------------------------------------------------------------------------------------------
# Send file
@ -135,17 +114,14 @@ class SendTransfer(FileTransfer):
def __init__(self, path, tox, friend_number, kind=TOX_FILE_KIND['DATA'], file_id=None):
if path is not None:
fl = open(path, 'rb')
self._file = open(path, 'rb')
size = getsize(path)
else:
fl = None
size = 0
super().__init__(path, tox, friend_number, size)
self._file = fl
self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
super(SendTransfer, self).__init__(path, tox, friend_number, size)
self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
self._file_number = tox.file_send(friend_number, kind, size, file_id,
bytes(basename(path), 'utf-8') if path else b'')
self._file_id = self.get_file_id()
def send_chunk(self, position, size):
"""
@ -160,12 +136,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 self._file is not None:
if hasattr(self, '_file'):
self._file.close()
self.state = FILE_TRANSFER_STATE['FINISHED']
self._finished()
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
self.signal()
class SendAvatar(SendTransfer):
@ -175,11 +151,11 @@ class SendAvatar(SendTransfer):
def __init__(self, path, tox, friend_number):
if path is None:
avatar_hash = None
hash = None
else:
with open(path, 'rb') as fl:
avatar_hash = Tox.hash(fl.read())
super().__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], avatar_hash)
hash = Tox.hash(fl.read())
super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], hash)
class SendFromBuffer(FileTransfer):
@ -188,8 +164,8 @@ class SendFromBuffer(FileTransfer):
"""
def __init__(self, tox, friend_number, data, file_name):
super().__init__(None, tox, friend_number, len(data))
self.state = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
super(SendFromBuffer, self).__init__(None, tox, friend_number, len(data))
self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
self._data = data
self._file_number = tox.file_send(friend_number, TOX_FILE_KIND['DATA'],
len(data), None, bytes(file_name, 'utf-8'))
@ -197,8 +173,6 @@ class SendFromBuffer(FileTransfer):
def get_data(self):
return self._data
data = property(get_data)
def send_chunk(self, position, size):
if self._creation_time is None:
self._creation_time = time()
@ -206,19 +180,19 @@ 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 = FILE_TRANSFER_STATE['FINISHED']
self._finished()
self._signal()
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
self.signal()
class SendFromFileBuffer(SendTransfer):
def __init__(self, *args):
super().__init__(*args)
super(SendFromFileBuffer, self).__init__(*args)
def send_chunk(self, position, size):
super().send_chunk(position, size)
super(SendFromFileBuffer, self).send_chunk(position, size)
if not size:
chdir(dirname(self._path))
remove(self._path)
@ -230,24 +204,16 @@ class SendFromFileBuffer(SendTransfer):
class ReceiveTransfer(FileTransfer):
def __init__(self, path, tox, friend_number, size, file_number, position=0):
super().__init__(path, tox, friend_number, size, file_number)
def __init__(self, path, tox, friend_number, size, file_number):
super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number)
self._file = open(self._path, 'wb')
self._file_size = position
self._file.truncate(position)
self._missed = set()
self._file_id = self.get_file_id()
self._done = position
self._file.truncate(0)
self._file_size = 0
def cancel(self):
super().cancel()
super(ReceiveTransfer, self).cancel()
remove(self._path)
def total_size(self):
self._missed.add(self._file_size)
return min(self._missed)
def write_chunk(self, position, data):
"""
Incoming chunk
@ -258,23 +224,20 @@ class ReceiveTransfer(FileTransfer):
self._creation_time = time()
if data is None:
self._file.close()
self.state = FILE_TRANSFER_STATE['FINISHED']
self._finished()
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
self.signal()
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):
@ -283,21 +246,18 @@ class ReceiveToBuffer(FileTransfer):
"""
def __init__(self, tox, friend_number, size, file_number):
super().__init__(None, tox, friend_number, size, file_number)
super(ReceiveToBuffer, self).__init__(None, tox, friend_number, size, file_number)
self._data = bytes()
self._data_size = 0
def get_data(self):
return self._data
data = property(get_data)
def write_chunk(self, position, data):
if self._creation_time is None:
self._creation_time = time()
if data is None:
self.state = FILE_TRANSFER_STATE['FINISHED']
self._finished()
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
else:
data = bytes(data)
l = len(data)
@ -307,7 +267,7 @@ class ReceiveToBuffer(FileTransfer):
if position + l > self._data_size:
self._data_size = position + l
self._done += l
self._signal()
self.signal()
class ReceiveAvatar(ReceiveTransfer):
@ -316,17 +276,20 @@ class ReceiveAvatar(ReceiveTransfer):
"""
MAX_AVATAR_SIZE = 512 * 1024
def __init__(self, path, tox, friend_number, size, file_number):
full_path = path + '.tmp'
super().__init__(full_path, tox, friend_number, size, file_number)
def __init__(self, tox, friend_number, size, file_number):
path = settings.ProfileHelper.get_path() + 'avatars/{}.png'.format(tox.friend_get_public_key(friend_number))
super(ReceiveAvatar, self).__init__(path + '.tmp', tox, friend_number, size, file_number)
if size > self.MAX_AVATAR_SIZE:
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self._file.close()
remove(full_path)
remove(path + '.tmp')
elif not size:
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self._file.close()
remove(full_path)
if exists(path):
remove(path)
self._file.close()
remove(path + '.tmp')
elif exists(path):
hash = self.get_file_id()
with open(path, 'rb') as fl:
@ -335,17 +298,17 @@ class ReceiveAvatar(ReceiveTransfer):
if hash == existing_hash:
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self._file.close()
remove(full_path)
remove(path + '.tmp')
else:
self.send_control(TOX_FILE_CONTROL['RESUME'])
else:
self.send_control(TOX_FILE_CONTROL['RESUME'])
def write_chunk(self, position, data):
if data is None:
super(ReceiveAvatar, self).write_chunk(position, data)
if self.state:
avatar_path = self._path[:-4]
if exists(avatar_path):
chdir(dirname(avatar_path))
remove(avatar_path)
rename(self._path, avatar_path)
super().write_chunk(position, data)

View File

@ -1,339 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from messenger.messages import *
from ui.contact_items import *
import utils.util as util
from common.tox_save import ToxSave
from wrapper_tests.support_testing import assert_main_thread
from copy import deepcopy
# LOG=util.log
global LOG
import logging
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):
self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {}
self._settings.save()
# -----------------------------------------------------------------------------------------------------------------
# File transfers support
# -----------------------------------------------------------------------------------------------------------------
def incoming_file_transfer(self, friend_number, file_number, size, file_name):
"""
New transfer
:param friend_number: number of friend who sent file
:param file_number: file number
:param size: file size in bytes
:param file_name: file name without path
"""
friend = self._get_friend_by_number(friend_number)
if friend is None: return None
auto = self._settings['allow_auto_accept'] and friend.tox_id in self._settings['auto_accept_from_friends']
inline = is_inline(file_name) and self._settings['allow_inline']
file_id = self._tox.file_get_file_id(friend_number, file_number)
accepted = True
if file_id in self._paused_file_transfers:
(path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[file_id]
pos = start_position if os.path.exists(path) else 0
if pos >= size:
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL'])
return
self._tox.file_seek(friend_number, file_number, pos)
self._file_transfers_message_service.add_incoming_transfer_message(
friend, accepted, size, file_name, file_number)
self.accept_transfer(path, friend_number, file_number, size, False, pos)
elif inline and size < 1024 * 1024:
self._file_transfers_message_service.add_incoming_transfer_message(
friend, accepted, size, file_name, file_number)
self.accept_transfer('', friend_number, file_number, size, True)
elif auto:
path = self._settings['auto_accept_path'] or util.curr_directory()
self._file_transfers_message_service.add_incoming_transfer_message(
friend, accepted, size, file_name, file_number)
self.accept_transfer(path + '/' + file_name, friend_number, file_number, size)
else:
accepted = False
self._file_transfers_message_service.add_incoming_transfer_message(
friend, accepted, size, file_name, file_number)
def cancel_transfer(self, friend_number, file_number, already_cancelled=False):
"""
Stop transfer
:param friend_number: number of friend
:param file_number: file number
:param already_cancelled: was cancelled by friend
"""
if (friend_number, file_number) in self._file_transfers:
tr = self._file_transfers[(friend_number, file_number)]
if not already_cancelled:
tr.cancel()
else:
tr.cancelled()
if (friend_number, file_number) in self._file_transfers:
del tr
del self._file_transfers[(friend_number, file_number)]
elif not already_cancelled:
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL'])
def cancel_not_started_transfer(self, friend_number, message_id):
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):
"""
Pause transfer with specified data
"""
tr = self._file_transfers[(friend_number, file_number)]
tr.pause(by_friend)
def resume_transfer(self, friend_number, file_number, by_friend=False):
"""
Resume transfer with specified data
"""
tr = self._file_transfers[(friend_number, file_number)]
if by_friend:
tr.state = FILE_TRANSFER_STATE['RUNNING']
else:
tr.send_control(TOX_FILE_CONTROL['RESUME'])
def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0):
"""
:param path: path for saving
:param friend_number: friend number
:param file_number: file number
:param size: file size
:param inline: is inline image
:param from_position: position for start
"""
path = self._generate_valid_path(path, from_position)
friend = self._get_friend_by_number(friend_number)
if 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):
"""
Send screenshot
:param data: raw data - png format
:param friend_number: friend number
"""
self.send_inline(data, 'toxygen_inline.png', friend_number)
def send_sticker(self, path, friend_number):
with open(path, 'rb') as fl:
data = fl.read()
self.send_inline(data, 'sticker.png', friend_number)
def send_inline(self, data, file_name, friend_number, is_resend=False):
friend = self._get_friend_by_number(friend_number)
if friend is None: return None
if friend.status is None and not is_resend:
self._file_transfers_message_service.add_unsent_file_message(friend, file_name, data)
return
elif friend.status is None and is_resend:
raise RuntimeError()
st = SendFromBuffer(self._tox, friend.number, data, file_name)
self._send_file_add_set_handlers(st, friend, file_name, True)
def send_file(self, path, friend_number, is_resend=False, file_id=None):
"""
Send file to current active friend
:param path: file path
:param friend_number: friend_number
:param is_resend: is 'offline' message
:param file_id: file id of transfer
"""
friend = self._get_friend_by_number(friend_number)
if friend 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.error('Error in sending')
return
st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id)
file_name = os.path.basename(path)
self._send_file_add_set_handlers(st, friend, file_name)
def incoming_chunk(self, friend_number, file_number, position, data):
"""
Incoming chunk
"""
self._file_transfers[(friend_number, file_number)].write_chunk(position, data)
def outgoing_chunk(self, friend_number, file_number, position, size):
"""
Outgoing chunk
"""
self._file_transfers[(friend_number, file_number)].send_chunk(position, size)
def transfer_finished(self, friend_number, file_number):
transfer = self._file_transfers[(friend_number, file_number)]
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):
try:
friend = self._get_friend_by_number(friend_number)
if friend is None: return None
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('Exception in file sending: ' + str(ex))
def friend_exit(self, friend_number):
# RuntimeError: dictionary changed size during iteration
lMayChangeDynamically = self._file_transfers.copy()
for friend_num, file_num in lMayChangeDynamically:
if friend_num != friend_number:
continue
if (friend_num, file_num) not in self._file_transfers:
continue
ft = self._file_transfers[(friend_num, file_num)]
if type(ft) is SendTransfer:
self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, False, -1]
elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']:
self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, True, ft.total_size()]
self.cancel_transfer(friend_num, file_num, True)
# -----------------------------------------------------------------------------------------------------------------
# Avatars support
# -----------------------------------------------------------------------------------------------------------------
def send_avatar(self, friend_number, avatar_path=None):
"""
:param friend_number: number of friend who should get new avatar
:param avatar_path: path to avatar or None if reset
"""
if (avatar_path, friend_number,) in self.lBlockAvatars:
return
try:
sa = SendAvatar(avatar_path, self._tox, friend_number)
self._file_transfers[(friend_number, sa.file_number)] = sa
except Exception as e:
# ArgumentError('This client is currently not connected to the friend.')
LOG.error(f"send_avatar {e}")
self.lBlockAvatars.append( (avatar_path, friend_number,) )
def incoming_avatar(self, friend_number, file_number, size):
"""
Friend changed avatar
:param friend_number: friend number
:param file_number: file number
:param size: size of avatar or 0 (default avatar)
"""
friend = self._get_friend_by_number(friend_number)
if friend is None: return None
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, _):
# 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):
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):
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

View File

@ -1,95 +0,0 @@
from messenger.messenger import *
import utils.util as util
from file_transfers.file_transfers import *
global LOG
import logging
LOG = logging.getLogger('app.'+__name__)
def LOG_ERROR(l): print('ERROR_: '+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('TRACE+ '+l)
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'])
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):
"""callback"""
if not self._is_friend_active(transfer.friend_number):
return
if transfer is None or not hasattr(transfer, 'data') or \
not transfer.data:
LOG_ERROR(f"add_inline_message empty data")
return
count = self._messages.count()
if count + index + 1 >= 0:
self._create_inline_item(transfer.data, count + index + 1)
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):
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)

64
toxygen/friend.py Normal file
View File

@ -0,0 +1,64 @@
import contact
from messages import *
class Friend(contact.Contact):
"""
Friend in list of friends. Can be hidden, properties 'has unread messages' and 'has alias' added
"""
def __init__(self, *args):
"""
:param number: number of friend.
"""
super(Friend, self).__init__(*args)
self._receipts = 0
def __del__(self):
super().__del__()
# -----------------------------------------------------------------------------------------------------------------
# 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()
# -----------------------------------------------------------------------------------------------------------------
# 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))

36
toxygen/groupchat.py Normal file
View File

@ -0,0 +1,36 @@
import contact
class GroupChat(contact.Contact):
def __init__(self, tox, *args):
super().__init__(*args)
self._tox = tox
def load_avatar(self, default_path='group.png'):
super().load_avatar(default_path)
def set_status(self, value):
print('In gc set_status')
super().set_status(value)
self.name = bytes(self._tox.group_get_name(self._number), 'utf-8')
self._tox_id = self._tox.group_get_chat_id(self._number)
self.status_message = bytes(self._tox.group_get_topic(self._number), 'utf-8')
def add_peer(self, peer_id):
print(peer_id)
print(self._tox.group_peer_get_name(self._number, peer_id))
# TODO: get peers list and add other methods
def get_peers_list(self):
return []
class Peer:
def __init__(self, peer_id, name, status, role):
self._data = (peer_id, name, status, role)
def get_data(self):
return self._data

View File

@ -1,23 +0,0 @@
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)

View File

@ -1,23 +0,0 @@
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)

View File

@ -1,71 +0,0 @@
class GroupChatPeer:
"""
Represents peer in group chat.
"""
def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False):
self._peer_id = peer_id
self._name = name
self._status = status
self._role = role
self._public_key = public_key
self._is_current_user = is_current_user
self._is_muted = is_muted
# unused?
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)
# -----------------------------------------------------------------------------------------------------------------
# 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)

View File

@ -1,307 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import common.tox_save as tox_save
import utils.ui as util_ui
from groups.peers_list import PeersListGenerator
from groups.group_invite import GroupInvite
import wrapper.toxcore_enums_and_consts as constants
from wrapper.toxcore_enums_and_consts import *
from wrapper.tox import UINT32_MAX
global LOG
import logging
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):
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):
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):
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):
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):
self._tox.group_disconnect(group_number)
group = self._get_group_by_number(group_number)
group.status = None
self._clear_peers_list(group)
def reconnect_to_group(self, group_number):
self._tox.group_reconnect(group_number)
group = self._get_group_by_number(group_number)
group.status = constants.TOX_USER_STATUS['NONE']
self._clear_peers_list(group)
# -----------------------------------------------------------------------------------------------------------------
# Group invites
# -----------------------------------------------------------------------------------------------------------------
def invite_friend(self, friend_number, group_number):
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):
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):
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):
self._delete_group_invite(invite)
self._main_screen.update_gc_invites_button_state()
def get_group_invites(self):
return self._group_invites[:]
group_invites = property(get_group_invites)
def get_group_invites_count(self):
return len(self._group_invites)
group_invites_count = property(get_group_invites_count)
# -----------------------------------------------------------------------------------------------------------------
# Group info methods
# -----------------------------------------------------------------------------------------------------------------
def update_group_info(self, group):
group.name = self._tox.group_get_name(group.number)
group.status_message = self._tox.group_get_topic(group.number)
def set_group_topic(self, group):
if not group.is_self_moderator_or_founder():
return
text = util_ui.tr('New topic for group "{}":'.format(group.name))
title = util_ui.tr('Set group topic')
topic, ok = util_ui.text_dialog(text, title, group.status_message)
if not ok or not topic:
return
self._tox.group_set_topic(group.number, topic)
group.status_message = topic
def show_group_management_screen(self, group):
widgets_factory = self._get_widgets_factory()
self._screen = widgets_factory.create_group_management_screen(group)
self._screen.show()
def show_group_settings_screen(self, group):
widgets_factory = self._get_widgets_factory()
self._screen = widgets_factory.create_group_settings_screen(group)
self._screen.show()
def set_group_password(self, group, password):
if group.password == password:
return
self._tox.group_founder_set_password(group.number, password)
group.password = password
def set_group_peers_limit(self, group, peers_limit):
if group.peers_limit == peers_limit:
return
self._tox.group_founder_set_peer_limit(group.number, peers_limit)
group.peers_limit = peers_limit
def set_group_privacy_state(self, group, privacy_state):
is_private = privacy_state == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE']
if group.is_private == is_private:
return
self._tox.group_founder_set_privacy_state(group.number, privacy_state)
group.is_private = is_private
# -----------------------------------------------------------------------------------------------------------------
# Peers list
# -----------------------------------------------------------------------------------------------------------------
def generate_peers_list(self):
if not self._contacts_manager.is_active_a_group():
return
group = self._contacts_manager.get_curr_contact()
PeersListGenerator().generate(group.peers, self, self._peers_list_widget, group.tox_id)
def peer_selected(self, chat_id, peer_id):
widgets_factory = self._get_widgets_factory()
group = self._get_group_by_public_key(chat_id)
self_peer = group.get_self_peer()
if self_peer.id != peer_id:
self._screen = widgets_factory.create_peer_screen_window(group, peer_id)
else:
self._screen = widgets_factory.create_self_peer_screen_window(group)
self._screen.show()
# -----------------------------------------------------------------------------------------------------------------
# Peers actions
# -----------------------------------------------------------------------------------------------------------------
def set_new_peer_role(self, group, peer, role):
self._tox.group_mod_set_role(group.number, peer.id, role)
peer.role = role
self.generate_peers_list()
def toggle_ignore_peer(self, group, peer, ignore):
self._tox.group_toggle_ignore(group.number, peer.id, ignore)
peer.is_muted = ignore
def set_self_info(self, group, name, status):
self._tox.group_self_set_name(group.number, name)
self._tox.group_self_set_status(group.number, status)
self_peer = group.get_self_peer()
self_peer.name = name
self_peer.status = status
self.generate_peers_list()
# -----------------------------------------------------------------------------------------------------------------
# Bans support
# -----------------------------------------------------------------------------------------------------------------
def show_bans_list(self, group):
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):
self._tox.group_mod_ban_peer(group.number, peer_id, ban_type)
def kick_peer(self, group, peer_id):
self._tox.group_mod_remove_peer(group.number, peer_id)
def cancel_ban(self, group_number, ban_id):
self._tox.group_mod_remove_ban(group_number, ban_id)
# -----------------------------------------------------------------------------------------------------------------
# Private methods
# -----------------------------------------------------------------------------------------------------------------
def _add_new_group_by_number(self, group_number):
LOG.debug(f"_add_new_group_by_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):
return self._contacts_provider.get_friend_by_number(friend_number)
def _get_friend_by_public_key(self, public_key):
return self._contacts_provider.get_friend_by_public_key(public_key)
def _clear_peers_list(self, group):
group.remove_all_peers_except_self()
self.generate_peers_list()
def _delete_group_invite(self, invite):
if invite in self._group_invites:
self._group_invites.remove(invite)
# status should be dropped
def _join_gc_via_invite(self, invite_data, friend_number, nick, status='', password=''):
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):
self._main_screen.update_gc_invites_button_state()
def _get_widgets_factory(self):
return self._widgets_factory_provider.get_item()

View File

@ -1,104 +0,0 @@
from ui.group_peers_list import PeerItem, PeerTypeItem
from wrapper.toxcore_enums_and_consts import *
from ui.widgets import *
# -----------------------------------------------------------------------------------------------------------------
# Builder
# -----------------------------------------------------------------------------------------------------------------
class PeerListBuilder:
def __init__(self):
self._peers = {}
self._titles = {}
self._index = 0
self._handler = None
def with_click_handler(self, handler):
self._handler = handler
return self
def with_title(self, title):
self._titles[self._index] = title
self._index += 1
return self
def with_peers(self, peers):
for peer in peers:
self._add_peer(peer)
return self
def build(self, list_widget):
list_widget.clear()
for i in range(self._index):
if i in self._peers:
peer = self._peers[i]
self._add_peer_item(peer, list_widget)
else:
title = self._titles[i]
self._add_peer_type_item(title, list_widget)
def _add_peer_item(self, peer, parent):
item = PeerItem(peer, self._handler, parent.width(), parent)
self._add_item(parent, item)
def _add_peer_type_item(self, text, parent):
item = PeerTypeItem(text, parent.width(), parent)
self._add_item(parent, item)
@staticmethod
def _add_item(parent, item):
elem = QtWidgets.QListWidgetItem(parent)
elem.setSizeHint(QtCore.QSize(parent.width(), item.height()))
parent.addItem(elem)
parent.setItemWidget(elem, item)
def _add_peer(self, peer):
self._peers[self._index] = peer
self._index += 1
# -----------------------------------------------------------------------------------------------------------------
# Generators
# -----------------------------------------------------------------------------------------------------------------
class PeersListGenerator:
@staticmethod
def generate(peers_list, groups_service, list_widget, chat_id):
admin_title = util_ui.tr('Administrator')
moderators_title = util_ui.tr('Moderators')
users_title = util_ui.tr('Users')
observers_title = util_ui.tr('Observers')
admins = list(filter(lambda p: p.role == TOX_GROUP_ROLE['FOUNDER'], peers_list))
moderators = list(filter(lambda p: p.role == TOX_GROUP_ROLE['MODERATOR'], peers_list))
users = list(filter(lambda p: p.role == TOX_GROUP_ROLE['USER'], peers_list))
observers = list(filter(lambda p: p.role == TOX_GROUP_ROLE['OBSERVER'], peers_list))
builder = (PeerListBuilder()
.with_click_handler(lambda peer_id: groups_service.peer_selected(chat_id, peer_id)))
if len(admins):
(builder
.with_title(admin_title)
.with_peers(admins))
if len(moderators):
(builder
.with_title(moderators_title)
.with_peers(moderators))
if len(users):
(builder
.with_title(users_title)
.with_peers(users))
if len(observers):
(builder
.with_title(observers_title)
.with_peers(observers))
builder.build(list_widget)

184
toxygen/history.py Normal file
View File

@ -0,0 +1,184 @@
# coding=utf-8
from sqlite3 import connect
import settings
from os import chdir
import os.path
from toxencryptsave import ToxEncryptSave
PAGE_SIZE = 42
SAVE_MESSAGES = 150
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()

View File

@ -1,233 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from sqlite3 import connect
import os.path
import utils.util as util
global LOG
import logging
LOG = logging.getLogger('app.db')
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 +' Database exception! ' +str(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 +' Database exception! ' +str(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("" +self._name +' Database exception! ' +str(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("" +self._name +' Database exception! ' +str(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("" +self._name +' Database exception! ' +str(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("" +self._name +' Database exception! ' +str(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)

View File

@ -1,145 +0,0 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from history.history_logs_generators import *
global LOG
import logging
LOG = logging.getLogger('app.db')
class History:
def __init__(self, contact_provider, db, settings, main_screen, messages_items_factory):
self._contact_provider = contact_provider
self._db = db
self._settings = settings
self._messages = main_screen.messages
self._messages_items_factory = messages_items_factory
self._is_loading = False
self._contacts_manager = None
def __del__(self):
del self._db
def set_contacts_manager(self, contacts_manager):
self._contacts_manager = contacts_manager
# -----------------------------------------------------------------------------------------------------------------
# History support
# -----------------------------------------------------------------------------------------------------------------
def save_history(self):
"""
Save history to db
"""
# me a mistake? was _db not _history
if self._settings['save_history']:
for friend in self._contact_provider.get_all_friends():
self._db.add_friend_to_db(friend.tox_id)
if not self._settings['save_unsent_only']:
messages = friend.get_corr_for_saving()
else:
messages = friend.get_unsent_messages_for_saving()
self._db.delete_messages(friend.tox_id)
messages = map(lambda m: (m.text, m.author.name, m.author.type, m.time, m.type), messages)
self._db.save_messages_to_db(friend.tox_id, messages)
self._db.save()
def clear_history(self, friend, save_unsent=False):
"""
Clear chat history
"""
friend.clear_corr(save_unsent)
self._db.delete_friend_from_db(friend.tox_id)
def export_history(self, contact, as_text=True):
extension = 'txt' if as_text else 'html'
file_name, _ = util_ui.save_file_dialog(util_ui.tr('Choose file name'), extension)
if not file_name:
return
if not file_name.endswith('.' + extension):
file_name += '.' + extension
history = self.generate_history(contact, as_text)
assert history
with open(file_name, 'wt') as fl:
fl.write(history)
LOG.info(f"wrote history to {file_name}")
def delete_message(self, message):
contact = self._contacts_manager.get_curr_contact()
if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']):
if message.is_saved():
self._db.delete_message(contact.tox_id, message.id)
contact.delete_message(message.message_id)
def load_history(self, friend):
"""
Tries to load next part of messages
"""
if self._is_loading:
return
self._is_loading = True
friend.load_corr(False)
messages = friend.get_corr()
if not messages:
self._is_loading = False
return
messages.reverse()
messages = messages[self._messages.count():self._messages.count() + PAGE_SIZE]
for message in messages:
message_type = message.get_type()
if message_type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): # text message
self._create_message_item(message)
elif message_type == MESSAGE_TYPE['FILE_TRANSFER']: # file transfer
if message.state == FILE_TRANSFER_STATE['UNSENT']:
self._create_unsent_file_item(message)
else:
self._create_file_transfer_item(message)
elif message_type == MESSAGE_TYPE['INLINE']: # inline image
self._create_inline_item(message)
else: # info message
self._create_message_item(message)
self._is_loading = False
def get_message_getter(self, friend_public_key):
self._db.add_friend_to_db(friend_public_key)
return self._db.messages_getter(friend_public_key)
def delete_history(self, friend):
self._db.delete_friend_from_db(friend.tox_id)
def add_friend_to_db(self, tox_id):
self._db.add_friend_to_db(tox_id)
@staticmethod
def generate_history(contact, as_text=True, _range=None):
if _range is None:
contact.load_all_corr()
corr = contact.get_corr()
elif _range[1] + 1:
corr = contact.get_corr()[_range[0]:_range[1] + 1]
else:
corr = contact.get_corr()[_range[0]:]
generator = TextHistoryGenerator(corr, contact.name) if as_text else HtmlHistoryGenerator(corr, contact.name)
return generator.generate()
# -----------------------------------------------------------------------------------------------------------------
# Items creation
# -----------------------------------------------------------------------------------------------------------------
def _create_message_item(self, message):
return self._messages_items_factory.create_message_item(message, False)
def _create_unsent_file_item(self, message):
return self._messages_items_factory.create_unsent_file_item(message, False)
def _create_file_transfer_item(self, message):
return self._messages_items_factory.create_file_transfer_item(message, False)
def _create_inline_item(self, message):
return self._messages_items_factory.create_inline_item(message, False)

View File

@ -1,48 +0,0 @@
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 = '[{}] <b>{}:</b> {}<br>'
arr.append(x.format(self._get_message_time(message), message.author.name, message.text))
s = '<br>'.join(arr)
html = '<html><head><meta charset="UTF-8"><title>{}</title></head><body>{}</body></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)

BIN
toxygen/images/accept.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 114 KiB

BIN
toxygen/images/accept_audio.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
toxygen/images/accept_video.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
toxygen/images/audio_message.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
toxygen/images/avatar.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 609 B

BIN
toxygen/images/call.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
toxygen/images/decline.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 118 KiB

BIN
toxygen/images/decline_call.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
toxygen/images/file.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
toxygen/images/finish_call.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

BIN
toxygen/images/group.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

BIN
toxygen/images/icon_new_messages.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

After

Width:  |  Height:  |  Size: 405 B

BIN
toxygen/images/incoming_call.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

BIN
toxygen/images/menu.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 351 B

BIN
toxygen/images/pause.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 B

After

Width:  |  Height:  |  Size: 306 B

BIN
toxygen/images/resume.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
toxygen/images/screenshot.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Some files were not shown because too many files have changed in this diff Show More