Compare commits
79 Commits
Author | SHA1 | Date |
---|---|---|
emdee@spm.plastiras.org | 8c9d53903f | 2 months ago |
emdee@spm.plastiras.org | ef68b7e2e2 | 3 months ago |
emdee@spm.plastiras.org | affaa3814b | 3 months ago |
emdee@spm.plastiras.org | 9c1014ee5e | 3 months ago |
emdee@spm.plastiras.org | ec79c0d6ae | 3 months ago |
emdee@spm.plastiras.org | 2717f4f6e5 | 3 months ago |
emdee@spm.plastiras.org | f7e260a355 | 3 months ago |
emdee@spm.plastiras.org | d9ef18631d | 3 months ago |
emdee@spm.plastiras.org | 76ad2ccd44 | 3 months ago |
emdee@spm.plastiras.org | dda2a9147a | 3 months ago |
emdee@spm.plastiras.org | d936663591 | 3 months ago |
emdee@spm.plastiras.org | ac6999924f | 3 months ago |
emdee@spm.plastiras.org | 31bed51455 | 3 months ago |
emdee@spm.plastiras.org | dd8ed70958 | 3 months ago |
emdee@spm.plastiras.org | d1c8d445bc | 3 months ago |
emdee@spm.plastiras.org | 4d2d4034f9 | 3 months ago |
emdee@spm.plastiras.org | c70c501fdd | 3 months ago |
emdee@spm.plastiras.org | e778108834 | 3 months ago |
emdee@spm.plastiras.org | 1c56b5b25a | 3 months ago |
emdee@spm.plastiras.org | ea454e27a1 | 3 months ago |
emdee@spm.plastiras.org | f62e28f5b4 | 3 months ago |
emdee@spm.plastiras.org | 7cebe9cd9f | 3 months ago |
emdee@spm.plastiras.org | 3ce822fc27 | 4 months ago |
emdee@spm.plastiras.org | e4b1b9c4d8 | 5 months ago |
emdee@macaw.me | 68f28fdac5 | 5 months ago |
emdee | 99136cd4e3 | 5 months ago |
emdee | 65d593cd20 | 5 months ago |
emdee | 9f32dc3f8a | 5 months ago |
emdee | 48efb5a44e | 5 months ago |
emdee | ba013b6a81 | 5 months ago |
emdee | 4109c822b3 | 5 months ago |
emdee | 4e77ddc2de | 5 months ago |
emdee | dcde8e3d1e | 10 months ago |
emdee | 948335c8a0 | 2 years ago |
emdee | 0b1eaa1391 | 2 years ago |
emdee | 424e15b31c | 2 years ago |
emdee | db37d29dc8 | 2 years ago |
emdee | f1d8ce105c | 2 years ago |
emdee | 1e5618060a | 2 years ago |
emdee | 1b8b26eafc | 2 years ago |
emdee | a073dd9bc9 | 2 years ago |
emdee | 5df00c3ccd | 2 years ago |
emdee | 0819fd4088 | 2 years ago |
emdee | 5f1b7d8d93 | 2 years ago |
emdee | cf5c5b1608 | 2 years ago |
emdee | 90e379a6de | 2 years ago |
emdee | a92bbbbcbf | 2 years ago |
emdee | d2fe721072 | 2 years ago |
emdee | fd7f2620ba | 2 years ago |
emdee | b75aafe638 | 2 years ago |
emdee | f7c0e7ce23 | 2 years ago |
emdee | 633b8f9561 | 2 years ago |
emdee | fb520357e9 | 2 years ago |
emdee | be6eb0e2a9 | 2 years ago |
emdee | 9e037f13c0 | 2 years ago |
emdee | ca9c6fc091 | 2 years ago |
emdee | 2916d0cb04 | 2 years ago |
emdee | 695d8e2cf9 | 2 years ago |
emdee | c5edc1f01b | 2 years ago |
emdee | a7c07ffdf7 | 2 years ago |
emdee | cdb0db5b4b | 2 years ago |
emdee | a365b7d54c | 2 years ago |
emdee | 870e3125ad | 2 years ago |
emdee | 675bf1b2b9 | 2 years ago |
emdee | cab3b4d9af | 2 years ago |
emdee | 9008bcdb7f | 2 years ago |
emdee | 61b926fe50 | 2 years ago |
emdee | 39f2638931 | 2 years ago |
emdee | 6f0c1a444e | 2 years ago |
emdee | b51ec9bd71 | 2 years ago |
emdee | fda07698db | 2 years ago |
ingvar1995 | 0a54012cf5 | 4 years ago |
ingvar1995 | 021ec52e3d | 4 years ago |
ingvar1995 | 5019535c0d | 4 years ago |
ingvar1995 | 1554d9e53a | 4 years ago |
ingvar1995 | a984b624b5 | 4 years ago |
ingvar1995 | 2aea5df33c | 6 years ago |
ingvar1995 | 1fa13db4e4 | 6 years ago |
ingvar1995 | 3582722faa | 6 years ago |
@ -0,0 +1,43 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
|
||||
name: Python ${{ matrix.python-version }}
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install bandit flake8 pylint
|
||||
|
||||
- name: Lint with flake8
|
||||
run: make flake8
|
||||
|
||||
# - name: Lint with pylint
|
||||
# run: make pylint
|
||||
|
||||
- name: Lint with bandit
|
||||
run: make bandit
|
@ -0,0 +1,23 @@
|
||||
# -*- mode: yaml; indent-tabs-mode: nil; tab-width: 2; coding: utf-8-unix -*-
|
||||
---
|
||||
|
||||
default_language_version:
|
||||
python: python3.11
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: true
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: env PYTHONPATH=/mnt/o/var/local/src/toxygen.git/toxygen toxcore_pylint.bash
|
||||
language: system
|
||||
types: [python]
|
||||
args:
|
||||
[
|
||||
"--source-roots=/mnt/o/var/local/src/toxygen.git/toxygen",
|
||||
"-rn", # Only display messages
|
||||
"-sn", # Don't display the score
|
||||
"--rcfile=/usr/local/etc/testforge/pylint.rc", # Link to your config file
|
||||
"-E"
|
||||
]
|
@ -0,0 +1,4 @@
|
||||
[pre-commit-hook]
|
||||
command=env PYTHONPATH=/mnt/o/var/local/src/toxygen.git/toxygen /usr/local/bin/toxcore_pylint.bash
|
||||
params= -E --exit-zero
|
||||
limit=8
|
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
#find * -name \*.py | xargs grep -l '[ ]*$' | xargs sed -i -e 's/[ ]*$//'
|
||||
rsync "$@" -vaxL --include \*.py \
|
||||
--exclude Toxygen.egg-info --exclude build \
|
||||
--exclude \*.pyc --exclude .pyl\* --exclude \*.so --exclude \*~ \
|
||||
--exclude __pycache__ --exclude \*.egg-info --exclude \*.new \
|
||||
./ ../toxygen.git/|grep -v /$
|
@ -0,0 +1,70 @@
|
||||
# Toxygen ToDo List
|
||||
|
||||
## Bugs
|
||||
|
||||
1. There is an agravating bug where new messages are not put in the
|
||||
current window, and a messages waiting indicator appears. You have
|
||||
to focus out of the window and then back in the window. this may be
|
||||
fixed already
|
||||
|
||||
2. The tray icon is flaky and has been disabled - look in app.py
|
||||
for bSHOW_TRAY
|
||||
|
||||
## Fix history
|
||||
|
||||
## Fix Audio
|
||||
|
||||
The code is in there but it's not working. It looks like audio input
|
||||
is working but not output. The code is all in there; I may have broken
|
||||
it trying to wire up the ability to set the audio device from the
|
||||
command line.
|
||||
|
||||
## Fix Video
|
||||
|
||||
The code is in there but it's not working. I may have broken it
|
||||
trying to wire up the ability to set the video device from the command
|
||||
line.
|
||||
|
||||
## NGC Groups
|
||||
|
||||
1. peer_id There has been a change of API on a field named
|
||||
```group.peer_id``` The code is broken in places because I have not
|
||||
seen the path to change from the old API ro the new one.
|
||||
|
||||
|
||||
## Plugin system
|
||||
|
||||
1. Needs better documentation and checking.
|
||||
|
||||
2. There's something broken in the way some of them plug into Qt menus.
|
||||
|
||||
3. Should the plugins be in toxygen or a separate repo?
|
||||
|
||||
4. There needs to be a uniform way for plugins to wire into callbacks.
|
||||
|
||||
## check toxygen_wrapper
|
||||
|
||||
1. I've broken out toxygen_wrapper to be standalone,
|
||||
https://git.plastiras.org/emdee/toxygen_wrapper but the tox.py
|
||||
needs each call double checking.
|
||||
|
||||
2. https://git.plastiras.org/emdee/toxygen_wrapper needs packaging
|
||||
and making a dependency.
|
||||
|
||||
## Migration
|
||||
|
||||
Migrate PyQt5 to qtpy - done, but I'm not sure qtpy supports PyQt6.
|
||||
https://github.com/spyder-ide/qtpy/
|
||||
|
||||
Maybe migrate gevent to asyncio, and migrate to
|
||||
[qasync](https://github.com/CabbageDevelopment/qasync)
|
||||
(see https://git.plastiras.org/emdee/phantompy ).
|
||||
|
||||
(Also look at https://pypi.org/project/asyncio-gevent/ but it's dead).
|
||||
|
||||
## Standards
|
||||
|
||||
There's a standard for Tox clients that this has not been tested against:
|
||||
https://tox.gitbooks.io/tox-client-standard/content/general_requirements/general_requirements.html
|
||||
https://github.com/Tox/Tox-Client-Standard
|
||||
|
@ -0,0 +1,130 @@
|
||||
0
|
||||
TRAC> network.c#1748:net_connect connecting socket 58 to 127.0.0.1:9050
|
||||
TRAC> Messenger.c#2709:do_messenger Friend num in DHT 2 != friend num in msger 14
|
||||
TRAC> Messenger.c#2723:do_messenger F[--: 0] D3385007C28852C5398393E3338E6AABE5F86EF249BF724E7404233207D4D927
|
||||
TRAC> Messenger.c#2723:do_messenger F[--: 1] 98984E104B8A97CC43AF03A27BE159AC1F4CF35FADCC03D6CD5F8D67B5942A56
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 185.87.49.189:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 185.87.49.189:3389 (0: OK) | 010001b95731bd0d...3d
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 37.221.66.161:443 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 37.221.66.161:443 (0: OK) | 01000125dd42a101...bb
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 172.93.52.70:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 139.162.110.188:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 37.59.63.150:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 130.133.110.14:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 37.97.185.116:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 85.143.221.42:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 104.244.74.69:38445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 49.12.229.145:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 168.119.209.10:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 81.169.136.229:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 91.219.59.156:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 46.101.197.175:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 198.199.98.108:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 130.133.110.14:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 49.12.229.145:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 188.225.9.167:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 5.19.249.240:38296 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 3= 94.156.35.247:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 172.93.52.70:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 172.93.52.70:33445 (0: OK) | 010001ac5d344682...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 139.162.110.188:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 139.162.110.188:33445 (0: OK) | 0100018ba26ebc82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 37.59.63.150:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 37.59.63.150:33445 (0: OK) | 010001253b3f9682...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 130.133.110.14:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 130.133.110.14:33445 (0: OK) | 01000182856e0e82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 37.97.185.116:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 37.97.185.116:33445 (0: OK) | 0100012561b97482...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 85.143.221.42:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 85.143.221.42:33445 (0: OK) | 010001558fdd2a82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 104.244.74.69:38445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 104.244.74.69:38445 (0: OK) | 01000168f44a4596...2d
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 49.12.229.145:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 49.12.229.145:3389 (0: OK) | 010001310ce5910d...3d
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 168.119.209.10:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 168.119.209.10:33445 (0: OK) | 010001a877d10a82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 81.169.136.229:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 81.169.136.229:33445 (0: OK) | 01000151a988e582...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 91.219.59.156:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 91.219.59.156:33445 (0: OK) | 0100015bdb3b9c82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 46.101.197.175:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 46.101.197.175:3389 (0: OK) | 0100012e65c5af0d...3d
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 198.199.98.108:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 198.199.98.108:33445 (0: OK) | 010001c6c7626c82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 130.133.110.14:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 130.133.110.14:33445 (0: OK) | 01000182856e0e82...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 49.12.229.145:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 49.12.229.145:3389 (0: OK) | 010001310ce5910d...3d
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 188.225.9.167:33445 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 188.225.9.167:33445 (0: OK) | 010001bce109a782...a5
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 5.19.249.240:38296 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 5.19.249.240:38296 (0: OK) | 0100010513f9f095...98
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] =>T 2= 94.156.35.247:3389 (0: OK) | 0000000000000000...00
|
||||
TRAC> network.c#789:loglogdata [05 = <unknown> ] T=> 10= 94.156.35.247:3389 (0: OK) | 0100015e9c23f70d...3d
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
|
||||
app.contacts.contacts_manager INFO update_groups_numbers len(groups)={len(groups)}
|
||||
|
||||
Thread 76 "ToxIterateThrea" received signal SIGSEGV, Segmentation fault.
|
||||
[Switching to Thread 0x7ffedcb6b640 (LWP 2950427)]
|
@ -0,0 +1,11 @@
|
||||
ping tox.abilinski.com
|
||||
ping: socket: Address family not supported by protocol
|
||||
PING tox.abilinski.com (172.103.226.229) 56(84) bytes of data.
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=1 ttl=48 time=86.6 ms
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=2 ttl=48 time=83.1 ms
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=3 ttl=48 time=82.9 ms
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=4 ttl=48 time=83.4 ms
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=5 ttl=48 time=102 ms
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=6 ttl=48 time=87.4 ms
|
||||
64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=7 ttl=48 time=84.9 ms
|
||||
^C
|
@ -0,0 +1,171 @@
|
||||
## Toxygen Weechat
|
||||
|
||||
You can have a [weechat](https://github.com/weechat/qweechat)
|
||||
console so that you can have IRC and jabber in a window as well as Tox.
|
||||
There's a copy of qweechat in ```thirdparty/qweechat``` backported to
|
||||
PyQt5 and integrated into toxygen. Follow the normal instructions for
|
||||
adding a ```relay``` to [weechat](https://github.com/weechat/weechat)
|
||||
```
|
||||
/relay add ipv4.ssl.weechat 9000
|
||||
/relay start ipv4.ssl.weechat
|
||||
```
|
||||
or
|
||||
```
|
||||
/set relay.network.ipv6 off
|
||||
/set relay.network.password password
|
||||
/relay add weechat 9000
|
||||
/relay start weechat
|
||||
```
|
||||
and use the Plugins/Weechat Console to start weechat under Toxygen.
|
||||
Then use the File/Connect menu item of the Console to connect to weechat.
|
||||
|
||||
Weechat has a Jabber plugin to enable XMPP:
|
||||
```
|
||||
/python load jabber.el
|
||||
/help jabber
|
||||
```
|
||||
so you can have Tox, IRC and XMPP in the same application!
|
||||
|
||||
### Creating servers for IRC over Tor
|
||||
|
||||
Create a proxy called tor
|
||||
```
|
||||
/proxy add tor socks5 127.0.0.1 9050
|
||||
```
|
||||
|
||||
It should now show up in the list of proxies.
|
||||
```
|
||||
/proxy list
|
||||
```
|
||||
|
||||
```
|
||||
/nick NickName
|
||||
```
|
||||
|
||||
## TLS certificates
|
||||
|
||||
[Create a Self-signed Certificate](https://www.oftc.net/NickServ/CertFP/)
|
||||
|
||||
Choose a NickName you will identify as.
|
||||
|
||||
Create a directory for your certificates ~/.config/weechat/ssl/
|
||||
and make a subdirectory for each server ~/.config/weechat/ssl/irc.oftc.net/
|
||||
|
||||
Change to the server directory and use openssl to make a keypair and answer the questions:
|
||||
```
|
||||
openssl req -nodes -newkey rsa:2048 -keyout NickName.key -x509 -days 3650 -out NickName.cer
|
||||
chmod 400 NickName.key
|
||||
```
|
||||
We now combine certificate and key to a single file NickName.pem
|
||||
```
|
||||
cat NickName.cer NickName.key > NickName.pem
|
||||
chmod 400 NickName.pem
|
||||
```
|
||||
|
||||
Do this for each server you want to connect to, or just use one for all of them.
|
||||
|
||||
### Libera TokTok channel
|
||||
|
||||
The main discussion forum for Tox is the #TokTok channel on libera.
|
||||
|
||||
https://mox.sh/sysadmin/secure-irc-connection-to-freenode-with-tor-and-weechat/
|
||||
We have to create an account without Tor, this is a requirement to use TOR:
|
||||
Connect to irc.libera.chat without Tor and register
|
||||
```
|
||||
/msg NickServ identify NickName password
|
||||
/msg NickServ REGISTER mypassword mycoolemail@example.com
|
||||
/msg NickServ SET PRIVATE ON
|
||||
```
|
||||
You'll get an email with a registration code.
|
||||
Confirm registration after getting the mail with the code:
|
||||
```
|
||||
/msg NickServ VERIFY REGISTER NickName code1235678
|
||||
```
|
||||
|
||||
Libera has an onion server so we can map an address in tor. Add this
|
||||
to your /etc/tor/torrc
|
||||
```
|
||||
MapAddress palladium.libera.chat libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion
|
||||
```
|
||||
Or without the MapAddress just use
|
||||
libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion
|
||||
as the server address below, but set tls_verify to off.
|
||||
|
||||
Define the server in weechat
|
||||
https://www.weechat.org/files/doc/stable/weechat_user.en.html#irc_sasl_authentication
|
||||
```
|
||||
/server remove libera
|
||||
/server add libera palladium.libera.chat/6697 -tls -tls_verify
|
||||
/set irc.server.libera.ipv6 off
|
||||
/set irc.server.libera.proxy tor
|
||||
/set irc.server.libera.username NickName
|
||||
/set irc.server.libera.password password
|
||||
/set irc.server.libera.nicks NickName
|
||||
/set irc.server.libera.tls on
|
||||
/set irc.server.libera.tls_cert "${weechat_config_dir}/ssl/libera.chat/NickName.pem"
|
||||
```
|
||||
|
||||
```
|
||||
/set irc.server.libera.sasl_mechanism ecdsa-nist256p-challenge
|
||||
/set irc.server.libera.sasl_username "NickName"
|
||||
/set irc.server.libera.sasl_key "${weechat_config_dir}/ssl/libera.chat/NickName.pem"
|
||||
```
|
||||
|
||||
Disconnect and connect back to the server.
|
||||
```
|
||||
/disconnect libera
|
||||
/connect libera
|
||||
```
|
||||
|
||||
/msg nickserv identify password NickName
|
||||
|
||||
|
||||
### oftc.net
|
||||
|
||||
To use oftc.net over tor, you need to authenticate by SSL certificates.
|
||||
|
||||
|
||||
Define the server in weechat
|
||||
```
|
||||
/server remove irc.oftc.net
|
||||
/server add OFTC irc.oftc.net/6697 -tls -tls_verify
|
||||
/set irc.server.OFTC.ipv6 off
|
||||
/set irc.server.OFTC.proxy tor
|
||||
/set irc.server.OFTC.username NickName
|
||||
/set irc.server.OFTC.nicks NickName
|
||||
/set irc.server.OFTC.tls on
|
||||
/set irc.server.OFTC.tls_cert "${weechat_config_dir}/ssl/irc.oftc.chat/NickName.pem"
|
||||
|
||||
# Disconnect and connect back to the server.
|
||||
/disconnect OFTC
|
||||
/connect OFTC
|
||||
```
|
||||
You must be identified in order to validate using certs
|
||||
```
|
||||
/msg nickserv identify password NickName
|
||||
```
|
||||
To allow NickServ to identify you based on this certificate you need
|
||||
to associate the certificate fingerprint with your nick. To do this
|
||||
issue the command cert add to Nickserv (try /msg nickserv helpcert).
|
||||
```
|
||||
/msg nickserv cert add
|
||||
```
|
||||
|
||||
### Privacy
|
||||
|
||||
[Add somes settings bellow to weechat](https://szorfein.github.io/weechat/tor/configure-weechat/).
|
||||
Detail from [faq](https://weechat.org/files/doc/weechat_faq.en.html#security).
|
||||
|
||||
```
|
||||
/set irc.server_default.msg_part ""
|
||||
/set irc.server_default.msg_quit ""
|
||||
/set irc.ctcp.clientinfo ""
|
||||
/set irc.ctcp.finger ""
|
||||
/set irc.ctcp.source ""
|
||||
/set irc.ctcp.time ""
|
||||
/set irc.ctcp.userinfo ""
|
||||
/set irc.ctcp.version ""
|
||||
/set irc.ctcp.ping ""
|
||||
/plugin unload xfer
|
||||
/set weechat.plugin.autoload "*,!xfer"
|
||||
```
|
@ -1,5 +1,6 @@
|
||||
# Contact us:
|
||||
|
||||
1) Using GitHub - open issue
|
||||
1) https://git.plastiras.org/emdee/toxygen/issues
|
||||
|
||||
2) Use Toxygen Tox Group - add bot kalina@toxme.io (or 12EDB939AA529641CE53830B518D6EB30241868EE0E5023C46A372363CAEC91C2C948AEFE4EB)
|
||||
2) Use Toxygen Tox Group (NGC) -
|
||||
ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA
|
||||
|
@ -0,0 +1,70 @@
|
||||
# Toxygen ToDo List
|
||||
|
||||
## Bugs
|
||||
|
||||
1. There is an agravating bug where new messages are not put in the
|
||||
current window, and a messages waiting indicator appears. You have
|
||||
to focus out of the window and then back in the window. this may be
|
||||
fixed already
|
||||
|
||||
2. The tray icon is flaky and has been disabled - look in app.py
|
||||
for bSHOW_TRAY
|
||||
|
||||
## Fix history
|
||||
|
||||
## Fix Audio
|
||||
|
||||
The code is in there but it's not working. It looks like audio input
|
||||
is working but not output. The code is all in there; I may have broken
|
||||
it trying to wire up the ability to set the audio device from the
|
||||
command line.
|
||||
|
||||
## Fix Video
|
||||
|
||||
The code is in there but it's not working. I may have broken it
|
||||
trying to wire up the ability to set the video device from the command
|
||||
line.
|
||||
|
||||
## NGC Groups
|
||||
|
||||
1. peer_id There has been a change of API on a field named
|
||||
```group.peer_id``` The code is broken in places because I have not
|
||||
seen the path to change from the old API ro the new one.
|
||||
|
||||
|
||||
## Plugin system
|
||||
|
||||
1. Needs better documentation and checking.
|
||||
|
||||
2. There's something broken in the way some of them plug into Qt menus.
|
||||
|
||||
3. Should the plugins be in toxygen or a separate repo?
|
||||
|
||||
4. There needs to be a uniform way for plugins to wire into callbacks.
|
||||
|
||||
## check toxygen_wrapper
|
||||
|
||||
1. I've broken out toxygen_wrapper to be standalone,
|
||||
https://git.plastiras.org/emdee/toxygen_wrapper but the tox.py
|
||||
needs each call double checking.
|
||||
|
||||
2. https://git.plastiras.org/emdee/toxygen_wrapper needs packaging
|
||||
and making a dependency.
|
||||
|
||||
## Migration
|
||||
|
||||
Migrate PyQt5 to qtpy - done, but I'm not sure qtpy supports PyQt6.
|
||||
https://github.com/spyder-ide/qtpy/
|
||||
|
||||
Maybe migrate gevent to asyncio, and migrate to
|
||||
[qasync](https://github.com/CabbageDevelopment/qasync)
|
||||
(see https://git.plastiras.org/emdee/phantompy ).
|
||||
|
||||
(Also look at https://pypi.org/project/asyncio-gevent/ but it's dead).
|
||||
|
||||
## Standards
|
||||
|
||||
There's a standard for Tox clients that this has not been tested against:
|
||||
https://tox.gitbooks.io/tox-client-standard/content/general_requirements/general_requirements.html
|
||||
https://github.com/Tox/Tox-Client-Standard
|
||||
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 71 KiB |
@ -0,0 +1,56 @@
|
||||
[project]
|
||||
name = "toxygen"
|
||||
description = "examples of using stem"
|
||||
authors = [{ name = "emdee", email = "emdee@spm.plastiras.org" } ]
|
||||
requires-python = ">=3.7"
|
||||
keywords = ["stem", "python3", "tox"]
|
||||
classifiers = [
|
||||
# How mature is this project? Common values are
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
"Development Status :: 4 - Beta",
|
||||
|
||||
# Indicate who your project is intended for
|
||||
"Intended Audience :: Developers",
|
||||
|
||||
# Specify the Python versions you support here.
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved",
|
||||
"Operating System :: POSIX :: BSD :: FreeBSD",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
]
|
||||
#
|
||||
dynamic = ["version", "readme", "dependencies"] # cannot be dynamic ['license']
|
||||
|
||||
[project.gui-scripts]
|
||||
toxygen = "toxygen.__main__:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
weechat = ["weechat"]
|
||||
|
||||
#[project.license]
|
||||
#file = "LICENSE.md"
|
||||
|
||||
[project.urls]
|
||||
repository = "https://git.plastiras.org/emdee/toxygen"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools >= 61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "toxygen.app.__version__"}
|
||||
readme = {file = ["README.md", "ToDo.txt"]}
|
||||
dependencies = {file = ["requirements.txt"]}
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["toxygen"]
|
@ -0,0 +1,26 @@
|
||||
# the versions are the current ones tested - may work with earlier versions
|
||||
# choose one of PyQt5 PyQt6 PySide2 PySide6
|
||||
# for now PyQt5 and PyQt6 is working, and most of the testing is PyQt5
|
||||
# usually this is installed by your OS package manager and pip may not
|
||||
# detect the right version, so we leave these commented
|
||||
# PyQt5 >= 5.15.10
|
||||
# this is not on pypi yet but is required - get it from
|
||||
# https://git.plastiras.org/emdee/toxygen_wrapper
|
||||
# toxygen_wrapper == 1.0.0
|
||||
QtPy >= 2.4.1
|
||||
PyAudio >= 0.2.13
|
||||
numpy >= 1.26.1
|
||||
opencv_python >= 4.8.0
|
||||
pillow >= 10.2.0
|
||||
gevent >= 23.9.1
|
||||
pydenticon >= 0.3.1
|
||||
greenlet >= 2.0.2
|
||||
sounddevice >= 0.3.15
|
||||
# this is optional
|
||||
coloredlogs >= 15.0.1
|
||||
# this is optional
|
||||
# qtconsole >= 5.4.3
|
||||
# this is not on pypi yet but is optional for qweechat - get it from
|
||||
# https://git.plastiras.org/emdee/qweechat
|
||||
# qweechat_wrapper == 0.0.1
|
||||
|
@ -0,0 +1,54 @@
|
||||
[metadata]
|
||||
classifiers =
|
||||
License :: OSI Approved
|
||||
License :: OSI Approved :: BSD 1-clause
|
||||
Intended Audience :: Web Developers
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: POSIX :: BSD :: FreeBSD
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
|
||||
[options]
|
||||
zip_safe = false
|
||||
python_requires = ~=3.7
|
||||
include_package_data =
|
||||
"*" = ["*.ui", "*.txt", "*.png", "*.ico", "*.gif", "*.wav"]
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
toxygen = toxygen.__main__:iMain
|
||||
|
||||
[easy_install]
|
||||
zip_ok = false
|
||||
|
||||
[flake8]
|
||||
jobs = 1
|
||||
max-line-length = 88
|
||||
ignore =
|
||||
E111
|
||||
E114
|
||||
E128
|
||||
E225
|
||||
E261
|
||||
E302
|
||||
E305
|
||||
E402
|
||||
E501
|
||||
E502
|
||||
E541
|
||||
E701
|
||||
E702
|
||||
E704
|
||||
E722
|
||||
E741
|
||||
F508
|
||||
F541
|
||||
W503
|
||||
W601
|
@ -1,78 +0,0 @@
|
||||
from setuptools import setup
|
||||
from setuptools.command.install import install
|
||||
from platform import system
|
||||
from subprocess import call
|
||||
from toxygen.util import program_version
|
||||
import sys
|
||||
|
||||
|
||||
version = program_version + '.0'
|
||||
|
||||
|
||||
if system() == 'Windows':
|
||||
MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python']
|
||||
else:
|
||||
MODULES = []
|
||||
try:
|
||||
import pyaudio
|
||||
except ImportError:
|
||||
MODULES.append('PyAudio')
|
||||
try:
|
||||
import PyQt5
|
||||
except ImportError:
|
||||
MODULES.append('PyQt5')
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
MODULES.append('numpy')
|
||||
try:
|
||||
import cv2
|
||||
except ImportError:
|
||||
MODULES.append('opencv-python')
|
||||
|
||||
|
||||
class InstallScript(install):
|
||||
"""This class configures Toxygen after installation"""
|
||||
|
||||
def run(self):
|
||||
install.run(self)
|
||||
try:
|
||||
if system() != 'Windows':
|
||||
call(["toxygen", "--clean"])
|
||||
except:
|
||||
try:
|
||||
params = list(filter(lambda x: x.startswith('--prefix='), sys.argv))
|
||||
if params:
|
||||
path = params[0][len('--prefix='):]
|
||||
if path[-1] not in ('/', '\\'):
|
||||
path += '/'
|
||||
path += 'bin/toxygen'
|
||||
if system() != 'Windows':
|
||||
call([path, "--clean"])
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
setup(name='Toxygen',
|
||||
version=version,
|
||||
description='Toxygen - Tox client',
|
||||
long_description='Toxygen is powerful Tox client written in Python3',
|
||||
url='https://github.com/toxygen-project/toxygen/',
|
||||
keywords='toxygen tox messenger',
|
||||
author='Ingvar',
|
||||
maintainer='Ingvar',
|
||||
license='GPL3',
|
||||
packages=['toxygen', 'toxygen.plugins', 'toxygen.styles'],
|
||||
install_requires=MODULES,
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['toxygen=toxygen.main:main'],
|
||||
},
|
||||
cmdclass={
|
||||
'install': InstallScript,
|
||||
})
|
@ -0,0 +1,53 @@
|
||||
import sys
|
||||
import os
|
||||
from setuptools import setup
|
||||
from setuptools.command.install import install
|
||||
|
||||
version = '1.0.0'
|
||||
|
||||
MODULES = open('requirements.txt', 'rt').readlines()
|
||||
|
||||
def get_packages():
|
||||
directory = os.path.join(os.path.dirname(__file__), 'tox_wrapper')
|
||||
for root, dirs, files in os.walk(directory):
|
||||
packages = map(lambda d: 'toxygen.' + d, dirs)
|
||||
packages = ['toxygen'] + list(packages)
|
||||
return packages
|
||||
|
||||
class InstallScript(install):
|
||||
"""This class configures Toxygen after installation"""
|
||||
|
||||
def run(self):
|
||||
install.run(self)
|
||||
|
||||
setup(name='Toxygen',
|
||||
version=version,
|
||||
description='Toxygen - Tox client',
|
||||
long_description='Toxygen is powerful Tox client written in Python3',
|
||||
url='https://git.plastiras.org/emdee/toxygen/',
|
||||
keywords='toxygen Tox messenger',
|
||||
author='Ingvar',
|
||||
maintainer='',
|
||||
license='GPL3',
|
||||
packages=get_packages(),
|
||||
install_requires=MODULES,
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
'Programming Language :: Python :: 3.11',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['toxygen=toxygen.main:main']
|
||||
},
|
||||
package_data={"": ["*.ui"],},
|
||||
cmdclass={
|
||||
'install': InstallScript,
|
||||
},
|
||||
zip_safe=False
|
||||
)
|
@ -1,177 +1,18 @@
|
||||
from toxygen.profile import *
|
||||
from toxygen.tox_dns import tox_dns
|
||||
from toxygen.history import History
|
||||
from toxygen.smileys import SmileyLoader
|
||||
from toxygen.messages import *
|
||||
import toxygen.toxes as encr
|
||||
import toxygen.util as util
|
||||
import time
|
||||
from toxygen.middleware.tox_factory import *
|
||||
|
||||
|
||||
# TODO: add new tests
|
||||
|
||||
class TestTox:
|
||||
|
||||
def test_creation(self):
|
||||
name = b'Toxygen User'
|
||||
status_message = b'Toxing on Toxygen'
|
||||
name = 'Toxygen User'
|
||||
status_message = 'Toxing on Toxygen'
|
||||
tox = tox_factory()
|
||||
tox.self_set_name(name)
|
||||
tox.self_set_status_message(status_message)
|
||||
data = tox.get_savedata()
|
||||
del tox
|
||||
tox = tox_factory(data)
|
||||
assert tox.self_get_name() == str(name, 'utf-8')
|
||||
assert tox.self_get_status_message() == str(status_message, 'utf-8')
|
||||
|
||||
|
||||
class TestProfileHelper:
|
||||
|
||||
def test_creation(self):
|
||||
file_name, path = 'test.tox', os.path.dirname(os.path.realpath(__file__)) + '/'
|
||||
data = b'test'
|
||||
with open(path + file_name, 'wb') as fl:
|
||||
fl.write(data)
|
||||
ph = ProfileHelper(path, file_name[:4])
|
||||
assert ProfileHelper.get_path() == path
|
||||
assert ph.open_profile() == data
|
||||
assert os.path.exists(path + 'avatars/')
|
||||
|
||||
|
||||
class TestDNS:
|
||||
|
||||
def test_dns(self):
|
||||
Settings._instance = Settings.get_default_settings()
|
||||
bot_id = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5'
|
||||
tox_id = tox_dns('groupbot@toxme.io')
|
||||
assert tox_id == bot_id
|
||||
|
||||
def test_dns2(self):
|
||||
Settings._instance = Settings.get_default_settings()
|
||||
bot_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
||||
tox_id = tox_dns('echobot@toxme.io')
|
||||
assert tox_id == bot_id
|
||||
|
||||
|
||||
class TestEncryption:
|
||||
|
||||
def test_encr_decr(self):
|
||||
tox = tox_factory()
|
||||
data = tox.get_savedata()
|
||||
lib = encr.ToxES()
|
||||
for password in ('easypassword', 'njvnFjfn7vaGGV6', 'toxygen'):
|
||||
lib.set_password(password)
|
||||
copy_data = data[:]
|
||||
new_data = lib.pass_encrypt(data)
|
||||
assert lib.is_data_encrypted(new_data)
|
||||
new_data = lib.pass_decrypt(new_data)
|
||||
assert copy_data == new_data
|
||||
|
||||
|
||||
class TestSmileys:
|
||||
|
||||
def test_loading(self):
|
||||
settings = {'smiley_pack': 'default', 'smileys': True}
|
||||
sm = SmileyLoader(settings)
|
||||
assert sm.get_smileys_path() is not None
|
||||
l = sm.get_packs_list()
|
||||
assert len(l) == 4
|
||||
|
||||
|
||||
def create_singletons():
|
||||
folder = util.curr_directory() + '/abc'
|
||||
Settings._instance = Settings.get_default_settings()
|
||||
if not os.path.exists(folder):
|
||||
os.makedirs(folder)
|
||||
ProfileHelper(folder, 'test')
|
||||
|
||||
|
||||
def create_friend(name, status_message, number, tox_id):
|
||||
friend = Friend(None, number, name, status_message, None, tox_id)
|
||||
return friend
|
||||
|
||||
|
||||
def create_random_friend():
|
||||
name, status_message, number = 'Friend', 'I am friend!', 0
|
||||
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
||||
friend = create_friend(name, status_message, number, tox_id)
|
||||
return friend
|
||||
|
||||
|
||||
class TestFriend:
|
||||
|
||||
def test_friend_creation(self):
|
||||
create_singletons()
|
||||
name, status_message, number = 'Friend', 'I am friend!', 0
|
||||
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
||||
friend = create_friend(name, status_message, number, tox_id)
|
||||
assert friend.name == name
|
||||
assert friend.tox_id == tox_id
|
||||
assert friend.status_message == status_message
|
||||
assert friend.number == number
|
||||
|
||||
def test_friend_corr(self):
|
||||
create_singletons()
|
||||
friend = create_random_friend()
|
||||
t = time.time()
|
||||
friend.append_message(InfoMessage('Info message', t))
|
||||
friend.append_message(TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t + 0.001, 0))
|
||||
friend.append_message(TextMessage('Hello!', MESSAGE_OWNER['FRIEND'], t + 0.002, 0))
|
||||
assert friend.get_last_message_text() == 'Hello! It is test!'
|
||||
assert len(friend.get_corr()) == 3
|
||||
assert len(friend.get_corr_for_saving()) == 2
|
||||
friend.append_message(TextMessage('Not sent', MESSAGE_OWNER['NOT_SENT'], t + 0.002, 0))
|
||||
arr = friend.get_unsent_messages_for_saving()
|
||||
assert len(arr) == 1
|
||||
assert arr[0][0] == 'Not sent'
|
||||
tm = TransferMessage(MESSAGE_OWNER['FRIEND'],
|
||||
time.time(),
|
||||
TOX_FILE_TRANSFER_STATE['RUNNING'],
|
||||
100, 'file_name', friend.number, 0)
|
||||
friend.append_message(tm)
|
||||
friend.clear_corr()
|
||||
assert len(friend.get_corr()) == 1
|
||||
assert len(friend.get_corr_for_saving()) == 0
|
||||
friend.append_message(TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t, 0))
|
||||
assert len(friend.get_corr()) == 2
|
||||
assert len(friend.get_corr_for_saving()) == 1
|
||||
|
||||
def test_history_search(self):
|
||||
create_singletons()
|
||||
friend = create_random_friend()
|
||||
message = 'Hello! It is test!'
|
||||
friend.append_message(TextMessage(message, MESSAGE_OWNER['ME'], time.time(), 0))
|
||||
last_message = friend.get_last_message_text()
|
||||
assert last_message == message
|
||||
result = friend.search_string('e[m|s]')
|
||||
assert result is not None
|
||||
result = friend.search_string('tox')
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestHistory:
|
||||
|
||||
def test_history(self):
|
||||
create_singletons()
|
||||
db_name = 'my_name'
|
||||
name, status_message, number = 'Friend', 'I am friend!', 0
|
||||
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
||||
friend = create_friend(name, status_message, number, tox_id)
|
||||
history = History(db_name)
|
||||
history.add_friend_to_db(friend.tox_id)
|
||||
assert history.friend_exists_in_db(friend.tox_id)
|
||||
text_message = 'Test!'
|
||||
t = time.time()
|
||||
friend.append_message(TextMessage(text_message, MESSAGE_OWNER['ME'], t, 0))
|
||||
messages = friend.get_corr_for_saving()
|
||||
history.save_messages_to_db(friend.tox_id, messages)
|
||||
getter = history.messages_getter(friend.tox_id)
|
||||
messages = getter.get_all()
|
||||
assert len(messages) == 1
|
||||
assert messages[0][0] == text_message
|
||||
assert messages[0][1] == MESSAGE_OWNER['ME']
|
||||
assert messages[0][-1] == 0
|
||||
history.delete_message(friend.tox_id, t)
|
||||
getter = history.messages_getter(friend.tox_id)
|
||||
messages = getter.get_all()
|
||||
assert len(messages) == 0
|
||||
history.delete_friend_from_db(friend.tox_id)
|
||||
assert not history.friend_exists_in_db(friend.tox_id)
|
||||
assert tox.self_get_name() == name
|
||||
assert tox.self_get_status_message() == status_message
|
||||
|
@ -1,4 +1,4 @@
|
||||
class TestToxygen:
|
||||
|
||||
def test_main(self):
|
||||
import toxygen.main # check for syntax errors
|
||||
import toxygen.__main__ # check for syntax errors
|
||||
|
@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
ROLE=logging
|
||||
/var/local/bin/pydev_pylint.bash -E -f text *py [a-nr-z]*/*py >.pylint.err
|
||||
/var/local/bin/pydev_pylint.bash *py [a-nr-z]*/*py >.pylint.out
|
||||
|
||||
sed -e "/Module 'os' has no/d" \
|
||||
-e "/Undefined variable 'app'/d" \
|
||||
-e '/tests\//d' \
|
||||
-e "/Instance of 'Curl' has no /d" \
|
||||
-e "/No name 'path' in module 'os' /d" \
|
||||
-e "/ in module 'os'/d" \
|
||||
-e "/.bak\//d" \
|
||||
-i .pylint.err .pylint.out
|
@ -0,0 +1,378 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
import warnings
|
||||
import faulthandler
|
||||
|
||||
from gevent import monkey; monkey.patch_all(); del monkey # noqa
|
||||
|
||||
faulthandler.enable()
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
import toxygen_wrapper.tests.support_testing as ts
|
||||
try:
|
||||
from trepan.interfaces import server as Mserver
|
||||
from trepan.api import debug
|
||||
except Exception as e:
|
||||
print('trepan3 TCP server NOT enabled.')
|
||||
else:
|
||||
import signal
|
||||
try:
|
||||
signal.signal(signal.SIGUSR1, ts.trepan_handler)
|
||||
print('trepan3 TCP server enabled on port 6666.')
|
||||
except: pass
|
||||
|
||||
import app
|
||||
from user_data.settings import *
|
||||
from user_data.settings import Settings
|
||||
from user_data import settings
|
||||
import utils.util as util
|
||||
with ts.ignoreStderr():
|
||||
import pyaudio
|
||||
|
||||
__maintainer__ = 'Ingvar'
|
||||
__version__ = '1.0.0' # was 0.5.0+
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__)) # curr dir
|
||||
sys.path.insert(0, os.path.join(path, 'styles'))
|
||||
sys.path.insert(0, os.path.join(path, 'plugins'))
|
||||
# sys.path.insert(0, os.path.join(path, 'third_party'))
|
||||
sys.path.insert(0, path)
|
||||
|
||||
sleep = time.sleep
|
||||
|
||||
os.environ['QT_API'] = os.environ.get('QT_API', 'pyqt5')
|
||||
|
||||
def reset() -> None:
|
||||
Settings.reset_auto_profile()
|
||||
|
||||
def clean() -> None:
|
||||
"""Removes libs folder"""
|
||||
directory = util.get_libs_directory()
|
||||
util.remove(directory)
|
||||
|
||||
def print_toxygen_version() -> None:
|
||||
print('toxygen ' + __version__)
|
||||
|
||||
def setup_default_audio():
|
||||
# need:
|
||||
audio = ts.get_audio()
|
||||
# unfinished
|
||||
global oPYA
|
||||
oPYA = pyaudio.PyAudio()
|
||||
audio['output_devices'] = dict()
|
||||
i = oPYA.get_device_count()
|
||||
while i > 0:
|
||||
i -= 1
|
||||
if oPYA.get_device_info_by_index(i)['maxOutputChannels'] == 0:
|
||||
continue
|
||||
audio['output_devices'][i] = oPYA.get_device_info_by_index(i)['name']
|
||||
i = oPYA.get_device_count()
|
||||
audio['input_devices'] = dict()
|
||||
while i > 0:
|
||||
i -= 1
|
||||
if oPYA.get_device_info_by_index(i)['maxInputChannels'] == 0:
|
||||
continue
|
||||
audio['input_devices'][i] = oPYA.get_device_info_by_index(i)['name']
|
||||
return audio
|
||||
|
||||
def setup_video(oArgs):
|
||||
video = setup_default_video()
|
||||
# this is messed up - no video_input in oArgs
|
||||
# parser.add_argument('--video_input', type=str,)
|
||||
print(video)
|
||||
if not video or not video['output_devices']:
|
||||
video['device'] = -1
|
||||
if not hasattr(oArgs, 'video_input'):
|
||||
video['device'] = video['output_devices'][0]
|
||||
elif oArgs.video_input == '-1':
|
||||
video['device'] = video['output_devices'][-1]
|
||||
else:
|
||||
video['device'] = oArgs.video_input
|
||||
return video
|
||||
|
||||
def setup_audio(oArgs):
|
||||
global oPYA
|
||||
audio = setup_default_audio()
|
||||
for k,v in audio['input_devices'].items():
|
||||
if v == 'default' and 'input' not in audio:
|
||||
audio['input'] = k
|
||||
if v == getattr(oArgs, 'audio_input'):
|
||||
audio['input'] = k
|
||||
LOG.debug(f"Setting audio['input'] {k} = {v} {k}")
|
||||
break
|
||||
for k,v in audio['output_devices'].items():
|
||||
if v == 'default' and 'output' not in audio:
|
||||
audio['output'] = k
|
||||
if v == getattr(oArgs, 'audio_output'):
|
||||
audio['output'] = k
|
||||
LOG.debug(f"Setting audio['output'] {k} = {v} " +str(k))
|
||||
break
|
||||
|
||||
if hasattr(oArgs, 'mode') and getattr(oArgs, 'mode') > 1:
|
||||
audio['enabled'] = True
|
||||
audio['audio_enabled'] = True
|
||||
audio['video_enabled'] = True
|
||||
elif hasattr(oArgs, 'mode') and getattr(oArgs, 'mode') > 0:
|
||||
audio['enabled'] = True
|
||||
audio['audio_enabled'] = False
|
||||
audio['video_enabled'] = True
|
||||
else:
|
||||
audio['enabled'] = False
|
||||
audio['audio_enabled'] = False
|
||||
audio['video_enabled'] = False
|
||||
|
||||
return audio
|
||||
|
||||
i = getattr(oArgs, 'audio_output')
|
||||
if i >= 0:
|
||||
try:
|
||||
elt = oPYA.get_device_info_by_index(i)
|
||||
if i >= 0 and ( 'maxOutputChannels' not in elt or \
|
||||
elt['maxOutputChannels'] == 0):
|
||||
LOG.warn(f"Audio output device has no output channels: {i}")
|
||||
oArgs.audio_output = -1
|
||||
except OSError as e:
|
||||
LOG.warn("Audio output device error looking for maxOutputChannels: " \
|
||||
+str(i) +' ' +str(e))
|
||||
oArgs.audio_output = -1
|
||||
|
||||
if getattr(oArgs, 'audio_output') < 0:
|
||||
LOG.info("Choose an output device:")
|
||||
i = oPYA.get_device_count()
|
||||
while i > 0:
|
||||
i -= 1
|
||||
if oPYA.get_device_info_by_index(i)['maxOutputChannels'] == 0:
|
||||
continue
|
||||
LOG.info(str(i) \
|
||||
+' ' +oPYA.get_device_info_by_index(i)['name'] \
|
||||
+' ' +str(oPYA.get_device_info_by_index(i)['defaultSampleRate'])
|
||||
)
|
||||
return 0
|
||||
|
||||
i = getattr(oArgs, 'audio_input')
|
||||
if i >= 0:
|
||||
try:
|
||||
elt = oPYA.get_device_info_by_index(i)
|
||||
if i >= 0 and ( 'maxInputChannels' not in elt or \
|
||||
elt['maxInputChannels'] == 0):
|
||||
LOG.warn(f"Audio input device has no input channels: {i}")
|
||||
setattr(oArgs, 'audio_input', -1)
|
||||
except OSError as e:
|
||||
LOG.warn("Audio input device error looking for maxInputChannels: " \
|
||||
+str(i) +' ' +str(e))
|
||||
setattr(oArgs, 'audio_input', -1)
|
||||
if getattr(oArgs, 'audio_input') < 0:
|
||||
LOG.info("Choose an input device:")
|
||||
i = oPYA.get_device_count()
|
||||
while i > 0:
|
||||
i -= 1
|
||||
if oPYA.get_device_info_by_index(i)['maxInputChannels'] == 0:
|
||||
continue
|
||||
LOG.info(str(i) \
|
||||
+' ' +oPYA.get_device_info_by_index(i)['name']
|
||||
+' ' +str(oPYA.get_device_info_by_index(i)['defaultSampleRate'])
|
||||
)
|
||||
return 0
|
||||
|
||||
def setup_default_video():
|
||||
default_video = ["-1"]
|
||||
default_video.extend(ts.get_video_indexes())
|
||||
LOG.info(f"Video input choices: {default_video}")
|
||||
video = {'device': -1, 'width': 320, 'height': 240, 'x': 0, 'y': 0}
|
||||
video['output_devices'] = default_video
|
||||
return video
|
||||
|
||||
def main_parser(_=None, iMode=2):
|
||||
if not os.path.exists('/proc/sys/net/ipv6'):
|
||||
bIpV6 = 'False'
|
||||
else:
|
||||
bIpV6 = 'True'
|
||||
lIpV6Choices=[bIpV6, 'False']
|
||||
|
||||
audio = setup_default_audio()
|
||||
default_video = setup_default_video()['output_devices']
|
||||
|
||||
parser = ts.oMainArgparser()
|
||||
parser.add_argument('--version', action='store_true', help='Prints Toxygen version')
|
||||
parser.add_argument('--clean', action='store_true', help='Delete toxcore libs from libs folder')
|
||||
parser.add_argument('--reset', action='store_true', help='Reset default profile')
|
||||
parser.add_argument('--uri', type=str, default='',
|
||||
help='Add specified Tox ID to friends')
|
||||
parser.add_argument('--auto_accept_path', '--auto-accept-path', type=str,
|
||||
default=os.path.join(os.environ['HOME'], 'Downloads'),
|
||||
help="auto_accept_path")
|
||||
# parser.add_argument('--mode', type=int, default=iMode,
|
||||
# help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0')
|
||||
parser.add_argument('--font', type=str, default="Courier",
|
||||
help='Message font')
|
||||
parser.add_argument('--message_font_size', type=int, default=15,
|
||||
help='Font size in pixels')
|
||||
parser.add_argument('--local_discovery_enabled',type=str,
|
||||
default='False', choices=['True','False'],
|
||||
help='Look on the local lan')
|
||||
parser.add_argument('--compact_mode',type=str,
|
||||
default='True', choices=['True','False'],
|
||||
help='Compact mode')
|
||||
parser.add_argument('--allow_inline',type=str,
|
||||
default='False', choices=['True','False'],
|
||||
help='Dis/Enable allow_inline')
|
||||
parser.add_argument('--notifications',type=str,
|
||||
default='True', choices=['True','False'],
|
||||
help='Dis/Enable notifications')
|
||||
parser.add_argument('--sound_notifications',type=str,
|
||||
default='True', choices=['True','False'],
|
||||
help='Enable sound notifications')
|
||||
parser.add_argument('--calls_sound',type=str,
|
||||
default='True', choices=['True','False'],
|
||||
help='Enable calls_sound')
|
||||
parser.add_argument('--core_logging',type=str,
|
||||
default='False', choices=['True','False'],
|
||||
help='Dis/Enable Toxcore notifications')
|
||||
parser.add_argument('--save_history',type=str,
|
||||
default='True', choices=['True','False'],
|
||||
help='En/Disable save history')
|
||||
parser.add_argument('--update', type=int, default=0,
|
||||
choices=[0,0],
|
||||
help='Update program (broken)')
|
||||
parser.add_argument('--video_input', type=str,
|
||||
default=-1,
|
||||
choices=default_video,
|
||||
help="Video input device number - /dev/video?")
|
||||
parser.add_argument('--audio_input', type=str,
|
||||
default=oPYA.get_default_input_device_info()['name'],
|
||||
choices=audio['input_devices'].values(),
|
||||
help="Audio input device name - aplay -L for help")
|
||||
parser.add_argument('--audio_output', type=str,
|
||||
default=oPYA.get_default_output_device_info()['index'],
|
||||
choices=audio['output_devices'].values(),
|
||||
help="Audio output device number - -1 for help")
|
||||
parser.add_argument('--theme', type=str, default='default',
|
||||
choices=['dark', 'default'],
|
||||
help='Theme - style of UI')
|
||||
# parser.add_argument('--sleep', type=str, default='time',
|
||||
# # could expand this to tk, gtk, gevent...
|
||||
# choices=['qt','gevent','time'],
|
||||
# help='Sleep method - one of qt, gevent , time')
|
||||
supported_languages = settings.supported_languages()
|
||||
parser.add_argument('--language', type=str, default='English',
|
||||
choices=supported_languages,
|
||||
help='Languages')
|
||||
parser.add_argument('profile', type=str, nargs='?', default=None,
|
||||
help='Path to Tox profile')
|
||||
return parser
|
||||
|
||||
# clean out the unchanged settings so these can override the profile
|
||||
lKEEP_SETTINGS = ['uri',
|
||||
'profile',
|
||||
'loglevel',
|
||||
'logfile',
|
||||
'mode',
|
||||
|
||||
# dunno
|
||||
'audio_input',
|
||||
'audio_output',
|
||||
'audio',
|
||||
'video',
|
||||
|
||||
'ipv6_enabled',
|
||||
'udp_enabled',
|
||||
'local_discovery_enabled',
|
||||
'trace_enabled',
|
||||
|
||||
'theme',
|
||||
'network',
|
||||
'message_font_size',
|
||||
'font',
|
||||
'save_history',
|
||||
'language',
|
||||
'update',
|
||||
'proxy_host',
|
||||
'proxy_type',
|
||||
'proxy_port',
|
||||
'core_logging',
|
||||
'audio',
|
||||
'video'
|
||||
] # , 'nodes_json'
|
||||
|
||||
class A(): pass
|
||||
|
||||
def main(lArgs=None) -> int:
|
||||
global oPYA
|
||||
from argparse import Namespace
|
||||
if lArgs is None:
|
||||
lArgs = sys.argv[1:]
|
||||
parser = main_parser()
|
||||
default_ns = parser.parse_args([])
|
||||
oArgs = parser.parse_args(lArgs)
|
||||
|
||||
if oArgs.version:
|
||||
print_toxygen_version()
|
||||
return 0
|
||||
|
||||
if oArgs.clean:
|
||||
clean()
|
||||
return 0
|
||||
|
||||
if oArgs.reset:
|
||||
reset()
|
||||
return 0
|
||||
|
||||
# if getattr(oArgs, 'network') in ['newlocal', 'localnew']: oArgs.network = 'new'
|
||||
|
||||
# clean out the unchanged settings so these can override the profile
|
||||
for key in default_ns.__dict__.keys():
|
||||
if key in lKEEP_SETTINGS: continue
|
||||
if not hasattr(oArgs, key): continue
|
||||
if getattr(default_ns, key) == getattr(oArgs, key):
|
||||
delattr(oArgs, key)
|
||||
|
||||
ts.clean_booleans(oArgs)
|
||||
|
||||
aArgs = A()
|
||||
for key in oArgs.__dict__.keys():
|
||||
setattr(aArgs, key, getattr(oArgs, key))
|
||||
|
||||
#setattr(aArgs, 'video', setup_video(oArgs))
|
||||
aArgs.video = setup_video(oArgs)
|
||||
assert 'video' in aArgs.__dict__
|
||||
|
||||
#setattr(aArgs, 'audio', setup_audio(oArgs))
|
||||
aArgs.audio = setup_audio(oArgs)
|
||||
assert 'audio' in aArgs.__dict__
|
||||
oArgs = aArgs
|
||||
|
||||
oApp = app.App(__version__, oArgs)
|
||||
# for pyqtconsole
|
||||
try:
|
||||
setattr(__builtins__, 'app', oApp)
|
||||
except Exception as e:
|
||||
pass
|
||||
i = oApp.iMain()
|
||||
return i
|
||||
|
||||
if __name__ == '__main__':
|
||||
iRet = 0
|
||||
try:
|
||||
iRet = main(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
iRet = 0
|
||||
except SystemExit as e:
|
||||
iRet = e
|
||||
except Exception as e:
|
||||
import traceback
|
||||
sys.stderr.write(f"Exception from main {e}" \
|
||||
+'\n' + traceback.format_exc() +'\n' )
|
||||
iRet = 1
|
||||
|
||||
# Exception ignored in: <module 'threading' from '/usr/lib/python3.9/threading.py'>
|
||||
# File "/usr/lib/python3.9/threading.py", line 1428, in _shutdown
|
||||
# lock.acquire()
|
||||
# gevent.exceptions.LoopExit as e:
|
||||
# This operation would block forever
|
||||
sys.stderr.write('Calling sys.exit' +'\n')
|
||||
with ts.ignoreStdout():
|
||||
sys.exit(iRet)
|
@ -0,0 +1,54 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class Call:
|
||||
|
||||
def __init__(self, out_audio, out_video, in_audio=False, in_video=False):
|
||||
self._in_audio = in_audio
|
||||
self._in_video = in_video
|
||||
self._out_audio = out_audio
|
||||
self._out_video = out_video
|
||||
self._is_active = False
|
||||
|
||||
def get_is_active(self):
|
||||
return self._is_active
|
||||
|
||||
def set_is_active(self, value):
|
||||
self._is_active = value
|
||||
|
||||
is_active = property(get_is_active, set_is_active)
|
||||
|
||||
# Audio
|
||||
|
||||
def get_in_audio(self):
|
||||
return self._in_audio
|
||||
|
||||
def set_in_audio(self, value):
|
||||
self._in_audio = value
|
||||
|
||||
in_audio = property(get_in_audio, set_in_audio)
|
||||
|
||||
def get_out_audio(self):
|
||||
return self._out_audio
|
||||
|
||||
def set_out_audio(self, value):
|
||||
self._out_audio = value
|
||||
|
||||
out_audio = property(get_out_audio, set_out_audio)
|
||||
|
||||
# Video
|
||||
|
||||
def get_in_video(self):
|
||||
return self._in_video
|
||||
|
||||
def set_in_video(self, value):
|
||||
self._in_video = value
|
||||
|
||||
in_video = property(get_in_video, set_in_video)
|
||||
|
||||
def get_out_video(self):
|
||||
return self._out_video
|
||||
|
||||
def set_out_video(self, value):
|
||||
self._out_video = value
|
||||
|
||||
out_video = property(get_out_video, set_out_video)
|
@ -0,0 +1,587 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
import itertools
|
||||
|
||||
from toxygen_wrapper.toxav_enums import *
|
||||
from toxygen_wrapper.tests import support_testing as ts
|
||||
from toxygen_wrapper.tests.support_testing import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
|
||||
|
||||
with ts.ignoreStderr():
|
||||
import pyaudio
|
||||
from av import screen_sharing
|
||||
from av.call import Call
|
||||
import common.tox_save
|
||||
from middleware.threads import BaseQThread
|
||||
|
||||
from utils import ui as util_ui
|
||||
from middleware.threads import invoke_in_main_thread
|
||||
# from middleware.threads import BaseThread
|
||||
|
||||
sleep = time.sleep
|
||||
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+__name__)
|
||||
|
||||
TIMER_TIMEOUT = 30.0
|
||||
iFPS = 25
|
||||
|
||||
class AudioThread(BaseQThread):
|
||||
def __init__(self, av, name=''):
|
||||
super().__init__()
|
||||
self.av = av
|
||||
self._name = name
|
||||
|
||||
def join(self, ito=ts.iTHREAD_TIMEOUT):
|
||||
LOG_DEBUG(f"AudioThread join {self}")
|
||||
# dunno
|
||||
|
||||
def run(self) -> None:
|
||||
LOG_DEBUG('AudioThread run: ')
|
||||
# maybe not needed
|
||||
while not self._stop_thread:
|
||||
self.av.send_audio()
|
||||
sleep(100.0 / 1000.0)
|
||||
|
||||
class VideoThread(BaseQThread):
|
||||
def __init__(self, av, name=''):
|
||||
super().__init__()
|
||||
self.av = av
|
||||
self._name = name
|
||||
|
||||
def join(self, ito=ts.iTHREAD_TIMEOUT):
|
||||
LOG_DEBUG(f"VideoThread join {self}")
|
||||
# dunno
|
||||
|
||||
def run(self) -> None:
|
||||
LOG_DEBUG('VideoThread run: ')
|
||||
# maybe not needed
|
||||
while not self._stop_thread:
|
||||
self.av.send_video()
|
||||
sleep(100.0 / 1000.0)
|
||||
|
||||
class AV(common.tox_save.ToxAvSave):
|
||||
|
||||
def __init__(self, toxav, settings):
|
||||
super().__init__(toxav)
|
||||
self._toxav = toxav
|
||||
self._settings = settings
|
||||
self._running = True
|
||||
s = settings
|
||||
if 'video' not in s:
|
||||
LOG.warn("AV.__init__ 'video' not in s" )
|
||||
LOG.debug(f"AV.__init__ {s}" )
|
||||
elif 'device' not in s['video']:
|
||||
LOG.warn("AV.__init__ 'device' not in s.video" )
|
||||
LOG.debug(f"AV.__init__ {s['video']}" )
|
||||
|
||||
self._calls = {} # dict: key - friend number, value - Call instance
|
||||
|
||||
self._audio = None
|
||||
self._audio_stream = None
|
||||
self._audio_thread = None
|
||||
self._audio_running = False
|
||||
self._out_stream = None
|
||||
|
||||
self._audio_channels = 1
|
||||
self._audio_duration = 60
|
||||
self._audio_rate_pa = 48000
|
||||
self._audio_rate_tox = 48000
|
||||
self._audio_rate_pa = 48000
|
||||
self._audio_krate_tox_audio = self._audio_rate_tox // 1000
|
||||
self._audio_krate_tox_video = 5000
|
||||
self._audio_sample_count_pa = self._audio_rate_pa * self._audio_channels * self._audio_duration // 1000
|
||||
self._audio_sample_count_tox = self._audio_rate_tox * self._audio_channels * self._audio_duration // 1000
|
||||
|
||||
self._video = None
|
||||
self._video_thread = None
|
||||
self._video_running = None
|
||||
|
||||
self._video_width = 320
|
||||
self._video_height = 240
|
||||
|
||||
# was iOutput = self._settings._args.audio['output']
|
||||
iInput = self._settings['audio']['input']
|
||||
self.lPaSampleratesI = ts.lSdSamplerates(iInput)
|
||||
iOutput = self._settings['audio']['output']
|
||||
self.lPaSampleratesO = ts.lSdSamplerates(iOutput)
|
||||
|
||||
global oPYA
|
||||
oPYA = self._audio = pyaudio.PyAudio()
|
||||
|
||||
def stop(self) -> None:
|
||||
LOG_DEBUG(f"AV.CA stop {self._video_thread}")
|
||||
self._running = False
|
||||
self.stop_audio_thread()
|
||||
self.stop_video_thread()
|
||||
|
||||
def __contains__(self, friend_number:int) -> bool:
|
||||
return friend_number in self._calls
|
||||
|
||||
# Calls
|
||||
|
||||
def __call__(self, friend_number, audio, video):
|
||||
"""Call friend with specified number"""
|
||||
if friend_number in self._calls:
|
||||
LOG.warn(f"__call__ already has {friend_number}")
|
||||
return
|
||||
if self._audio_krate_tox_audio not in ts.lToxSampleratesK:
|
||||
LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}")
|
||||
|
||||
try:
|
||||
self._toxav.call(friend_number,
|
||||
self._audio_krate_tox_audio if audio else 0,
|
||||
self._audio_krate_tox_video if video else 0)
|
||||
except Exception as e:
|
||||
LOG.warn(f"_toxav.call already has {friend_number}")
|
||||
return
|
||||
self._calls[friend_number] = Call(audio, video)
|
||||
threading.Timer(TIMER_TIMEOUT,
|
||||
lambda: self.finish_not_started_call(friend_number)).start()
|
||||
|
||||
def accept_call(self, friend_number, audio_enabled, video_enabled):
|
||||
# obsolete
|
||||
self.call_accept_call(friend_number, audio_enabled, video_enabled)
|
||||
|
||||
def call_accept_call(self, friend_number, audio_enabled, video_enabled) -> None:
|
||||
# called from CM.accept_call in a try:
|
||||
LOG.debug(f"call_accept_call from F={friend_number} R={self._running}" +
|
||||
f" A={audio_enabled} V={video_enabled}")
|
||||
# import pdb; pdb.set_trace() - gets into q Qt exec_ problem
|
||||
# ts.trepan_handler()
|
||||
|
||||
if self._audio_krate_tox_audio not in ts.lToxSampleratesK:
|
||||
LOG.warn(f"__call__ {self._audio_krate_tox_audio} not in {ts.lToxSampleratesK}")
|
||||
if self._running:
|
||||
self._calls[friend_number] = Call(audio_enabled, video_enabled)
|
||||
# audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending.
|
||||
# video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending.
|
||||
try:
|
||||
self._toxav.answer(friend_number,
|
||||
self._audio_krate_tox_audio if audio_enabled else 0,
|
||||
self._audio_krate_tox_video if video_enabled else 0)
|
||||
except Exception as e:
|
||||
LOG.error(f"AV accept_call error from {friend_number} {self._running} {e}")
|
||||
raise
|
||||
if video_enabled:
|
||||
# may raise
|
||||
self.start_video_thread()
|
||||
if audio_enabled:
|
||||
LOG.debug(f"calls accept_call calling start_audio_thread F={friend_number}")
|
||||
# may raise
|
||||
self.start_audio_thread()
|
||||
|
||||
def finish_call(self, friend_number, by_friend=False) -> None:
|
||||
LOG.debug(f"finish_call {friend_number}")
|
||||
if friend_number in self._calls:
|
||||
del self._calls[friend_number]
|
||||
try:
|
||||
# AttributeError: 'int' object has no attribute 'out_audio'
|
||||
if not len(list(filter(lambda c: c.out_audio, self._calls))):
|
||||
self.stop_audio_thread()
|
||||
if not len(list(filter(lambda c: c.out_video, self._calls))):
|
||||
self.stop_video_thread()
|
||||
except Exception as e:
|
||||
LOG.error(f"finish_call FixMe: {e}")
|
||||
# dunno
|
||||
self.stop_audio_thread()
|
||||
self.stop_video_thread()
|
||||
if not by_friend:
|
||||
LOG.debug(f"finish_call before call_control {friend_number}")
|
||||
self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL'])
|
||||
LOG.debug(f"finish_call after call_control {friend_number}")
|
||||
|
||||
def finish_not_started_call(self, friend_number:int) -> None:
|
||||
if friend_number in self:
|
||||
call = self._calls[friend_number]
|
||||
if not call.is_active:
|
||||
self.finish_call(friend_number)
|
||||
|
||||
def toxav_call_state_cb(self, friend_number, state) -> None:
|
||||
"""
|
||||
New call state
|
||||
"""
|
||||
LOG.debug(f"toxav_call_state_cb {friend_number}")
|
||||
call = self._calls[friend_number]
|
||||
call.is_active = True
|
||||
|
||||
call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A'] > 0
|
||||
call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V'] > 0
|
||||
|
||||
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio:
|
||||
self.start_audio_thread()
|
||||
|
||||
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video:
|
||||
self.start_video_thread()
|
||||
|
||||
def is_video_call(self, number) -> bool:
|
||||
return number in self and self._calls[number].in_video
|
||||
|
||||
# Threads
|
||||
|
||||
def start_audio_thread(self, bSTREAM_CALLBACK=False) -> None:
|
||||
"""
|
||||
Start audio sending
|
||||
from a callback
|
||||
"""
|
||||
# called from call_accept_call in an try: from CM.accept_call
|
||||
global oPYA
|
||||
# was iInput = self._settings._args.audio['input']
|
||||
iInput = self._settings['audio']['input']
|
||||
if self._audio_thread is not None:
|
||||
LOG_WARN(f"start_audio_thread device={iInput}")
|
||||
return
|
||||
LOG_DEBUG(f"start_audio_thread device={iInput}")
|
||||
lPaSamplerates = ts.lSdSamplerates(iInput)
|
||||
if not(len(lPaSamplerates)):
|
||||
e = f"No sample rates for device: audio[input]={iInput}"
|
||||
LOG_WARN(f"start_audio_thread {e}")
|
||||
#?? dunno - cancel call? - no let the user do it
|
||||
# return
|
||||
# just guessing here in case that's a false negative
|
||||
lPaSamplerates = [round(oPYA.get_device_info_by_index(iInput)['defaultSampleRate'])]
|
||||
if lPaSamplerates and self._audio_rate_pa in lPaSamplerates:
|
||||
pass
|
||||
elif lPaSamplerates:
|
||||
LOG_WARN(f"Setting audio_rate to: {lPaSamplerates[0]}")
|
||||
self._audio_rate_pa = lPaSamplerates[0]
|
||||
elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iInput):
|
||||
self._audio_rate_pa = oPYA.get_device_info_by_index(iInput)['defaultSampleRate']
|
||||
LOG_WARN(f"setting to defaultSampleRate")
|
||||
else:
|
||||
LOG_WARN(f"{self._audio_rate_pa} not in {lPaSamplerates}")
|
||||
# a float is in here - must it be int?
|
||||
if type(self._audio_rate_pa) == float:
|
||||
self._audio_rate_pa = round(self._audio_rate_pa)
|
||||
try:
|
||||
if self._audio_rate_pa not in lPaSamplerates:
|
||||
LOG_WARN(f"PAudio sampling rate was {self._audio_rate_pa} changed to {lPaSamplerates[0]}")
|
||||
LOG_DEBUG(f"lPaSamplerates={lPaSamplerates}")
|
||||
self._audio_rate_pa = lPaSamplerates[0]
|
||||
else:
|
||||
LOG_DEBUG( f"start_audio_thread framerate: {self._audio_rate_pa}" \
|
||||
+f" device: {iInput}"
|
||||
+f" supported: {lPaSamplerates}")
|
||||
|
||||
if bSTREAM_CALLBACK:
|
||||
# why would you not call a thread?
|
||||
self._audio_stream = oPYA.open(format=pyaudio.paInt16,
|
||||
rate=self._audio_rate_pa,
|
||||
channels=self._audio_channels,
|
||||
input=True,
|
||||
input_device_index=iInput,
|
||||
frames_per_buffer=self._audio_sample_count_pa * 10,
|
||||
stream_callback=self.send_audio_data)
|
||||
self._audio_running = True
|
||||
self._audio_stream.start_stream()
|
||||
while self._audio_stream.is_active():
|
||||
sleep(0.1)
|
||||
self._audio_stream.stop_stream()
|
||||
self._audio_stream.close()
|
||||
else:
|
||||
LOG_DEBUG( f"start_audio_thread starting thread {self._audio_rate_pa}")
|
||||
self._audio_stream = oPYA.open(format=pyaudio.paInt16,
|
||||
rate=self._audio_rate_pa,
|
||||
channels=self._audio_channels,
|
||||
input=True,
|
||||
input_device_index=iInput,
|
||||
frames_per_buffer=self._audio_sample_count_pa * 10)
|
||||
self._audio_running = True
|
||||
self._audio_thread = AudioThread(self,
|
||||
name='_audio_thread')
|
||||
self._audio_thread.start()
|
||||
LOG_DEBUG( f"start_audio_thread started thread name='_audio_thread'")
|
||||
|
||||
except Exception as e:
|
||||
LOG_ERROR(f"Starting self._audio.open {e}")
|
||||
LOG_DEBUG(repr(dict(format=pyaudio.paInt16,
|
||||
rate=self._audio_rate_pa,
|
||||
channels=self._audio_channels,
|
||||
input=True,
|
||||
input_device_index=iInput,
|
||||
frames_per_buffer=self._audio_sample_count_pa * 10)))
|
||||
# catcher in place in calls_manager? yes accept_call
|
||||
# calls_manager._call.toxav_call_state_cb(friend_number, mask)
|
||||
invoke_in_main_thread(util_ui.message_box,
|
||||
str(e),
|
||||
util_ui.tr("Starting self._audio.open"))
|
||||
return
|
||||
else:
|
||||
LOG_DEBUG(f"start_audio_thread {self._audio_stream}")
|
||||
|
||||
def stop_audio_thread(self) -> None:
|
||||
LOG_DEBUG(f"stop_audio_thread {self._audio_stream}")
|
||||
|
||||
if self._audio_thread is None:
|
||||
return
|
||||
self._audio_running = False
|
||||
self._audio_thread._stop_thread = True
|
||||
|
||||
self._audio_thread = None
|
||||
self._audio_stream = None
|
||||
self._audio = None
|
||||
|
||||
if self._out_stream is not None:
|
||||
self._out_stream.stop_stream()
|
||||
self._out_stream.close()
|
||||
self._out_stream = None
|
||||
|
||||
def start_video_thread(self) -> None:
|
||||
if self._video_thread is not None:
|
||||
return
|
||||
s = self._settings
|
||||
if 'video' not in s:
|
||||
LOG.warn("AV.__init__ 'video' not in s" )
|
||||
LOG.debug(f"start_video_thread {s}" )
|
||||
raise RuntimeError("start_video_thread not 'video' in s)" )
|
||||
if 'device' not in s['video']:
|
||||
LOG.error("start_video_thread not 'device' in s['video']" )
|
||||
LOG.debug(f"start_video_thread {s['video']}" )
|
||||
raise RuntimeError("start_video_thread not 'device' ins s['video']" )
|
||||
self._video_width = s['video']['width']
|
||||
self._video_height = s['video']['height']
|
||||
|
||||
# dunno
|
||||
if s['video']['device'] == -1:
|
||||
self._video = screen_sharing.DesktopGrabber(s['video']['x'],
|
||||
s['video']['y'],
|
||||
s['video']['width'],
|
||||
s['video']['height'])
|
||||
else:
|
||||
with ts.ignoreStdout(): import cv2
|
||||
if s['video']['device'] == 0:
|
||||
# webcam
|
||||
self._video = cv2.VideoCapture(s['video']['device'], cv2.DSHOW)
|
||||
else:
|
||||
self._video = cv2.VideoCapture(s['video']['device'])
|
||||
self._video.set(cv2.CAP_PROP_FPS, iFPS)
|
||||
self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width)
|
||||
self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height)
|
||||
# self._video.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
|
||||
if self._video is None:
|
||||
LOG.error("start_video_thread " \
|
||||
+f" device: {s['video']['device']}" \
|
||||
+f" supported: {s['video']['width']} {s['video']['height']}")
|
||||
return
|
||||
LOG.info("start_video_thread " \
|
||||
+f" device: {s['video']['device']}" \
|
||||
+f" supported: {s['video']['width']} {s['video']['height']}")
|
||||
|
||||
self._video_running = True
|
||||
self._video_thread = VideoThread(self,
|
||||
name='_video_thread')
|
||||
self._video_thread.start()
|
||||
|
||||
def stop_video_thread(self) -> None:
|
||||
LOG_DEBUG(f"stop_video_thread {self._video_thread}")
|
||||
if self._video_thread is None:
|
||||
return
|
||||
|
||||
self._video_thread._stop_thread = True
|
||||
self._video_running = False
|
||||
i = 0
|
||||
while i < ts.iTHREAD_JOINS:
|
||||
self._video_thread.join(ts.iTHREAD_TIMEOUT)
|
||||
try:
|
||||
if not self._video_thread.is_alive(): break
|
||||
except:
|
||||
break
|
||||
i = i + 1
|
||||
else:
|
||||
LOG.warn("self._video_thread.is_alive BLOCKED")
|
||||
self._video_thread = None
|
||||
self._video = None
|
||||
# Incoming chunks
|
||||
|
||||
def audio_chunk(self, samples, channels_count, rate) -> None:
|
||||
"""
|
||||
Incoming chunk
|
||||
"""
|
||||
# from callback
|
||||
if self._out_stream is None:
|
||||
# was iOutput = self._settings._args.audio['output']
|
||||
iOutput = self._settings['audio']['output']
|
||||
if self.lPaSampleratesO and rate in self.lPaSampleratesO:
|
||||
LOG_DEBUG(f"Using rate {rate} in self.lPaSampleratesO")
|
||||
elif self.lPaSampleratesO:
|
||||
LOG_WARN(f"{rate} not in {self.lPaSampleratesO}")
|
||||
LOG_WARN(f"Setting audio_rate to: {self.lPaSampleratesO[0]}")
|
||||
rate = self.lPaSampleratesO[0]
|
||||
elif 'defaultSampleRate' in oPYA.get_device_info_by_index(iOutput):
|
||||
rate = round(oPYA.get_device_info_by_index(iOutput)['defaultSampleRate'])
|
||||
LOG_WARN(f"Setting rate to {rate} empty self.lPaSampleratesO")
|
||||
else:
|
||||
LOG_WARN(f"Using rate {rate} empty self.lPaSampleratesO")
|
||||
if type(rate) == float:
|
||||
rate = round(rate)
|
||||
# test output device?
|
||||
# [Errno -9985] Device unavailable
|
||||
try:
|
||||
with ts.ignoreStderr():
|
||||
self._out_stream = oPYA.open(format=pyaudio.paInt16,
|
||||
channels=channels_count,
|
||||
rate=rate,
|
||||
output_device_index=iOutput,
|
||||
output=True)
|
||||
except Exception as e:
|
||||
LOG_ERROR(f"Error playing audio_chunk creating self._out_stream output_device_index={iOutput} {e}")
|
||||
invoke_in_main_thread(util_ui.message_box,
|
||||
str(e),
|
||||
util_ui.tr("Error Chunking audio"))
|
||||
# dunno
|
||||
self.stop()
|
||||
return
|
||||
|
||||
iOutput = self._settings['audio']['output']
|
||||
#trace LOG_DEBUG(f"audio_chunk output_device_index={iOutput} rate={rate} channels={channels_count}")
|
||||
try:
|
||||
self._out_stream.write(samples)
|
||||
except Exception as e:
|
||||
# OSError: [Errno -9999] Unanticipated host error
|
||||
LOG_WARN(f"audio_chunk output_device_index={iOutput} {e}")
|
||||
|
||||
# AV sending
|
||||
|
||||
def send_audio_data(self, data, count, *largs, **kwargs) -> None:
|
||||
# callback
|
||||
pcm = data
|
||||
# :param sampling_rate: Audio sampling rate used in this frame.
|
||||
try:
|
||||
if self._toxav is None:
|
||||
LOG_ERROR("_toxav not initialized")
|
||||
return
|
||||
if self._audio_rate_tox not in ts.lToxSamplerates:
|
||||
LOG_WARN(f"ToxAudio sampling rate was {self._audio_rate_tox} changed to {ts.lToxSamplerates[0]}")
|
||||
self._audio_rate_tox = ts.lToxSamplerates[0]
|
||||
|
||||
for friend_num in self._calls:
|
||||
if self._calls[friend_num].out_audio:
|
||||
# app.av.calls ERROR Error send_audio audio_send_frame: This client is currently not in a call with the friend.
|
||||
self._toxav.audio_send_frame(friend_num,
|
||||
pcm,
|
||||
count,
|
||||
self._audio_channels,
|
||||
self._audio_rate_tox)
|
||||
|
||||
except Exception as e:
|
||||
LOG.error(f"Error send_audio_data audio_send_frame: {e}")
|
||||
LOG.debug(f"send_audio_data self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}")
|
||||
self.stop_audio_thread()
|
||||
invoke_in_main_thread(util_ui.message_box,
|
||||
str(e),
|
||||
util_ui.tr("Error send_audio_data audio_send_frame"))
|
||||
#? stop ? endcall?
|
||||
|
||||
def send_audio(self) -> None:
|
||||
"""
|
||||
This method sends audio to friends
|
||||
"""
|
||||
i=0
|
||||
count = self._audio_sample_count_tox
|
||||
LOG_DEBUG(f"send_audio stream={self._audio_stream}")
|
||||
while self._audio_running:
|
||||
try:
|
||||
pcm = self._audio_stream.read(count, exception_on_overflow=False)
|
||||
if not pcm:
|
||||
sleep(0.1)
|
||||
else:
|
||||
self.send_audio_data(pcm, count)
|
||||
except:
|
||||
LOG_DEBUG(f"error send_audio {i}")
|
||||
else:
|
||||
LOG_TRACE(f"send_audio {i}")
|
||||
i += 1
|
||||
sleep(0.01)
|
||||
|
||||
def send_video(self) -> None:
|
||||
"""
|
||||
This method sends video to friends
|
||||
"""
|
||||
# LOG_DEBUG(f"send_video thread={threading.current_thread().name}"
|
||||
# +f" self._video_running={self._video_running}"
|
||||
# +f" device: {self._settings['video']['device']}" )
|
||||
while self._video_running:
|
||||
try:
|
||||
result, frame = self._video.read()
|
||||
if not result:
|
||||
LOG_WARN(f"send_video video_send_frame _video.read result={result}")
|
||||
break
|
||||
if frame is None:
|
||||
LOG_WARN(f"send_video video_send_frame _video.read result={result} frame={frame}")
|
||||
continue
|
||||
|
||||
LOG_TRACE(f"send_video video_send_frame _video.read result={result}")
|
||||
height, width, channels = frame.shape
|
||||
friends = []
|
||||
for friend_num in self._calls:
|
||||
if self._calls[friend_num].out_video:
|
||||
friends.append(friend_num)
|
||||
if len(friends) == 0:
|
||||
LOG_WARN(f"send_video video_send_frame no friends")
|
||||
else:
|
||||
LOG_TRACE(f"send_video video_send_frame {friends}")
|
||||
friend_num = friends[0]
|
||||
try:
|
||||
y, u, v = self.convert_bgr_to_yuv(frame)
|
||||
self._toxav.video_send_frame(friend_num, width, height, y, u, v)
|
||||
except Exception as e:
|
||||
LOG_WARN(f"send_video video_send_frame ERROR {e}")
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
LOG_ERROR(f"send_video video_send_frame {e}")
|
||||
pass
|
||||
|
||||
sleep( 1.0/iFPS)
|
||||
|
||||
def convert_bgr_to_yuv(self, frame) -> tuple:
|
||||
"""
|
||||
:param frame: input bgr frame
|
||||
:return y, u, v: y, u, v values of frame
|
||||
|
||||
How this function works:
|
||||
OpenCV creates YUV420 frame from BGR
|
||||
This frame has following structure and size:
|
||||
width, height - dim of input frame
|
||||
width, height * 1.5 - dim of output frame
|
||||
|
||||
width
|
||||
-------------------------
|
||||
| |
|
||||
| Y | height
|
||||
| |
|
||||
-------------------------
|
||||
| | |
|
||||
| U even | U odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
| | |
|
||||
| V even | V odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
|
||||
width // 2 width // 2
|
||||
|
||||
Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable()
|
||||
Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes
|
||||
"""
|
||||
with ts.ignoreStdout():
|
||||
import cv2
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420)
|
||||
|
||||
y = frame[:self._video_height, :]
|
||||
y = list(itertools.chain.from_iterable(y))
|
||||
|
||||
import numpy as np
|
||||
# was np.int
|
||||
u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32)
|
||||
u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2]
|
||||
u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:]
|
||||
u = list(itertools.chain.from_iterable(u))
|
||||
v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int32)
|
||||
v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2]
|
||||
v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:]
|
||||
v = list(itertools.chain.from_iterable(v))
|
||||
|
||||
return bytes(y), bytes(u), bytes(v)
|
@ -0,0 +1,184 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import logging
|
||||
|
||||
from qtpy import QtCore
|
||||
|
||||
import av.calls
|
||||
from messenger.messages import *
|
||||
from ui import av_widgets
|
||||
import common.event as event
|
||||
import utils.ui as util_ui
|
||||
from toxygen_wrapper.tests import support_testing as ts
|
||||
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+__name__)
|
||||
|
||||
class CallsManager:
|
||||
|
||||
def __init__(self, toxav, settings, main_screen, contacts_manager, app=None):
|
||||
self._callav = av.calls.AV(toxav, settings) # object with data about calls
|
||||
self._call = self._callav
|
||||
self._call_widgets = {} # dict of incoming call widgets
|
||||
self._incoming_calls = set()
|
||||
self._settings = settings
|
||||
self._main_screen = main_screen
|
||||
self._contacts_manager = contacts_manager
|
||||
self._call_started_event = event.Event() # friend_number, audio, video, is_outgoing
|
||||
self._call_finished_event = event.Event() # friend_number, is_declined
|
||||
self._app = app
|
||||
|
||||
def set_toxav(self, toxav) -> None:
|
||||
self._callav.set_toxav(toxav)
|
||||
|
||||
# Events
|
||||
|
||||
def get_call_started_event(self):
|
||||
return self._call_started_event
|
||||
|
||||
call_started_event = property(get_call_started_event)
|
||||
|
||||
def get_call_finished_event(self):
|
||||
return self._call_finished_event
|
||||
|
||||
call_finished_event = property(get_call_finished_event)
|
||||
|
||||
# AV support
|
||||
|
||||
def call_click(self, audio=True, video=False) -> None:
|
||||
"""User clicked audio button in main window"""
|
||||
num = self._contacts_manager.get_active_number()
|
||||
if not self._contacts_manager.is_active_a_friend():
|
||||
return
|
||||
if num not in self._callav and self._contacts_manager.is_active_online(): # start call
|
||||
if not self._settings['audio']['enabled']:
|
||||
return
|
||||
self._callav(num, audio, video)
|
||||
self._main_screen.active_call()
|
||||
self._call_started_event(num, audio, video, True)
|
||||
elif num in self._callav: # finish or cancel call if you call with active friend
|
||||
self.stop_call(num, False)
|
||||
|
||||
def incoming_call(self, audio, video, friend_number) -> None:
|
||||
"""
|
||||
Incoming call from friend.
|
||||
"""
|
||||
LOG.debug(f"CM incoming_call {friend_number}")
|
||||
# if not self._settings['audio']['enabled']: return
|
||||
friend = self._contacts_manager.get_friend_by_number(friend_number)
|
||||
self._call_started_event(friend_number, audio, video, False)
|
||||
self._incoming_calls.add(friend_number)
|
||||
if friend_number == self._contacts_manager.get_active_number():
|
||||
self._main_screen.incoming_call()
|
||||
else:
|
||||
friend.actions = True
|
||||
text = util_ui.tr("Incoming video call") if video else util_ui.tr("Incoming audio call")
|
||||
self._call_widgets[friend_number] = self._get_incoming_call_widget(friend_number, text, friend.name)
|
||||
self._call_widgets[friend_number].set_pixmap(friend.get_pixmap())
|
||||
self._call_widgets[friend_number].show()
|
||||
|
||||
def accept_call(self, friend_number, audio, video) -> None:
|
||||
"""
|
||||
Accept incoming call with audio or video
|
||||
Called from a thread
|
||||
"""
|
||||
|
||||
LOG.debug(f"CM accept_call from friend_number={friend_number} {audio} {video}")
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
self._main_screen.active_call()
|
||||
# failsafe added somewhere this was being left up
|
||||
self.close_call(friend_number)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
self._callav.call_accept_call(friend_number, audio, video)
|
||||
LOG.debug(f"accept_call _call.accept_call CALLED f={friend_number}")
|
||||
except Exception as e:
|
||||
#
|
||||
LOG.error(f"accept_call _call.accept_call ERROR for {friend_number} {e}")
|
||||
LOG.debug(traceback.print_exc())
|
||||
self._main_screen.call_finished()
|
||||
if hasattr(self._main_screen, '_settings') and \
|
||||
'audio' in self._main_screen._settings and \
|
||||
'input' in self._main_screen._settings['audio']:
|
||||
iInput = self._settings['audio']['input']
|
||||
iOutput = self._settings['audio']['output']
|
||||
iVideo = self._settings['video']['device']
|
||||
LOG.debug(f"iInput={iInput} iOutput={iOutput} iVideo={iVideo}")
|
||||
elif hasattr(self._main_screen, '_settings') and \
|
||||
hasattr(self._main_screen._settings, 'audio') and \
|
||||
'input' not in self._main_screen._settings['audio']:
|
||||
LOG.warn(f"'audio' not in {self._main_screen._settings}")
|
||||
elif hasattr(self._main_screen, '_settings') and \
|
||||
hasattr(self._main_screen._settings, 'audio') and \
|
||||
'input' not in self._main_screen._settings['audio']:
|
||||
LOG.warn(f"'audio' not in {self._main_screen._settings}")
|
||||
else:
|
||||
LOG.warn(f"_settings not in self._main_screen")
|
||||
util_ui.message_box(str(e),
|
||||
util_ui.tr('ERROR Accepting call from {friend_number}'))
|
||||
finally:
|
||||
# does not terminate call - just the av_widget
|
||||
LOG.debug(f"CM.accept_call close av_widget")
|
||||
self.close_call(friend_number)
|
||||
LOG.debug(f" closed self._call_widgets[{friend_number}]")
|
||||
|
||||
def close_call(self, friend_number:int) -> None:
|
||||
# refactored out from above because the accept window not getting
|
||||
# taken down in some accept audio calls
|
||||
LOG.debug(f"close_call {friend_number}")
|
||||
try:
|
||||
if friend_number in self._call_widgets:
|
||||
self._call_widgets[friend_number].close()
|
||||
del self._call_widgets[friend_number]
|
||||
if friend_number in self._incoming_calls:
|
||||
self._incoming_calls.remove(friend_number)
|
||||
except Exception as e:
|
||||
# RuntimeError: wrapped C/C++ object of type IncomingCallWidget has been deleted
|
||||
|
||||
LOG.warn(f" closed self._call_widgets[{friend_number}] {e}")
|
||||
# invoke_in_main_thread(QtCore.QCoreApplication.processEvents)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
|
||||
def stop_call(self, friend_number, by_friend) -> None:
|
||||
"""
|
||||
Stop call with friend
|
||||
"""
|
||||
LOG.debug(f"CM.stop_call friend={friend_number}")
|
||||
if friend_number in self._incoming_calls:
|
||||
self._incoming_calls.remove(friend_number)
|
||||
is_declined = True
|
||||
else:
|
||||
is_declined = False
|
||||
if friend_number in self._call_widgets:
|
||||
LOG.debug(f"CM.stop_call _call_widgets close")
|
||||
self.close_call(friend_number)
|
||||
|
||||
LOG.debug(f"CM.stop_call _main_screen.call_finished")
|
||||
self._main_screen.call_finished()
|
||||
self._callav.finish_call(friend_number, by_friend) # finish or decline call
|
||||
is_video = self._callav.is_video_call(friend_number)
|
||||
if is_video:
|
||||
def destroy_window():
|
||||
#??? FixMe
|
||||
with ts.ignoreStdout(): import cv2
|
||||
cv2.destroyWindow(str(friend_number))
|
||||
LOG.debug(f"CM.stop_call destroy_window")
|
||||
threading.Timer(2.0, destroy_window).start()
|
||||
|
||||
LOG.debug(f"CM.stop_call _call_finished_event")
|
||||
self._call_finished_event(friend_number, is_declined)
|
||||
|
||||
def friend_exit(self, friend_number:int) -> None:
|
||||
if friend_number in self._callav:
|
||||
self._callav.finish_call(friend_number, True)
|
||||
|
||||
# Private methods
|
||||
|
||||
def _get_incoming_call_widget(self, friend_number, text, friend_name):
|
||||
return av_widgets.IncomingCallWidget(self._settings, self, friend_number, text, friend_name)
|
@ -1,134 +0,0 @@
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
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 = QtWidgets.QLabel(self)
|
||||
self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64))
|
||||
self.avatar_label.setScaledContents(False)
|
||||
self.name = widgets.DataLabel(self)
|
||||
self.name.setGeometry(QtCore.QRect(90, 20, 300, 25))
|
||||
self._friend_number = friend_number
|
||||
font = QtGui.QFont()
|
||||
font.setFamily(settings.Settings.get_instance()['font'])
|
||||
font.setPointSize(16)
|
||||
font.setBold(True)
|
||||
self.name.setFont(font)
|
||||
self.call_type = widgets.DataLabel(self)
|
||||
self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25))
|
||||
self.call_type.setFont(font)
|
||||
self.accept_audio = QtWidgets.QPushButton(self)
|
||||
self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150))
|
||||
self.accept_video = QtWidgets.QPushButton(self)
|
||||
self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150))
|
||||
self.decline = QtWidgets.QPushButton(self)
|
||||
self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150))
|
||||
pixmap = QtGui.QPixmap(util.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)
|
||||
self._processing = False
|
||||
self.accept_audio.clicked.connect(self.accept_call_with_audio)
|
||||
self.accept_video.clicked.connect(self.accept_call_with_video)
|
||||
self.decline.clicked.connect(self.decline_call)
|
||||
|
||||
class SoundPlay(QtCore.QThread):
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.a = None
|
||||
|
||||
def run(self):
|
||||
class AudioFile:
|
||||
chunk = 1024
|
||||
|
||||
def __init__(self, fl):
|
||||
self.stop = False
|
||||
self.fl = fl
|
||||
self.wf = wave.open(self.fl, 'rb')
|
||||
self.p = pyaudio.PyAudio()
|
||||
self.stream = self.p.open(
|
||||
format=self.p.get_format_from_width(self.wf.getsampwidth()),
|
||||
channels=self.wf.getnchannels(),
|
||||
rate=self.wf.getframerate(),
|
||||
output=True)
|
||||
|
||||
def play(self):
|
||||
while not self.stop:
|
||||
data = self.wf.readframes(self.chunk)
|
||||
while data and not self.stop:
|
||||
self.stream.write(data)
|
||||
data = self.wf.readframes(self.chunk)
|
||||
self.wf = wave.open(self.fl, 'rb')
|
||||
|
||||
def close(self):
|
||||
self.stream.close()
|
||||
self.p.terminate()
|
||||
|
||||
self.a = AudioFile(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 accept_call_with_audio(self):
|
||||
if self._processing:
|
||||
return
|
||||
self._processing = True
|
||||
pr = profile.Profile.get_instance()
|
||||
pr.accept_call(self._friend_number, True, False)
|
||||
self.stop()
|
||||
|
||||
def accept_call_with_video(self):
|
||||
if self._processing:
|
||||
return
|
||||
self._processing = True
|
||||
pr = profile.Profile.get_instance()
|
||||
pr.accept_call(self._friend_number, True, True)
|
||||
self.stop()
|
||||
|
||||
def decline_call(self):
|
||||
if self._processing:
|
||||
return
|
||||
self._processing = True
|
||||
pr = profile.Profile.get_instance()
|
||||
pr.stop_call(self._friend_number, False)
|
||||
self.stop()
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self.avatar_label.setPixmap(pixmap)
|
@ -1,118 +0,0 @@
|
||||
from settings import *
|
||||
from PyQt5 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, tox id (or public key)
|
||||
Base class for all contacts.
|
||||
"""
|
||||
|
||||
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._tox_id = tox_id
|
||||
self.init_widget()
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Name - current name or alias of user
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def set_name(self, value):
|
||||
self._name = str(value, 'utf-8')
|
||||
self._widget.name.setText(self._name)
|
||||
self._widget.name.repaint()
|
||||
|
||||
name = property(get_name, set_name)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Status message
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_status_message(self):
|
||||
return self._status_message
|
||||
|
||||
def set_status_message(self, value):
|
||||
self._status_message = str(value, 'utf-8')
|
||||
self._widget.status_message.setText(self._status_message)
|
||||
self._widget.status_message.repaint()
|
||||
|
||||
status_message = property(get_status_message, set_status_message)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Status
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_status(self):
|
||||
return self._status
|
||||
|
||||
def set_status(self, value):
|
||||
self._status = value
|
||||
self._widget.connection_status.update(value)
|
||||
|
||||
status = property(get_status, set_status)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# TOX ID. WARNING: for friend it will return public key, for profile - full address
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_tox_id(self):
|
||||
return self._tox_id
|
||||
|
||||
tox_id = property(get_tox_id)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Avatars
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def load_avatar(self):
|
||||
"""
|
||||
Tries to load avatar of contact or uses default avatar
|
||||
"""
|
||||
prefix = ProfileHelper.get_path() + 'avatars/'
|
||||
avatar_path = prefix + '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
|
||||
if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image
|
||||
avatar_path = curr_directory() + '/images/avatar.png'
|
||||
width = self._widget.avatar_label.width()
|
||||
pixmap = QtGui.QPixmap(avatar_path)
|
||||
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation))
|
||||
self._widget.avatar_label.repaint()
|
||||
|
||||
def reset_avatar(self):
|
||||
avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
|
||||
if os.path.isfile(avatar_path):
|
||||
os.remove(avatar_path)
|
||||
self.load_avatar()
|
||||
|
||||
def set_avatar(self, avatar):
|
||||
avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
|
||||
with open(avatar_path, 'wb') as f:
|
||||
f.write(avatar)
|
||||
self.load_avatar()
|
||||
|
||||
def get_pixmap(self):
|
||||
return self._widget.avatar_label.pixmap()
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Widgets
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def init_widget(self):
|
||||
if self._widget is not None:
|
||||
self._widget.name.setText(self._name)
|
||||
self._widget.status_message.setText(self._status_message)
|
||||
self._widget.connection_status.update(self._status)
|
||||
self.load_avatar()
|
@ -1,75 +0,0 @@
|
||||
import random
|
||||
import urllib.request
|
||||
from util import log, curr_directory
|
||||
import settings
|
||||
from PyQt5 import QtNetwork, QtCore
|
||||
import json
|
||||
|
||||
|
||||
class Node:
|
||||
|
||||
def __init__(self, node):
|
||||
self._ip, self._port, self._tox_key = node['ipv4'], node['port'], node['public_key']
|
||||
self._priority = random.randint(1, 1000000) if node['status_tcp'] and node['status_udp'] else 0
|
||||
|
||||
def get_priority(self):
|
||||
return self._priority
|
||||
|
||||
priority = property(get_priority)
|
||||
|
||||
def get_data(self):
|
||||
return bytes(self._ip, 'utf-8'), self._port, self._tox_key
|
||||
|
||||
|
||||
def generate_nodes():
|
||||
with open(curr_directory() + '/nodes.json', 'rt') as fl:
|
||||
json_nodes = json.loads(fl.read())['nodes']
|
||||
nodes = map(lambda json_node: Node(json_node), json_nodes)
|
||||
sorted_nodes = sorted(nodes, key=lambda x: x.priority)[-4:]
|
||||
for node in sorted_nodes:
|
||||
yield node.get_data()
|
||||
|
||||
|
||||
def save_nodes(nodes):
|
||||
if not nodes:
|
||||
return
|
||||
print('Saving nodes...')
|
||||
with open(curr_directory() + '/nodes.json', 'wb') as fl:
|
||||
fl.write(nodes)
|
||||
|
||||
|
||||
def download_nodes_list():
|
||||
url = 'https://nodes.tox.chat/json'
|
||||
s = settings.Settings.get_instance()
|
||||
if not s['download_nodes_list']:
|
||||
return
|
||||
|
||||
if not s['proxy_type']: # no proxy
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
response = urllib.request.urlopen(req)
|
||||
result = response.read()
|
||||
save_nodes(result)
|
||||
except Exception as ex:
|
||||
log('TOX nodes loading error: ' + str(ex))
|
||||
else: # proxy
|
||||
netman = QtNetwork.QNetworkAccessManager()
|
||||
proxy = QtNetwork.QNetworkProxy()
|
||||
proxy.setType(
|
||||
QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy)
|
||||
proxy.setHostName(s['proxy_host'])
|
||||
proxy.setPort(s['proxy_port'])
|
||||
netman.setProxy(proxy)
|
||||
try:
|
||||
request = QtNetwork.QNetworkRequest()
|
||||
request.setUrl(QtCore.QUrl(url))
|
||||
reply = netman.get(request)
|
||||
|
||||
while not reply.isFinished():
|
||||
QtCore.QThread.msleep(1)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
data = bytes(reply.readAll().data())
|
||||
save_nodes(data)
|
||||
except Exception as ex:
|
||||
log('TOX nodes loading error: ' + str(ex))
|
@ -0,0 +1,48 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
import random
|
||||
import logging
|
||||
|
||||
from qtpy import QtCore
|
||||
try:
|
||||
import certifi
|
||||
from io import BytesIO
|
||||
except ImportError:
|
||||
certifi = None
|
||||
|
||||
from user_data.settings import get_user_config_path
|
||||
from utils.util import *
|
||||
|
||||
from toxygen_wrapper.tests.support_testing import _get_nodes_path
|
||||
from toxygen_wrapper.tests.support_http import download_url
|
||||
import toxygen_wrapper.tests.support_testing as ts
|
||||
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+'bootstrap')
|
||||
|
||||
def download_nodes_list(settings, oArgs) -> str:
|
||||
if not settings['download_nodes_list']:
|
||||
return ''
|
||||
if not ts.bAreWeConnected():
|
||||
return ''
|
||||
url = settings['download_nodes_url']
|
||||
path = _get_nodes_path(oArgs=oArgs)
|
||||
# dont download blindly so we can edit the file and not block on startup
|
||||
if os.path.isfile(path):
|
||||
with open(path, 'rt') as fl:
|
||||
result = fl.read()
|
||||
return result
|
||||
LOG.debug("downloading list of nodes")
|
||||
result = download_url(url, settings._app._settings)
|
||||
if not result:
|
||||
LOG.warn("failed downloading list of nodes")
|
||||
return ''
|
||||
LOG.info("downloaded list of nodes")
|
||||
_save_nodes(result, settings._app)
|
||||
return result
|
||||
|
||||
def _save_nodes(nodes, app) -> None:
|
||||
if not nodes:
|
||||
return
|
||||
with open(_get_nodes_path(app._args), 'wb') as fl:
|
||||
LOG.info("Saving nodes to " +_get_nodes_path(app._args))
|
||||
fl.write(nodes)
|
@ -0,0 +1 @@
|
||||
{"nodes":[{"ipv4":"80.211.19.83","ipv6":"-","port":33445,"public_key":"A2D7BF17C10A12C339B9F4E8DD77DEEE8457D580535A6F0D0F9AF04B8B4C4420","status_udp":true,"status_tcp":true}]}
|
@ -1,469 +0,0 @@
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from notifications import *
|
||||
from settings import Settings
|
||||
from profile import Profile
|
||||
from toxcore_enums_and_consts import *
|
||||
from toxav_enums import *
|
||||
from tox import bin_to_string
|
||||
from plugin_support import PluginLoader
|
||||
import queue
|
||||
import threading
|
||||
import util
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Threads
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class InvokeEvent(QtCore.QEvent):
|
||||
EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
|
||||
|
||||
def __init__(self, fn, *args, **kwargs):
|
||||
QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE)
|
||||
self.fn = fn
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
class Invoker(QtCore.QObject):
|
||||
|
||||
def event(self, event):
|
||||
event.fn(*event.args, **event.kwargs)
|
||||
return True
|
||||
|
||||
|
||||
_invoker = Invoker()
|
||||
|
||||
|
||||
def invoke_in_main_thread(fn, *args, **kwargs):
|
||||
QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs))
|
||||
|
||||
|
||||
class FileTransfersThread(threading.Thread):
|
||||
|
||||
def __init__(self):
|
||||
self._queue = queue.Queue()
|
||||
self._timeout = 0.01
|
||||
self._continue = True
|
||||
super().__init__()
|
||||
|
||||
def execute(self, function, *args, **kwargs):
|
||||
self._queue.put((function, args, kwargs))
|
||||
|
||||
def stop(self):
|
||||
self._continue = False
|
||||
|
||||
def run(self):
|
||||
while self._continue:
|
||||
try:
|
||||
function, args, kwargs = self._queue.get(timeout=self._timeout)
|
||||
function(*args, **kwargs)
|
||||
except queue.Empty:
|
||||
pass
|
||||
except queue.Full:
|
||||
util.log('Queue is Full in _thread')
|
||||
except Exception as ex:
|
||||
util.log('Exception in _thread: ' + str(ex))
|
||||
|
||||
|
||||
_thread = FileTransfersThread()
|
||||
|
||||
|
||||
def start():
|
||||
_thread.start()
|
||||
|
||||
|
||||
def stop():
|
||||
_thread.stop()
|
||||
_thread.join()
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - current user
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def self_connection_status(tox_link):
|
||||
"""
|
||||
Current user changed connection status (offline, UDP, TCP)
|
||||
"""
|
||||
def wrapped(tox, connection, user_data):
|
||||
print('Connection status: ', str(connection))
|
||||
profile = Profile.get_instance()
|
||||
if profile.status is None:
|
||||
status = tox_link.self_get_status()
|
||||
invoke_in_main_thread(profile.set_status, status)
|
||||
elif connection == TOX_CONNECTION['NONE']:
|
||||
invoke_in_main_thread(profile.set_status, None)
|
||||
return wrapped
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - friends
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def friend_status(tox, friend_num, new_status, user_data):
|
||||
"""
|
||||
Check friend's status (none, busy, away)
|
||||
"""
|
||||
print("Friend's #{} status changed!".format(friend_num))
|
||||
profile = Profile.get_instance()
|
||||
friend = profile.get_friend_by_number(friend_num)
|
||||
if friend.status is None and Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
||||
sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS'])
|
||||
invoke_in_main_thread(friend.set_status, new_status)
|
||||
invoke_in_main_thread(QtCore.QTimer.singleShot, 5000, lambda: profile.send_files(friend_num))
|
||||
invoke_in_main_thread(profile.update_filtration)
|
||||
|
||||
|
||||
def friend_connection_status(tox, friend_num, new_status, user_data):
|
||||
"""
|
||||
Check friend's connection status (offline, udp, tcp)
|
||||
"""
|
||||
print("Friend #{} connection status: {}".format(friend_num, new_status))
|
||||
profile = Profile.get_instance()
|
||||
friend = profile.get_friend_by_number(friend_num)
|
||||
if new_status == TOX_CONNECTION['NONE']:
|
||||
invoke_in_main_thread(profile.friend_exit, friend_num)
|
||||
invoke_in_main_thread(profile.update_filtration)
|
||||
if Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
||||
sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS'])
|
||||
elif friend.status is None:
|
||||
invoke_in_main_thread(profile.send_avatar, friend_num)
|
||||
invoke_in_main_thread(PluginLoader.get_instance().friend_online, friend_num)
|
||||
|
||||
|
||||
def friend_name(tox, friend_num, name, size, user_data):
|
||||
"""
|
||||
Friend changed his name
|
||||
"""
|
||||
profile = Profile.get_instance()
|
||||
print('New name friend #' + str(friend_num))
|
||||
invoke_in_main_thread(profile.new_name, friend_num, name)
|
||||
|
||||
|
||||
def friend_status_message(tox, friend_num, status_message, size, user_data):
|
||||
"""
|
||||
:return: function for callback friend_status_message. It updates friend's status message
|
||||
and calls window repaint
|
||||
"""
|
||||
profile = Profile.get_instance()
|
||||
friend = profile.get_friend_by_number(friend_num)
|
||||
invoke_in_main_thread(friend.set_status_message, status_message)
|
||||
print('User #{} has new status'.format(friend_num))
|
||||
invoke_in_main_thread(profile.send_messages, friend_num)
|
||||
if profile.get_active_number() == friend_num:
|
||||
invoke_in_main_thread(profile.set_active)
|
||||
|
||||
|
||||
def friend_message(window, tray):
|
||||
"""
|
||||
New message from friend
|
||||
"""
|
||||
def wrapped(tox, friend_number, message_type, message, size, user_data):
|
||||
profile = Profile.get_instance()
|
||||
settings = Settings.get_instance()
|
||||
message = str(message, 'utf-8')
|
||||
invoke_in_main_thread(profile.new_message, friend_number, message_type, message)
|
||||
if not window.isActiveWindow():
|
||||
friend = profile.get_friend_by_number(friend_number)
|
||||
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked:
|
||||
invoke_in_main_thread(tray_notification, friend.name, message, tray, window)
|
||||
if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
||||
sound_notification(SOUND_NOTIFICATION['MESSAGE'])
|
||||
invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png'))
|
||||
return wrapped
|
||||
|
||||
|
||||
def friend_request(tox, public_key, message, message_size, user_data):
|
||||
"""
|
||||
Called when user get new friend request
|
||||
"""
|
||||
print('Friend request')
|
||||
profile = Profile.get_instance()
|
||||
key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE])
|
||||
tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE)
|
||||
if tox_id not in Settings.get_instance()['blocked']:
|
||||
invoke_in_main_thread(profile.process_friend_request, tox_id, str(message, 'utf-8'))
|
||||
|
||||
|
||||
def friend_typing(tox, friend_number, typing, user_data):
|
||||
invoke_in_main_thread(Profile.get_instance().friend_typing, friend_number, typing)
|
||||
|
||||
|
||||
def friend_read_receipt(tox, friend_number, message_id, user_data):
|
||||
profile = Profile.get_instance()
|
||||
profile.get_friend_by_number(friend_number).dec_receipt()
|
||||
if friend_number == profile.get_active_number():
|
||||
invoke_in_main_thread(profile.receipt)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - file transfers
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def tox_file_recv(window, tray):
|
||||
"""
|
||||
New incoming file
|
||||
"""
|
||||
def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data):
|
||||
profile = Profile.get_instance()
|
||||
settings = Settings.get_instance()
|
||||
if file_type == TOX_FILE_KIND['DATA']:
|
||||
print('File')
|
||||
try:
|
||||
file_name = str(file_name[:file_name_size], 'utf-8')
|
||||
except:
|
||||
file_name = 'toxygen_file'
|
||||
invoke_in_main_thread(profile.incoming_file_transfer,
|
||||
friend_number,
|
||||
file_number,
|
||||
size,
|
||||
file_name)
|
||||
if not window.isActiveWindow():
|
||||
friend = profile.get_friend_by_number(friend_number)
|
||||
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked:
|
||||
file_from = QtWidgets.QApplication.translate("Callback", "File from")
|
||||
invoke_in_main_thread(tray_notification, file_from + ' ' + friend.name, file_name, tray, window)
|
||||
if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
||||
sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER'])
|
||||
invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png'))
|
||||
else: # AVATAR
|
||||
print('Avatar')
|
||||
invoke_in_main_thread(profile.incoming_avatar,
|
||||
friend_number,
|
||||
file_number,
|
||||
size)
|
||||
return wrapped
|
||||
|
||||
|
||||
def file_recv_chunk(tox, friend_number, file_number, position, chunk, length, user_data):
|
||||
"""
|
||||
Incoming chunk
|
||||
"""
|
||||
_thread.execute(Profile.get_instance().incoming_chunk, friend_number, file_number, position,
|
||||
chunk[:length] if length else None)
|
||||
|
||||
|
||||
def file_chunk_request(tox, friend_number, file_number, position, size, user_data):
|
||||
"""
|
||||
Outgoing chunk
|
||||
"""
|
||||
Profile.get_instance().outgoing_chunk(friend_number, file_number, position, size)
|
||||
|
||||
|
||||
def file_recv_control(tox, friend_number, file_number, file_control, user_data):
|
||||
"""
|
||||
Friend cancelled, paused or resumed file transfer
|
||||
"""
|
||||
if file_control == TOX_FILE_CONTROL['CANCEL']:
|
||||
invoke_in_main_thread(Profile.get_instance().cancel_transfer, friend_number, file_number, True)
|
||||
elif file_control == TOX_FILE_CONTROL['PAUSE']:
|
||||
invoke_in_main_thread(Profile.get_instance().pause_transfer, friend_number, file_number, True)
|
||||
elif file_control == TOX_FILE_CONTROL['RESUME']:
|
||||
invoke_in_main_thread(Profile.get_instance().resume_transfer, friend_number, file_number, True)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - custom packets
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def lossless_packet(tox, friend_number, data, length, user_data):
|
||||
"""
|
||||
Incoming lossless packet
|
||||
"""
|
||||
data = data[:length]
|
||||
plugin = PluginLoader.get_instance()
|
||||
invoke_in_main_thread(plugin.callback_lossless, friend_number, data)
|
||||
|
||||
|
||||
def lossy_packet(tox, friend_number, data, length, user_data):
|
||||
"""
|
||||
Incoming lossy packet
|
||||
"""
|
||||
data = data[:length]
|
||||
plugin = PluginLoader.get_instance()
|
||||
invoke_in_main_thread(plugin.callback_lossy, friend_number, data)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - audio
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def call_state(toxav, friend_number, mask, user_data):
|
||||
"""
|
||||
New call state
|
||||
"""
|
||||
print(friend_number, mask)
|
||||
if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']:
|
||||
invoke_in_main_thread(Profile.get_instance().stop_call, friend_number, True)
|
||||
else:
|
||||
Profile.get_instance().call.toxav_call_state_cb(friend_number, mask)
|
||||
|
||||
|
||||
def call(toxav, friend_number, audio, video, user_data):
|
||||
"""
|
||||
Incoming call from friend
|
||||
"""
|
||||
print(friend_number, audio, video)
|
||||
invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number)
|
||||
|
||||
|
||||
def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data):
|
||||
"""
|
||||
New audio chunk
|
||||
"""
|
||||
Profile.get_instance().call.audio_chunk(
|
||||
bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]),
|
||||
audio_channels_count,
|
||||
rate)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - video
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data):
|
||||
"""
|
||||
Creates yuv frame from y, u, v and shows it using OpenCV
|
||||
For yuv => bgr we need this YUV420 frame:
|
||||
|
||||
width
|
||||
-------------------------
|
||||
| |
|
||||
| Y | height
|
||||
| |
|
||||
-------------------------
|
||||
| | |
|
||||
| U even | U odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
| | |
|
||||
| V even | V odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
|
||||
width // 2 width // 2
|
||||
|
||||
It can be created from initial y, u, v using slices
|
||||
"""
|
||||
try:
|
||||
y_size = abs(max(width, abs(ystride)))
|
||||
u_size = abs(max(width // 2, abs(ustride)))
|
||||
v_size = abs(max(width // 2, abs(vstride)))
|
||||
|
||||
y = np.asarray(y[:y_size * height], dtype=np.uint8).reshape(height, y_size)
|
||||
u = np.asarray(u[:u_size * height // 2], dtype=np.uint8).reshape(height // 2, u_size)
|
||||
v = np.asarray(v[:v_size * height // 2], dtype=np.uint8).reshape(height // 2, v_size)
|
||||
|
||||
width -= width % 4
|
||||
height -= height % 4
|
||||
|
||||
frame = np.zeros((int(height * 1.5), width), dtype=np.uint8)
|
||||
|
||||
frame[:height, :] = y[:height, :width]
|
||||
frame[height:height * 5 // 4, :width // 2] = u[:height // 2:2, :width // 2]
|
||||
frame[height:height * 5 // 4, width // 2:] = u[1:height // 2:2, :width // 2]
|
||||
|
||||
frame[height * 5 // 4:, :width // 2] = v[:height // 2:2, :width // 2]
|
||||
frame[height * 5 // 4:, width // 2:] = v[1:height // 2:2, :width // 2]
|
||||
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
||||
|
||||
invoke_in_main_thread(cv2.imshow, str(friend_number), frame)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - groups
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def group_invite(tox, friend_number, gc_type, data, length, user_data):
|
||||
invoke_in_main_thread(Profile.get_instance().group_invite, friend_number, gc_type,
|
||||
bytes(data[:length]))
|
||||
|
||||
|
||||
def show_gc_notification(window, tray, message, group_number, peer_number):
|
||||
profile = Profile.get_instance()
|
||||
settings = Settings.get_instance()
|
||||
chat = profile.get_group_by_number(group_number)
|
||||
peer_name = chat.get_peer_name(peer_number)
|
||||
if not window.isActiveWindow() and (profile.name in message or settings['group_notifications']):
|
||||
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked:
|
||||
invoke_in_main_thread(tray_notification, chat.name + ' ' + peer_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'))
|
||||
|
||||
|
||||
def group_message(window, tray):
|
||||
def wrapped(tox, group_number, peer_number, message, length, user_data):
|
||||
message = str(message[:length], 'utf-8')
|
||||
invoke_in_main_thread(Profile.get_instance().new_gc_message, group_number,
|
||||
peer_number, TOX_MESSAGE_TYPE['NORMAL'], message)
|
||||
show_gc_notification(window, tray, message, group_number, peer_number)
|
||||
return wrapped
|
||||
|
||||
|
||||
def group_action(window, tray):
|
||||
def wrapped(tox, group_number, peer_number, message, length, user_data):
|
||||
message = str(message[:length], 'utf-8')
|
||||
invoke_in_main_thread(Profile.get_instance().new_gc_message, group_number,
|
||||
peer_number, TOX_MESSAGE_TYPE['ACTION'], message)
|
||||
show_gc_notification(window, tray, message, group_number, peer_number)
|
||||
return wrapped
|
||||
|
||||
|
||||
def group_title(tox, group_number, peer_number, title, length, user_data):
|
||||
invoke_in_main_thread(Profile.get_instance().new_gc_title, group_number,
|
||||
title[:length])
|
||||
|
||||
|
||||
def group_namelist_change(tox, group_number, peer_number, change, user_data):
|
||||
invoke_in_main_thread(Profile.get_instance().update_gc, group_number)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# 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)
|
||||
toxav.callback_video_receive_frame(video_receive_frame, 0)
|
||||
|
||||
tox.callback_friend_lossless_packet(lossless_packet, 0)
|
||||
tox.callback_friend_lossy_packet(lossy_packet, 0)
|
||||
|
||||
tox.callback_group_invite(group_invite)
|
||||
tox.callback_group_message(group_message(window, tray))
|
||||
tox.callback_group_action(group_action(window, tray))
|
||||
tox.callback_group_title(group_title)
|
||||
tox.callback_group_namelist_change(group_namelist_change)
|
@ -1,339 +0,0 @@
|
||||
import pyaudio
|
||||
import time
|
||||
import threading
|
||||
import settings
|
||||
from toxav_enums import *
|
||||
import cv2
|
||||
import itertools
|
||||
import numpy as np
|
||||
import screen_sharing
|
||||
# TODO: play sound until outgoing call will be started or cancelled
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class AV:
|
||||
|
||||
def __init__(self, toxav):
|
||||
self._toxav = toxav
|
||||
self._running = True
|
||||
|
||||
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_rate = 8000
|
||||
self._audio_channels = 1
|
||||
self._audio_duration = 60
|
||||
self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000
|
||||
|
||||
self._video = None
|
||||
self._video_thread = None
|
||||
self._video_running = False
|
||||
|
||||
self._video_width = 640
|
||||
self._video_height = 480
|
||||
|
||||
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"""
|
||||
self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0)
|
||||
self._calls[friend_number] = Call(audio, video)
|
||||
threading.Timer(30.0, lambda: self.finish_not_started_call(friend_number)).start()
|
||||
|
||||
def accept_call(self, friend_number, audio_enabled, video_enabled):
|
||||
if self._running:
|
||||
self._calls[friend_number] = Call(audio_enabled, video_enabled)
|
||||
self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0)
|
||||
if audio_enabled:
|
||||
self.start_audio_thread()
|
||||
if video_enabled:
|
||||
self.start_video_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(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()
|
||||
|
||||
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
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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 start_video_thread(self):
|
||||
if self._video_thread is not None:
|
||||
return
|
||||
|
||||
self._video_running = True
|
||||
s = settings.Settings.get_instance()
|
||||
self._video_width = s.video['width']
|
||||
self._video_height = s.video['height']
|
||||
|
||||
if s.video['device'] == -1:
|
||||
self._video = screen_sharing.DesktopGrabber(s.video['x'], s.video['y'],
|
||||
s.video['width'], s.video['height'])
|
||||
else:
|
||||
self._video = cv2.VideoCapture(s.video['device'])
|
||||
self._video.set(cv2.CAP_PROP_FPS, 25)
|
||||
self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width)
|
||||
self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height)
|
||||
|
||||
self._video_thread = threading.Thread(target=self.send_video)
|
||||
self._video_thread.start()
|
||||
|
||||
def stop_video_thread(self):
|
||||
if self._video_thread is None:
|
||||
return
|
||||
|
||||
self._video_running = False
|
||||
self._video_thread.join()
|
||||
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:
|
||||
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)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# AV sending
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
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_num in self._calls:
|
||||
if self._calls[friend_num].out_audio:
|
||||
try:
|
||||
self._toxav.audio_send_frame(friend_num, pcm, self._audio_sample_count,
|
||||
self._audio_channels, self._audio_rate)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
def send_video(self):
|
||||
"""
|
||||
This method sends video to friends
|
||||
"""
|
||||
while self._video_running:
|
||||
try:
|
||||
result, frame = self._video.read()
|
||||
if result:
|
||||
height, width, channels = frame.shape
|
||||
for friend_num in self._calls:
|
||||
if self._calls[friend_num].out_video:
|
||||
try:
|
||||
y, u, v = self.convert_bgr_to_yuv(frame)
|
||||
self._toxav.video_send_frame(friend_num, width, height, y, u, v)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
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
|
||||
"""
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420)
|
||||
|
||||
y = frame[:self._video_height, :]
|
||||
y = list(itertools.chain.from_iterable(y))
|
||||
|
||||
u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
|
||||
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)
|
@ -0,0 +1,26 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class Event:
|
||||
|
||||
def __init__(self):
|
||||
self._callbacks = set()
|
||||
|
||||
def __iadd__(self, callback):
|
||||
self.add_callback(callback)
|
||||
|
||||
return self
|
||||
|
||||
def __isub__(self, callback):
|
||||
self.remove_callback(callback)
|
||||
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
for callback in self._callbacks:
|
||||
callback(*args, **kwargs)
|
||||
|
||||
def add_callback(self, callback):
|
||||
self._callbacks.add(callback)
|
||||
|
||||
def remove_callback(self, callback):
|
||||
self._callbacks.discard(callback)
|
@ -0,0 +1,13 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class Provider:
|
||||
|
||||
def __init__(self, get_item_action):
|
||||
self._get_item_action = get_item_action
|
||||
self._item = None
|
||||
|
||||
def get_item(self):
|
||||
if self._item is None:
|
||||
self._item = self._get_item_action()
|
||||
|
||||
return self._item
|
@ -0,0 +1,18 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class ToxSave:
|
||||
|
||||
def __init__(self, tox):
|
||||
self._tox = tox
|
||||
|
||||
def set_tox(self, tox):
|
||||
self._tox = tox
|
||||
|
||||
|
||||
class ToxAvSave:
|
||||
|
||||
def __init__(self, toxav):
|
||||
self._toxav = toxav
|
||||
|
||||
def set_toxav(self, toxav):
|
||||
self._toxav = toxav
|
@ -0,0 +1,181 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
from user_data.settings import *
|
||||
from qtpy import QtCore, QtGui
|
||||
from toxygen_wrapper.toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE
|
||||
import utils.util as util
|
||||
import common.event as event
|
||||
import contacts.common as common
|
||||
|
||||
|
||||
class BaseContact:
|
||||
"""
|
||||
Class encapsulating TOX contact
|
||||
Properties: name (alias of contact or name), status_message, status (connection status)
|
||||
widget - widget for update, tox id (or public key)
|
||||
Base class for all contacts.
|
||||
"""
|
||||
|
||||
def __init__(self, profile_manager, name, status_message, widget, tox_id, kind=''):
|
||||
"""
|
||||
:param name: name, example: 'Toxygen user'
|
||||
:param status_message: status message, example: 'Toxing on Toxygen'
|
||||
:param widget: ContactItem instance
|
||||
:param tox_id: tox id of contact
|
||||
:param kind: one of ['bot', 'friend', 'group', 'invite', 'grouppeer', '']
|
||||
"""
|
||||
self._profile_manager = profile_manager
|
||||
self._name, self._status_message = name, status_message
|
||||
self._kind = kind
|
||||
self._status, self._widget = None, widget
|
||||
self._tox_id = tox_id
|
||||
|
||||
self._name_changed_event = event.Event()
|
||||
self._status_message_changed_event = event.Event()
|
||||
self._status_changed_event = event.Event()
|
||||
self._avatar_changed_event = event.Event()
|
||||
self.init_widget()
|
||||
|
||||
# Name - current name or alias of user
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def set_name(self, value):
|
||||
if self._name == value:
|
||||
return
|
||||
self._name = value
|
||||
self._widget.name.setText(self._name)
|
||||
self._widget.name.repaint()
|
||||
self._name_changed_event(self._name)
|
||||
|
||||
name = property(get_name, set_name)
|
||||
|
||||
def get_name_changed_event(self):
|
||||
return self._name_changed_event
|
||||
|
||||
name_changed_event = property(get_name_changed_event)
|
||||
|
||||
# Status message
|
||||
|
||||
def get_status_message(self):
|
||||
return self._status_message
|
||||
|
||||
def set_status_message(self, value):
|
||||
if self._status_message == value:
|
||||
return
|
||||
self._status_message = value
|
||||
self._widget.status_message.setText(self._status_message)
|
||||
self._widget.status_message.repaint()
|
||||
self._status_message_changed_event(self._status_message)
|
||||
|
||||
status_message = property(get_status_message, set_status_message)
|
||||
|
||||
def get_status_message_changed_event(self):
|
||||
return self._status_message_changed_event
|
||||
|
||||
status_message_changed_event = property(get_status_message_changed_event)
|
||||
|
||||
# Status
|
||||
|
||||
def get_status(self):
|
||||
return self._status
|
||||
|
||||
def set_status(self, value):
|
||||
if self._status == value:
|
||||
return
|
||||
self._status = value
|
||||
self._widget.connection_status.update(value)
|
||||
self._status_changed_event(self._status)
|
||||
|
||||
status = property(get_status, set_status)
|
||||
|
||||
def get_status_changed_event(self):
|
||||
return self._status_changed_event
|
||||
|
||||
status_changed_event = property(get_status_changed_event)
|
||||
|
||||
# TOX ID. WARNING: for friend it will return public key, for profile - full address
|
||||
|
||||
def get_tox_id(self):
|
||||
return self._tox_id
|
||||
|
||||
tox_id = property(get_tox_id)
|
||||
|
||||
# Avatars
|
||||
|
||||
def load_avatar(self):
|
||||
"""
|
||||
Tries to load avatar of contact or uses default avatar
|
||||
"""
|
||||
try:
|
||||
avatar_path = self.get_avatar_path()
|
||||
width = self._widget.avatar_label.width()
|
||||
pixmap = QtGui.QPixmap(avatar_path)
|
||||
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation))
|
||||
self._widget.avatar_label.repaint()
|
||||
self._avatar_changed_event(avatar_path)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def reset_avatar(self, generate_new):
|
||||
avatar_path = self.get_avatar_path()
|
||||
if os.path.isfile(avatar_path) and not avatar_path == self._get_default_avatar_path():
|
||||
os.remove(avatar_path)
|
||||
if generate_new:
|
||||
self.set_avatar(common.generate_avatar(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]))
|
||||
else:
|
||||
self.load_avatar()
|
||||
|
||||
def set_avatar(self, avatar):
|
||||
avatar_path = self.get_contact_avatar_path()
|
||||
with open(avatar_path, 'wb') as f:
|
||||
f.write(avatar)
|
||||
self.load_avatar()
|
||||
|
||||
def get_pixmap(self):
|
||||
return self._widget.avatar_label.pixmap()
|
||||
|
||||
def get_avatar_path(self):
|
||||
avatar_path = self.get_contact_avatar_path()
|
||||
if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image
|
||||
avatar_path = self._get_default_avatar_path()
|
||||
|
||||
return avatar_path
|
||||
|
||||
def get_contact_avatar_path(self):
|
||||
directory = util.join_path(self._profile_manager.get_dir(), 'avatars')
|
||||
|
||||
return util.join_path(directory, '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]))
|
||||
|
||||
def has_avatar(self):
|
||||
path = self.get_contact_avatar_path()
|
||||
|
||||
return util.file_exists(path)
|
||||
|
||||
def get_avatar_changed_event(self):
|
||||
return self._avatar_changed_event
|
||||
|
||||
avatar_changed_event = property(get_avatar_changed_event)
|
||||
|
||||
# Widgets
|
||||
|
||||
def init_widget(self):
|
||||
# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contacts_manager.py", line 252, in filtration_and_sorting
|
||||
# contact.set_widget(item_widget)
|
||||
# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact.py", line 320, in set_widget
|
||||
if not self._widget:
|
||||
LOG.warn("BC.init_widget self._widget is NULL")
|
||||
return
|
||||
self._widget.name.setText(self._name)
|
||||
self._widget.status_message.setText(self._status_message)
|
||||
if hasattr(self._widget, 'kind'):
|
||||
self._widget.kind.setText(self._kind)
|
||||
self._widget.connection_status.update(self._status)
|
||||
self.load_avatar()
|
||||
|
||||
# Private methods
|
||||
|
||||
@staticmethod
|
||||
def _get_default_avatar_path():
|
||||
return util.join_path(util.get_images_directory(), 'avatar.png')
|
@ -0,0 +1,48 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
|
||||
from pydenticon import Generator
|
||||
|
||||
# Typing notifications
|
||||
|
||||
class BaseTypingNotificationHandler:
|
||||
|
||||
DEFAULT_HANDLER = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def send(self, tox, is_typing):
|
||||
pass
|
||||
|
||||
|
||||
class FriendTypingNotificationHandler(BaseTypingNotificationHandler):
|
||||
|
||||
def __init__(self, friend_number:int):
|
||||
super().__init__()
|
||||
self._friend_number = friend_number
|
||||
|
||||
def send(self, tox, is_typing):
|
||||
tox.self_set_typing(self._friend_number, is_typing)
|
||||
|
||||
|
||||
BaseTypingNotificationHandler.DEFAULT_HANDLER = BaseTypingNotificationHandler()
|
||||
|
||||
|
||||
# Identicons support
|
||||
|
||||
|
||||
def generate_avatar(public_key):
|
||||
foreground = ['rgb(45,79,255)', 'rgb(185, 66, 244)', 'rgb(185, 66, 244)',
|
||||
'rgb(254,180,44)', 'rgb(252, 2, 2)', 'rgb(109, 198, 0)',
|
||||
'rgb(226,121,234)', 'rgb(130, 135, 124)',
|
||||
'rgb(30,179,253)', 'rgb(160, 157, 0)',
|
||||
'rgb(232,77,65)', 'rgb(102, 4, 4)',
|
||||
'rgb(49,203,115)',
|
||||
'rgb(141,69,170)']
|
||||
generator = Generator(5, 5, foreground=foreground, background='rgba(42,42,42,0)')
|
||||
digest = hashlib.sha256(public_key.encode('utf-8')).hexdigest()
|
||||
identicon = generator.generate(digest, 220, 220, padding=(10, 10, 10, 10))
|
||||
|
||||
return identicon
|
@ -0,0 +1,229 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
from qtpy import QtWidgets
|
||||
|
||||
import utils.ui as util_ui
|
||||
from toxygen_wrapper.toxcore_enums_and_consts import *
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger('app')
|
||||
|
||||
# Builder
|
||||
|
||||
def _create_menu(menu_name, parent):
|
||||
menu_name = menu_name or ''
|
||||
|
||||
return QtWidgets.QMenu(menu_name) if parent is None else parent.addMenu(menu_name)
|
||||
|
||||
|
||||
class ContactMenuBuilder:
|
||||
|
||||
def __init__(self):
|
||||
self._actions = {}
|
||||
self._submenus = {}
|
||||
self._name = None
|
||||
self._index = 0
|
||||
|
||||
def with_name(self, name):
|
||||
self._name = name
|
||||
|
||||
return self
|
||||
|
||||
def with_action(self, text, handler):
|
||||
self._add_action(text, handler)
|
||||
|
||||
return self
|
||||
|
||||
def with_optional_action(self, text, handler, show_action):
|
||||
if show_action:
|
||||
self._add_action(text, handler)
|
||||
|
||||
return self
|
||||
|
||||
def with_actions(self, actions):
|
||||
for action in actions:
|
||||
(text, handler) = action
|
||||
self._add_action(text, handler)
|
||||
|
||||
return self
|
||||
|
||||
def with_submenu(self, submenu_builder):
|
||||
self._add_submenu(submenu_builder)
|
||||
|
||||
return self
|
||||
|
||||
def with_optional_submenu(self, submenu_builder):
|
||||
if submenu_builder is not None:
|
||||
self._add_submenu(submenu_builder)
|
||||
|
||||
return self
|
||||
|
||||
def build(self, parent=None):
|
||||
menu = _create_menu(self._name, parent)
|
||||
|
||||
for i in range(self._index):
|
||||
if i in self._actions:
|
||||
text, handler = self._actions[i]
|
||||
action = menu.addAction(text)
|
||||
action.triggered.connect(handler)
|
||||
else:
|
||||
submenu_builder = self._submenus[i]
|
||||
submenu = submenu_builder.build(menu)
|
||||
menu.addMenu(submenu)
|
||||
|
||||
return menu
|
||||
|
||||
def _add_submenu(self, submenu):
|
||||
self._submenus[self._index] = submenu
|
||||
self._index += 1
|
||||
|
||||
def _add_action(self, text, handler):
|
||||
self._actions[self._index] = (text, handler)
|
||||
self._index += 1
|
||||
|
||||
# Generators
|
||||
|
||||
|
||||
class BaseContactMenuGenerator:
|
||||
|
||||
def __init__(self, contact):
|
||||
self._contact = contact
|
||||
|
||||
def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader):
|
||||
return ContactMenuBuilder().build()
|
||||
|
||||
# Private methods
|
||||
|
||||
def _generate_copy_menu_builder(self, main_screen):
|
||||
copy_menu_builder = ContactMenuBuilder()
|
||||
(copy_menu_builder
|
||||
.with_name(util_ui.tr('Copy'))
|
||||
.with_action(util_ui.tr('Name'), lambda: main_screen.copy_text(self._contact.name))
|
||||
.with_action(util_ui.tr("Status message"), lambda: main_screen.copy_text(self._contact.status_message))
|
||||
.with_action(util_ui.tr("Public key"), lambda: main_screen.copy_text(self._contact.tox_id))
|
||||
)
|
||||
|
||||
return copy_menu_builder
|
||||
|
||||
def _generate_history_menu_builder(self, history_loader, main_screen):
|
||||
history_menu_builder = ContactMenuBuilder()
|
||||
(history_menu_builder
|
||||
.with_name(util_ui.tr("Chat history"))
|
||||
.with_action(util_ui.tr("Clear history"), lambda: history_loader.clear_history(self._contact)
|
||||
or main_screen.messages.clear())
|
||||
.with_action(util_ui.tr("Export as text"), lambda: history_loader.export_history(self._contact))
|
||||
.with_action(util_ui.tr("Export as HTML"), lambda: history_loader.export_history(self._contact, False))
|
||||
)
|
||||
|
||||
return history_menu_builder
|
||||
|
||||
|
||||
class FriendMenuGenerator(BaseContactMenuGenerator):
|
||||
|
||||
def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader):
|
||||
history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen)
|
||||
copy_menu_builder = self._generate_copy_menu_builder(main_screen)
|
||||
plugins_menu_builder = self._generate_plugins_menu_builder(plugin_loader, number)
|
||||
groups_menu_builder = self._generate_groups_menu(contacts_manager, groups_service)
|
||||
|
||||
allowed = self._contact.tox_id in settings['auto_accept_from_friends']
|
||||
auto = util_ui.tr("Disallow auto accept") if allowed else util_ui.tr('Allow auto accept')
|
||||
|
||||
builder = ContactMenuBuilder()
|
||||
menu = (builder
|
||||
.with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number))
|
||||
.with_submenu(history_menu_builder)
|
||||
.with_submenu(copy_menu_builder)
|
||||
.with_action(auto, lambda: main_screen.auto_accept(number, not allowed))
|
||||
.with_action(util_ui.tr("Remove friend"), lambda: main_screen.remove_friend(number))
|
||||
.with_action(util_ui.tr("Block friend"), lambda: main_screen.block_friend(number))
|
||||
.with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact))
|
||||
.with_optional_submenu(plugins_menu_builder)
|
||||
.with_optional_submenu(groups_menu_builder)
|
||||
).build()
|
||||
|
||||
return menu
|
||||
|
||||
# Private methods
|
||||
|
||||
@staticmethod
|
||||
def _generate_plugins_menu_builder(plugin_loader, number):
|
||||
if plugin_loader is None:
|
||||
return None
|
||||
plugins_actions = plugin_loader.get_menu(number)
|
||||
if not len(plugins_actions):
|
||||
return None
|
||||
plugins_menu_builder = ContactMenuBuilder()
|
||||
(plugins_menu_builder
|
||||
.with_name(util_ui.tr('Plugins'))
|
||||
.with_actions(plugins_actions)
|
||||
)
|
||||
|
||||
return plugins_menu_builder
|
||||
|
||||
def _generate_groups_menu(self, contacts_manager, groups_service):
|
||||
chats = contacts_manager.get_group_chats()
|
||||
LOG.debug(f"_generate_groups_menu len(chats)={len(chats)} or self._contact.status={self._contact.status}")
|
||||
if not len(chats) or self._contact.status is None:
|
||||
#? return None
|
||||
pass
|
||||
groups_menu_builder = ContactMenuBuilder()
|
||||
(groups_menu_builder
|
||||
.with_name(util_ui.tr("Invite to group"))
|
||||
.with_actions([(g.name, lambda: groups_service.invite_friend(self._contact.number, g.number)) for g in chats])
|
||||
)
|
||||
|
||||
return groups_menu_builder
|
||||
|
||||
|
||||
class GroupMenuGenerator(BaseContactMenuGenerator):
|
||||
|
||||
def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader):
|
||||
copy_menu_builder = self._generate_copy_menu_builder(main_screen)
|
||||
history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen)
|
||||
|
||||
builder = ContactMenuBuilder()
|
||||
menu = (builder
|
||||
.with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number))
|
||||
.with_submenu(copy_menu_builder)
|
||||
.with_submenu(history_menu_builder)
|
||||
.with_optional_action(util_ui.tr("Manage group"),
|
||||
lambda: groups_service.show_group_management_screen(self._contact),
|
||||
self._contact.is_self_founder())
|
||||
.with_optional_action(util_ui.tr("Group settings"),
|
||||
lambda: groups_service.show_group_settings_screen(self._contact),
|
||||
not self._contact.is_self_founder())
|
||||
.with_optional_action(util_ui.tr("Set topic"),
|
||||
lambda: groups_service.set_group_topic(self._contact),
|
||||
self._contact.is_self_moderator_or_founder())
|
||||
# .with_action(util_ui.tr("Bans list"),
|
||||
# lambda: groups_service.show_bans_list(self._contact))
|
||||
.with_action(util_ui.tr("Reconnect to group"),
|
||||
lambda: groups_service.reconnect_to_group(self._contact.number))
|
||||
.with_optional_action(util_ui.tr("Disconnect from group"),
|
||||
lambda: groups_service.disconnect_from_group(self._contact.number),
|
||||
self._contact.status is not None)
|
||||
.with_action(util_ui.tr("Leave group"), lambda: groups_service.leave_group(self._contact.number))
|
||||
.with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact))
|
||||
).build()
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
class GroupPeerMenuGenerator(BaseContactMenuGenerator):
|
||||
|
||||
def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader):
|
||||
copy_menu_builder = self._generate_copy_menu_builder(main_screen)
|
||||
history_menu_builder = self._generate_history_menu_builder(history_loader, main_screen)
|
||||
|
||||
builder = ContactMenuBuilder()
|
||||
menu = (builder
|
||||
.with_action(util_ui.tr("Set alias"), lambda: main_screen.set_alias(number))
|
||||
.with_submenu(copy_menu_builder)
|
||||
.with_submenu(history_menu_builder)
|
||||
.with_action(util_ui.tr("Quit chat"),
|
||||
lambda: contacts_manager.remove_group_peer(self._contact))
|
||||
.with_action(util_ui.tr('Notes'), lambda: main_screen.show_note(self._contact))
|
||||
).build()
|
||||
|
||||
return menu
|
@ -0,0 +1,166 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import common.tox_save as tox_save
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# callbacks can be called in any thread so were being careful
|
||||
from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
|
||||
|
||||
class ContactProvider(tox_save.ToxSave):
|
||||
|
||||
def __init__(self, tox, friend_factory, group_factory, group_peer_factory, app=None):
|
||||
super().__init__(tox)
|
||||
self._friend_factory = friend_factory
|
||||
self._group_factory = group_factory
|
||||
self._group_peer_factory = group_peer_factory
|
||||
self._cache = {} # key - contact's public key, value - contact instance
|
||||
self._app = app
|
||||
|
||||
# Friends
|
||||
|
||||
def get_friend_by_number(self, friend_number:int):
|
||||
try:
|
||||
public_key = self._tox.friend_get_public_key(friend_number)
|
||||
except Exception as e:
|
||||
LOG_WARN(f"CP.get_friend_by_number NO {friend_number} {e} ")
|
||||
return None
|
||||
return self.get_friend_by_public_key(public_key)
|
||||
|
||||
def get_friend_by_public_key(self, public_key):
|
||||
friend = self._get_contact_from_cache(public_key)
|
||||
if friend is not None:
|
||||
return friend
|
||||
friend = self._friend_factory.create_friend_by_public_key(public_key)
|
||||
if friend is None:
|
||||
LOG_WARN(f"CP.get_friend_by_public_key NULL {friend} ")
|
||||
else:
|
||||
self._add_to_cache(public_key, friend)
|
||||
LOG_DEBUG(f"CP.get_friend_by_public_key ADDED {friend} ")
|
||||
return friend
|
||||
|
||||
def get_all_friends(self) -> list:
|
||||
if self._app and self._app.bAppExiting:
|
||||
return []
|
||||
try:
|
||||
friend_numbers = self._tox.self_get_friend_list()
|
||||
except Exception as e:
|
||||
LOG_WARN(f"CP.get_all_friends EXCEPTION {e} ")
|
||||
return []
|
||||
friends = map(lambda n: self.get_friend_by_number(n), friend_numbers)
|
||||
return list(friends)
|
||||
|
||||
# Groups
|
||||
|
||||
def get_all_groups(self):
|
||||
"""from callbacks"""
|
||||
try:
|
||||
len_groups = self._tox.group_get_number_groups()
|
||||
group_numbers = range(len_groups)
|
||||
except Exception as e:
|
||||
return None
|
||||
groups = list(map(lambda n: self.get_group_by_number(n), group_numbers))
|
||||
# failsafe in case there are bogus None groups?
|
||||
fgroups = list(filter(lambda x: x, groups))
|
||||
if len(fgroups) != len_groups:
|
||||
LOG_WARN(f"CP.are there are bogus None groups in libtoxcore? {len(fgroups)} != {len_groups}")
|
||||
for group_num in group_numbers:
|
||||
group = self.get_group_by_number(group_num)
|
||||
if group is None:
|
||||
LOG_ERROR(f"There are bogus None groups in libtoxcore {group_num}!")
|
||||
# fixme: do something
|
||||
groups = fgroups
|
||||
return groups
|
||||
|
||||
def get_group_by_number(self, group_number):
|
||||
group = None
|
||||
try:
|
||||
# LOG_DEBUG(f"CP.CP.group_get_number {group_number} ")
|
||||
# original code
|
||||
chat_id = self._tox.group_get_chat_id(group_number)
|
||||
if chat_id is None:
|
||||
LOG_ERROR(f"get_group_by_number NULL chat_id ({group_number})")
|
||||
elif chat_id == '-1':
|
||||
LOG_ERROR(f"get_group_by_number <0 chat_id ({group_number})")
|
||||
else:
|
||||
LOG_INFO(f"CP.group_get_number {group_number} {chat_id}")
|
||||
group = self.get_group_by_chat_id(chat_id)
|
||||
if group is None or group == '-1':
|
||||
LOG_WARN(f"CP.get_group_by_number leaving {group} ({group_number})")
|
||||
#? iRet = self._tox.group_leave(group_number)
|
||||
# invoke in main thread?
|
||||
# self._contacts_manager.delete_group(group_number)
|
||||
return group
|
||||
except Exception as e:
|
||||
LOG_WARN(f"CP.group_get_number {group_number} {e}")
|
||||
return None
|
||||
|
||||
def get_group_by_chat_id(self, chat_id):
|
||||
group = self._get_contact_from_cache(chat_id)
|
||||
if group is not None:
|
||||
return group
|
||||
group = self._group_factory.create_group_by_chat_id(chat_id)
|
||||
if group is None:
|
||||
LOG_ERROR(f"get_group_by_chat_id NULL chat_id={chat_id}")
|
||||
else:
|
||||
self._add_to_cache(chat_id, group)
|
||||
|
||||
return group
|
||||
|
||||
def get_group_by_public_key(self, public_key):
|
||||
group = self._get_contact_from_cache(public_key)
|
||||
if group is not None:
|
||||
return group
|
||||
group = self._group_factory.create_group_by_public_key(public_key)
|
||||
if group is None:
|
||||
LOG_WARN(f"get_group_by_public_key NULL group public_key={public_key}")
|
||||
else:
|
||||
self._add_to_cache(public_key, group)
|
||||
|
||||
return group
|
||||
|
||||
# Group peers
|
||||
|
||||
def get_all_group_peers(self):
|
||||
return []
|
||||
|
||||
def get_group_peer_by_id(self, group, peer_id):
|
||||
peer = group.get_peer_by_id(peer_id)
|
||||
if peer is not None:
|
||||
return self._get_group_peer(group, peer)
|
||||
LOG_WARN(f"get_group_peer_by_id peer_id={peer_id}")
|
||||
return None
|
||||
|
||||
def get_group_peer_by_public_key(self, group, public_key):
|
||||
peer = group.get_peer_by_public_key(public_key)
|
||||
if peer is not None:
|
||||
return self._get_group_peer(group, peer)
|
||||
LOG_WARN(f"get_group_peer_by_public_key public_key={public_key}")
|
||||
return None
|
||||
|
||||
# All contacts
|
||||
|
||||
def get_all(self):
|
||||
return self.get_all_friends() + self.get_all_groups() + self.get_all_group_peers()
|
||||
|
||||
# Caching
|
||||
|
||||
def clear_cache(self):
|
||||
self._cache.clear()
|
||||
|
||||
def remove_contact_from_cache(self, contact_public_key):
|
||||
if contact_public_key in self._cache:
|
||||
del self._cache[contact_public_key]
|
||||
|
||||
# Private methods
|
||||
|
||||
def _get_contact_from_cache(self, public_key):
|
||||
return self._cache[public_key] if public_key in self._cache else None
|
||||
|
||||
def _add_to_cache(self, public_key, contact):
|
||||
self._cache[public_key] = contact
|
||||
|
||||
def _get_group_peer(self, group, peer):
|
||||
return self._group_peer_factory.create_group_peer(group, peer)
|
@ -0,0 +1,670 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from contacts.friend import Friend
|
||||
from contacts.group_chat import GroupChat
|
||||
from messenger.messages import *
|
||||
from common.tox_save import ToxSave
|
||||
from contacts.group_peer_contact import GroupPeerContact
|
||||
from groups.group_peer import GroupChatPeer
|
||||
from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
|
||||
import toxygen_wrapper.toxcore_enums_and_consts as enums
|
||||
|
||||
# LOG=util.log
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+__name__)
|
||||
|
||||
UINT32_MAX = 2 ** 32 -1
|
||||
|
||||
def set_contact_kind(contact) -> None:
|
||||
bInvite = len(contact.name) == enums.TOX_PUBLIC_KEY_SIZE * 2 and \
|
||||
contact.status_message == ''
|
||||
bBot = not bInvite and contact.name.lower().endswith(' bot')
|
||||
if type(contact) == Friend and bInvite:
|
||||
contact._kind = 'invite'
|
||||
elif type(contact) == Friend and bBot:
|
||||
contact._kind = 'bot'
|
||||
elif type(contact) == Friend:
|
||||
contact._kind = 'friend'
|
||||
elif type(contact) == GroupChat:
|
||||
contact._kind = 'group'
|
||||
elif type(contact) == GroupChatPeer:
|
||||
contact._kind = 'grouppeer'
|
||||
|
||||
class ContactsManager(ToxSave):
|
||||
"""
|
||||
Represents contacts list.
|
||||
"""
|
||||
|
||||
def __init__(self, tox, settings, screen, profile_manager, contact_provider, history, tox_dns,
|
||||
messages_items_factory):
|
||||
super().__init__(tox)
|
||||
self._settings = settings
|
||||
self._screen = screen
|
||||
self._ms = screen
|
||||
self._profile_manager = profile_manager
|
||||
self._contact_provider = contact_provider
|
||||
self._tox_dns = tox_dns
|
||||
self._messages_items_factory = messages_items_factory
|
||||
self._messages = screen.messages
|
||||
self._contacts = []
|
||||
self._active_contact = -1
|
||||
self._active_contact_changed = Event()
|
||||
self._sorting = settings['sorting']
|
||||
self._filter_string = ''
|
||||
screen.contacts_filter.setCurrentIndex(int(self._sorting))
|
||||
self._history = history
|
||||
self._load_contacts()
|
||||
|
||||
def _log(self, s) -> None:
|
||||
try:
|
||||
self._ms._log(s)
|
||||
except: pass
|
||||
|
||||
def get_contact(self, num):
|
||||
if num < 0 or num >= len(self._contacts):
|
||||
return None
|
||||
return self._contacts[num]
|
||||
|
||||
def get_curr_contact(self):
|
||||
return self._contacts[self._active_contact] if self._active_contact + 1 else None
|
||||
|
||||
def save_profile(self) -> None:
|
||||
data = self._tox.get_savedata()
|
||||
self._profile_manager.save_profile(data)
|
||||
|
||||
def is_friend_active(self, friend_number:int) -> bool:
|
||||
if not self.is_active_a_friend():
|
||||
return False
|
||||
|
||||
return self.get_curr_contact().number == friend_number
|
||||
|
||||
def is_group_active(self, group_number) -> bool:
|
||||
if self.is_active_a_friend():
|
||||
return False
|
||||
|
||||
return self.get_curr_contact().number == group_number
|
||||
|
||||
def is_contact_active(self, contact) -> bool:
|
||||
if self._active_contact == -1:
|
||||
# LOG.debug("No self._active_contact")
|
||||
return False
|
||||
if self._active_contact >= len(self._contacts):
|
||||
LOG.warn(f"ERROR _active_contact={self._active_contact} >= contacts len={len(self._contacts)}")
|
||||
return False
|
||||
if not self._contacts[self._active_contact]:
|
||||
LOG.warn(f"ERROR NULL {self._contacts[self._active_contact]} {contact.tox_id}")
|
||||
return False
|
||||
|
||||
if not hasattr(contact, 'tox_id'):
|
||||
LOG.warn(f"ERROR is_contact_active no contact.tox_id {type(contact)} contact={contact}")
|
||||
return False
|
||||
|
||||
return self._contacts[self._active_contact].tox_id == contact.tox_id
|
||||
|
||||
# Reconnection support
|
||||
|
||||
def reset_contacts_statuses(self) -> None:
|
||||
for contact in self._contacts:
|
||||
contact.status = None
|
||||
|
||||
# Work with active friend
|
||||
|
||||
def get_active(self):
|
||||
return self._active_contact
|
||||
|
||||
def set_active(self, value):
|
||||
"""
|
||||
Change current active friend or update info
|
||||
:param value: number of new active friend in friend's list
|
||||
"""
|
||||
if value is None and self._active_contact == -1: # nothing to update
|
||||
return
|
||||
if value == -1: # all friends were deleted
|
||||
self._screen.account_name.setText('')
|
||||
self._screen.account_status.setText('')
|
||||
self._screen.account_status.setToolTip('')
|
||||
self._active_contact = -1
|
||||
self._screen.account_avatar.setHidden(True)
|
||||
self._messages.clear()
|
||||
self._screen.messageEdit.clear()
|
||||
return
|
||||
try:
|
||||
self._screen.typing.setVisible(False)
|
||||
current_contact = self.get_curr_contact()
|
||||
if current_contact is not None:
|
||||
# TODO: send when needed
|
||||
current_contact.typing_notification_handler.send(self._tox, False)
|
||||
current_contact.remove_messages_widgets() # TODO: if required
|
||||
self._unsubscribe_from_events(current_contact)
|
||||
|
||||
if self._active_contact >= 0 and self._active_contact != value:
|
||||
try:
|
||||
current_contact.curr_text = self._screen.messageEdit.toPlainText()
|
||||
except:
|
||||
pass
|
||||
|
||||
# IndexError: list index out of range
|
||||
if value >= len(self._contacts):
|
||||
LOG.warn("CM.set_active value too big: {{self._contacts}}")
|
||||
return
|
||||
contact = self._contacts[value]
|
||||
self._subscribe_to_events(contact)
|
||||
contact.remove_invalid_unsent_files()
|
||||
if self._active_contact != value:
|
||||
self._screen.messageEdit.setPlainText(contact.curr_text)
|
||||
self._active_contact = value
|
||||
contact.reset_messages()
|
||||
if not self._settings['save_history']:
|
||||
contact.delete_old_messages()
|
||||
self._messages.clear()
|
||||
contact.load_corr()
|
||||
corr = contact.get_corr()[-PAGE_SIZE:]
|
||||
for message in corr:
|
||||
if message.type == MESSAGE_TYPE['FILE_TRANSFER']:
|
||||
self._messages_items_factory.create_file_transfer_item(message)
|
||||
elif message.type == MESSAGE_TYPE['INLINE']:
|
||||
self._messages_items_factory.create_inline_item(message)
|
||||
else:
|
||||
self._messages_items_factory.create_message_item(message)
|
||||
self._messages.scrollToBottom()
|
||||
# if value in self._call:
|
||||
# self._screen.active_call()
|
||||
# elif value in self._incoming_calls:
|
||||
# self._screen.incoming_call()
|
||||
# else:
|
||||
# self._screen.call_finished()
|
||||
self._set_current_contact_data(contact)
|
||||
self._active_contact_changed(contact)
|
||||
except Exception as e: # no friend found. ignore
|
||||
LOG.warn(f"CM.set_active EXCEPTION value:{value} len={len(self._contacts)} {e}")
|
||||
# gulp raise
|
||||
|
||||
active_contact = property(get_active, set_active)
|
||||
|
||||
def get_active_contact_changed(self):
|
||||
return self._active_contact_changed
|
||||
|
||||
active_contact_changed = property(get_active_contact_changed)
|
||||
|
||||
def update(self):
|
||||
if self._active_contact + 1:
|
||||
self.set_active(self._active_contact)
|
||||
|
||||
def is_active_a_friend(self):
|
||||
return type(self.get_curr_contact()) is Friend
|
||||
|
||||
def is_active_a_group(self):
|
||||
return type(self.get_curr_contact()) is GroupChat
|
||||
|
||||
def is_active_a_group_chat_peer(self):
|
||||
return type(self.get_curr_contact()) is GroupPeerContact
|
||||
|
||||
# Filtration
|
||||
|
||||
def filtration_and_sorting(self, sorting=0, filter_str=''):
|
||||
"""
|
||||
Filtration of friends list
|
||||
:param sorting: 0 - no sorting, 1 - online only, 2 - online first, 3 - by name,
|
||||
4 - online and by name, 5 - online first and by name, 6 kind
|
||||
:param filter_str: show contacts which name contains this substring
|
||||
"""
|
||||
filter_str = filter_str.lower()
|
||||
current_contact = self.get_curr_contact()
|
||||
|
||||
for index, contact in enumerate(self._contacts):
|
||||
if not contact._kind:
|
||||
set_contact_kind(contact)
|
||||
|
||||
if sorting > 6 or sorting < 0:
|
||||
sorting = 0
|
||||
|
||||
if sorting in (1, 2, 4, 5): # online first
|
||||
self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True)
|
||||
sort_by_name = sorting in (4, 5)
|
||||
# save results of previous sorting
|
||||
online_friends = filter(lambda x: x.status is not None, self._contacts)
|
||||
online_friends_count = len(list(online_friends))
|
||||
part1 = self._contacts[:online_friends_count]
|
||||
part2 = self._contacts[online_friends_count:]
|
||||
key_lambda = lambda x: x.name.lower() if sort_by_name else x.number
|
||||
part1 = sorted(part1, key=key_lambda)
|
||||
part2 = sorted(part2, key=key_lambda)
|
||||
self._contacts = part1 + part2
|
||||
elif sorting == 0:
|
||||
# AttributeError: 'NoneType' object has no attribute 'number'
|
||||
for (i, contact) in enumerate(self._contacts):
|
||||
if contact is None or not hasattr(contact, 'number'):
|
||||
LOG.error(f"Contact {i} is None or not hasattr 'number'")
|
||||
del self._contacts[i]
|
||||
continue
|
||||
contacts = sorted(self._contacts, key=lambda c: c.number)
|
||||
friends = filter(lambda c: type(c) is Friend, contacts)
|
||||
groups = filter(lambda c: type(c) is GroupChat, contacts)
|
||||
group_peers = filter(lambda c: type(c) is GroupPeerContact, contacts)
|
||||
self._contacts = list(friends) + list(groups) + list(group_peers)
|
||||
elif sorting == 6:
|
||||
self._contacts = sorted(self._contacts, key=lambda x: x._kind)
|
||||
else:
|
||||
self._contacts = sorted(self._contacts, key=lambda x: x.name.lower())
|
||||
|
||||
|
||||
# change item widgets
|
||||
for index, contact in enumerate(self._contacts):
|
||||
list_item = self._screen.friends_list.item(index)
|
||||
item_widget = self._screen.friends_list.itemWidget(list_item)
|
||||
if not item_widget:
|
||||
LOG_WARN("CM.filtration_and_sorting( item_widget is NULL")
|
||||
continue
|
||||
contact.set_widget(item_widget)
|
||||
|
||||
for index, friend in enumerate(self._contacts):
|
||||
filtered_by_name = filter_str in friend.name.lower()
|
||||
friend.visibility = (friend.status is not None or sorting not in (1, 4)) and filtered_by_name
|
||||
# show friend even if it's hidden when there any unread messages/actions
|
||||
friend.visibility = friend.visibility or friend.messages or friend.actions
|
||||
item = self._screen.friends_list.item(index)
|
||||
item_widget = self._screen.friends_list.itemWidget(item)
|
||||
item.setSizeHint(QtCore.QSize(250, item_widget.height() if friend.visibility else 0))
|
||||
|
||||
# save soring results
|
||||
self._sorting, self._filter_string = sorting, filter_str
|
||||
self._settings['sorting'] = self._sorting
|
||||
self._settings.save()
|
||||
|
||||
# update active contact
|
||||
if current_contact is not None:
|
||||
index = self._contacts.index(current_contact)
|
||||
self.set_active(index)
|
||||
|
||||
def update_filtration(self):
|
||||
"""
|
||||
Update list of contacts when 1 of friends change connection status
|
||||
"""
|
||||
self.filtration_and_sorting(self._sorting, self._filter_string)
|
||||
|
||||
# Contact getters
|
||||
|
||||
def get_friend_by_number(self, number):
|
||||
return list(filter(lambda c: c.number == number and type(c) is Friend, self._contacts))[0]
|
||||
|
||||
def get_group_by_number(self, number):
|
||||
return list(filter(lambda c: c.number == number and type(c) is GroupChat, self._contacts))[0]
|
||||
|
||||
def get_or_create_group_peer_contact(self, group_number, peer_id):
|
||||
group = self.get_group_by_number(group_number)
|
||||
peer = group.get_peer_by_id(peer_id)
|
||||
if peer is None:
|
||||
LOG.warn(f'get_or_create_group_peer_contact group_number={group_number} peer_id={peer_id} peer={peer}')
|
||||
return None
|
||||
LOG.debug(f'get_or_create_group_peer_contact group_number={group_number} peer_id={peer_id} peer={peer}')
|
||||
if not self.check_if_contact_exists(peer.public_key):
|
||||
contact = self.add_group_peer(group, peer)
|
||||
# dunno
|
||||
return contact
|
||||
# me - later wrong kind of object?
|
||||
return self.get_contact_by_tox_id(peer.public_key)
|
||||
|
||||
def check_if_contact_exists(self, tox_id):
|
||||
return any(filter(lambda c: c.tox_id == tox_id, self._contacts))
|
||||
|
||||
def get_contact_by_tox_id(self, tox_id):
|
||||
return list(filter(lambda c: c.tox_id == tox_id, self._contacts))[0]
|
||||
|
||||
def get_active_number(self):
|
||||
return self.get_curr_contact().number if self._active_contact + 1 else -1
|
||||
|
||||
def get_active_name(self):
|
||||
return self.get_curr_contact().name if self._active_contact + 1 else ''
|
||||
|
||||
def is_active_online(self):
|
||||
return self._active_contact + 1 and self.get_curr_contact().status is not None
|
||||
|
||||
# Work with friends (remove, block, set alias, get public key)
|
||||
|
||||
def set_alias(self, num):
|
||||
"""
|
||||
Set new alias for friend
|
||||
"""
|
||||
friend = self._contacts[num]
|
||||
name = friend.name
|
||||
text = util_ui.tr("Enter new alias for friend {} or leave empty to use friend's name:").format(name)
|
||||
title = util_ui.tr('Set alias')
|
||||
text, ok = util_ui.text_dialog(text, title, name)
|
||||
if not ok:
|
||||
return
|
||||
aliases = self._settings['friends_aliases']
|
||||
if text:
|
||||
friend.name = text
|
||||
try:
|
||||
index = list(map(lambda x: x[0], aliases)).index(friend.tox_id)
|
||||
aliases[index] = (friend.tox_id, text)
|
||||
except:
|
||||
aliases.append((friend.tox_id, text))
|
||||
friend.set_alias(text)
|
||||
else: # use default name
|
||||
friend.name = self._tox.friend_get_name(friend.number)
|
||||
friend.set_alias('')
|
||||
try:
|
||||
index = list(map(lambda x: x[0], aliases)).index(friend.tox_id)
|
||||
del aliases[index]
|
||||
except:
|
||||
pass
|
||||
self._settings.save()
|
||||
|
||||
def friend_public_key(self, num):
|
||||
return self._contacts[num].tox_id
|
||||
|
||||
def delete_friend(self, num):
|
||||
"""
|
||||
Removes friend from contact list
|
||||
:param num: number of friend in list
|
||||
"""
|
||||
friend = self._contacts[num]
|
||||
self._cleanup_contact_data(friend)
|
||||
try:
|
||||
self._tox.friend_delete(friend.number)
|
||||
except Exception as e:
|
||||
LOG.warn(f"'There was no friend with the given friend number {e}")
|
||||
self._delete_contact(num)
|
||||
|
||||
def add_friend(self, tox_id):
|
||||
"""
|
||||
Adds friend to list
|
||||
"""
|
||||
self._tox.friend_add_norequest(tox_id)
|
||||
self._add_friend(tox_id)
|
||||
self.update_filtration()
|
||||
|
||||
def block_user(self, tox_id):
|
||||
"""
|
||||
Block user with specified tox id (or public key) - delete from friends list and ignore friend requests
|
||||
"""
|
||||
tox_id = tox_id[:enums.TOX_PUBLIC_KEY_SIZE * 2]
|
||||
if tox_id == self._tox.self_get_address()[:enums.TOX_PUBLIC_KEY_SIZE * 2]:
|
||||
return
|
||||
if tox_id not in self._settings['blocked']:
|
||||
self._settings['blocked'].append(tox_id)
|
||||
self._settings.save()
|
||||
try:
|
||||
num = self._tox.friend_by_public_key(tox_id)
|
||||
self.delete_friend(num)
|
||||
self.save_profile()
|
||||
except: # not in friend list
|
||||
pass
|
||||
|
||||
def unblock_user(self, tox_id, add_to_friend_list):
|
||||
"""
|
||||
Unblock user
|
||||
:param tox_id: tox id of contact
|
||||
:param add_to_friend_list: add this contact to friend list or not
|
||||
"""
|
||||
self._settings['blocked'].remove(tox_id)
|
||||
self._settings.save()
|
||||
if add_to_friend_list:
|
||||
self.add_friend(tox_id)
|
||||
self.save_profile()
|
||||
|
||||
# Groups support
|
||||
|
||||
def get_group_chats(self):
|
||||
return list(filter(lambda c: type(c) is GroupChat, self._contacts))
|
||||
|
||||
def add_group(self, group_number):
|
||||
index = len(self._contacts)
|
||||
group = self._contact_provider.get_group_by_number(group_number)
|
||||
if group is None:
|
||||
LOG.warn(f"CM.add_group: NULL group from group_number={group_number}")
|
||||
elif type(group) == int and group < 0:
|
||||
LOG.warn(f"CM.add_group: NO group from group={group} group_number={group_number}")
|
||||
else:
|
||||
LOG.info(f"CM.add_group: Adding group {group._name}")
|
||||
self._contacts.append(group)
|
||||
LOG.info(f"contacts_manager.add_group: saving profile")
|
||||
self._save_profile()
|
||||
group.reset_avatar(self._settings['identicons'])
|
||||
LOG.info(f"contacts_manager.add_group: setting active")
|
||||
self.set_active(index)
|
||||
self.update_filtration()
|
||||
|
||||
def delete_group(self, group_number):
|
||||
group = self.get_group_by_number(group_number)
|
||||
self._cleanup_contact_data(group)
|
||||
num = self._contacts.index(group)
|
||||
self._delete_contact(num)
|
||||
|
||||
# Groups private messaging
|
||||
|
||||
def add_group_peer(self, group, peer):
|
||||
contact = self._contact_provider.get_group_peer_by_id(group, peer.id)
|
||||
if self.check_if_contact_exists(contact.tox_id):
|
||||
return contact
|
||||
contact._kind = 'grouppeer'
|
||||
self._contacts.append(contact)
|
||||
contact.reset_avatar(self._settings['identicons'])
|
||||
self._save_profile()
|
||||
return contact
|
||||
|
||||
def remove_group_peer_by_id(self, group, peer_id):
|
||||
peer = group.get_peer_by_id(peer_id)
|
||||
if peer: # broken
|
||||
if not self.check_if_contact_exists(peer.public_key):
|
||||
return
|
||||
contact = self.get_contact_by_tox_id(peer.public_key)
|
||||
self.remove_group_peer(contact)
|
||||
|
||||
def remove_group_peer(self, group_peer_contact):
|
||||
contact = self.get_contact_by_tox_id(group_peer_contact.tox_id)
|
||||
if contact:
|
||||
self._cleanup_contact_data(contact)
|
||||
num = self._contacts.index(contact)
|
||||
self._delete_contact(num)
|
||||
|
||||
def get_gc_peer_name(self, name):
|
||||
group = self.get_curr_contact()
|
||||
|
||||
names = sorted(group.get_peers_names())
|
||||
if name in names: # return next nick
|
||||
index = names.index(name)
|
||||
index = (index + 1) % len(names)
|
||||
|
||||
return names[index]
|
||||
|
||||
suggested_names = list(filter(lambda x: x.startswith(name), names))
|
||||
if not len(suggested_names):
|
||||
return '\t'
|
||||
|
||||
return suggested_names[0]
|
||||
|
||||
# Friend requests
|
||||
|
||||
def send_friend_request(self, sToxPkOrId, message):
|
||||
"""
|
||||
Function tries to send request to contact with specified id
|
||||
:param sToxPkOrId: id of new contact or tox dns 4 value
|
||||
:param message: additional message
|
||||
:return: True on success else error string
|
||||
"""
|
||||
retval = ''
|
||||
try:
|
||||
message = message or 'Hello! Add me to your contact list please'
|
||||
if len(sToxPkOrId) == enums.TOX_PUBLIC_KEY_SIZE * 2: # public key
|
||||
self.add_friend(sToxPkOrId)
|
||||
title = 'Friend added'
|
||||
text = 'Friend added without sending friend request'
|
||||
else:
|
||||
num = self._tox.friend_add(sToxPkOrId, message.encode('utf-8'))
|
||||
if num < UINT32_MAX:
|
||||
tox_pk = sToxPkOrId[:enums.TOX_PUBLIC_KEY_SIZE * 2]
|
||||
self._add_friend(tox_pk)
|
||||
self.update_filtration()
|
||||
title = 'Friend added'
|
||||
text = 'Friend added by sending friend request'
|
||||
self.save_profile()
|
||||
retval = True
|
||||
else:
|
||||
title = 'Friend failed'
|
||||
text = 'Friend failed sending friend request'
|
||||
retval = text
|
||||
|
||||
except Exception as ex: # wrong data
|
||||
title = 'Friend add exception'
|
||||
text = 'Friend request exception with ' + str(ex)
|
||||
self._log(text)
|
||||
LOG.exception(text)
|
||||
LOG.warn(f"DELETE {sToxPkOrId} ?")
|
||||
retval = str(ex)
|
||||
title = util_ui.tr(title)
|
||||
text = util_ui.tr(text)
|
||||
util_ui.message_box(text, title)
|
||||
return retval
|
||||
|
||||
def process_friend_request(self, tox_id, message):
|
||||
"""
|
||||
Accept or ignore friend request
|
||||
:param tox_id: tox id of contact
|
||||
:param message: message
|
||||
"""
|
||||
if tox_id in self._settings['blocked']:
|
||||
return
|
||||
try:
|
||||
text = util_ui.tr('User {} wants to add you to contact list. Message:\n{}')
|
||||
reply = util_ui.question(text.format(tox_id, message), util_ui.tr('Friend request'))
|
||||
if reply: # accepted
|
||||
self.add_friend(tox_id)
|
||||
data = self._tox.get_savedata()
|
||||
self._profile_manager.save_profile(data)
|
||||
except Exception as ex: # something is wrong
|
||||
LOG.error('Accept friend request failed! ' + str(ex))
|
||||
|
||||
def can_send_typing_notification(self):
|
||||
return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer()
|
||||
|
||||
# Contacts numbers update
|
||||
|
||||
def update_friends_numbers(self):
|
||||
for friend in self._contact_provider.get_all_friends():
|
||||
friend.number = self._tox.friend_by_public_key(friend.tox_id)
|
||||
self.update_filtration()
|
||||
|
||||
def update_groups_numbers(self):
|
||||
groups = self._contact_provider.get_all_groups()
|
||||
LOG.info(f"update_groups_numbers len(groups)={len(groups)}")
|
||||
# Thread 76 "ToxIterateThrea" received signal SIGSEGV, Segmentation fault.
|
||||
for i in range(len(groups)):
|
||||
chat_id = self._tox.group_get_chat_id(i)
|
||||
if not chat_id:
|
||||
LOG.warn(f"update_groups_numbers {i} chat_id")
|
||||
continue
|
||||
group = self.get_contact_by_tox_id(chat_id)
|
||||
if not group:
|
||||
LOG.warn(f"update_groups_numbers {i} group")
|
||||
continue
|
||||
group.number = i
|
||||
self.update_filtration()
|
||||
|
||||
def update_groups_lists(self):
|
||||
groups = self._contact_provider.get_all_groups()
|
||||
for group in groups:
|
||||
group.remove_all_peers_except_self()
|
||||
|
||||
# Private methods
|
||||
|
||||
def _load_contacts(self):
|
||||
self._load_friends()
|
||||
self._load_groups()
|
||||
if len(self._contacts):
|
||||
self.set_active(0)
|
||||
# filter(lambda c: not c.has_avatar(), self._contacts)
|
||||
for (i, contact) in enumerate(self._contacts):
|
||||
if contact is None:
|
||||
LOG.warn(f"_load_contacts NULL contact {i}")
|
||||
LOG.info(f"_load_contacts deleting NULL {self._contacts[i]}")
|
||||
del self._contacts[i]
|
||||
#? self.save_profile()
|
||||
continue
|
||||
if contact.has_avatar(): continue
|
||||
contact.reset_avatar(self._settings['identicons'])
|
||||
self.update_filtration()
|
||||
|
||||
def _load_friends(self):
|
||||
self._contacts.extend(self._contact_provider.get_all_friends())
|
||||
|
||||
def _load_groups(self):
|
||||
self._contacts.extend(self._contact_provider.get_all_groups())
|
||||
|
||||
# Current contact subscriptions
|
||||
|
||||
def _subscribe_to_events(self, contact):
|
||||
contact.name_changed_event.add_callback(self._current_contact_name_changed)
|
||||
contact.status_changed_event.add_callback(self._current_contact_status_changed)
|
||||
contact.status_message_changed_event.add_callback(self._current_contact_status_message_changed)
|
||||
contact.avatar_changed_event.add_callback(self._current_contact_avatar_changed)
|
||||
|
||||
def _unsubscribe_from_events(self, contact):
|
||||
contact.name_changed_event.remove_callback(self._current_contact_name_changed)
|
||||
contact.status_changed_event.remove_callback(self._current_contact_status_changed)
|
||||
contact.status_message_changed_event.remove_callback(self._current_contact_status_message_changed)
|
||||
contact.avatar_changed_event.remove_callback(self._current_contact_avatar_changed)
|
||||
|
||||
def _current_contact_name_changed(self, name):
|
||||
self._screen.account_name.setText(name)
|
||||
|
||||
def _current_contact_status_changed(self, status):
|
||||
pass
|
||||
|
||||
def _current_contact_status_message_changed(self, status_message):
|
||||
self._screen.account_status.setText(status_message)
|
||||
|
||||
def _current_contact_avatar_changed(self, avatar_path):
|
||||
self._set_current_contact_avatar(avatar_path)
|
||||
|
||||
def _set_current_contact_data(self, contact):
|
||||
self._screen.account_name.setText(contact.name)
|
||||
self._screen.account_status.setText(contact.status_message)
|
||||
self._set_current_contact_avatar(contact.get_avatar_path())
|
||||
|
||||
def _set_current_contact_avatar(self, avatar_path):
|
||||
width = self._screen.account_avatar.width()
|
||||
pixmap = QtGui.QPixmap(avatar_path)
|
||||
self._screen.account_avatar.setPixmap(pixmap.scaled(width, width,
|
||||
QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
|
||||
|
||||
def _add_friend(self, tox_id):
|
||||
self._history.add_friend_to_db(tox_id)
|
||||
friend = self._contact_provider.get_friend_by_public_key(tox_id)
|
||||
index = len(self._contacts)
|
||||
self._contacts.append(friend)
|
||||
if not friend.has_avatar():
|
||||
friend.reset_avatar(self._settings['identicons'])
|
||||
self._save_profile()
|
||||
self.set_active(index)
|
||||
|
||||
def _save_profile(self):
|
||||
data = self._tox.get_savedata()
|
||||
self._profile_manager.save_profile(data)
|
||||
|
||||
def _cleanup_contact_data(self, contact):
|
||||
try:
|
||||
index = list(map(lambda x: x[0], self._settings['friends_aliases'])).index(contact.tox_id)
|
||||
del self._settings['friends_aliases'][index]
|
||||
except Exception as e:
|
||||
pass
|
||||
if contact.tox_id in self._settings['notes']:
|
||||
del self._settings['notes'][contact.tox_id]
|
||||
self._settings.save()
|
||||
self._history.delete_history(contact)
|
||||
if contact.has_avatar():
|
||||
avatar_path = contact.get_contact_avatar_path()
|
||||
remove(avatar_path)
|
||||
|
||||
def _delete_contact(self, num):
|
||||
self.set_active(-1 if len(self._contacts) == 1 else 0)
|
||||
|
||||
self._contact_provider.remove_contact_from_cache(self._contacts[num].tox_id)
|
||||
del self._contacts[num]
|
||||
self._screen.friends_list.takeItem(num)
|
||||
self._save_profile()
|
||||
|
||||
self.update_filtration()
|
@ -0,0 +1,68 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from contacts import contact, common
|
||||
from messenger.messages import *
|
||||
from contacts.contact_menu import *
|
||||
|
||||
class Friend(contact.Contact):
|
||||
"""
|
||||
Friend in list of friends.
|
||||
"""
|
||||
|
||||
def __init__(self, profile_manager, message_getter, number, name, status_message, widget, tox_id):
|
||||
super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id)
|
||||
self._receipts = 0
|
||||
self._typing_notification_handler = common.FriendTypingNotificationHandler(number)
|
||||
|
||||
# File transfers support
|
||||
|
||||
def insert_inline(self, before_message_id, inline):
|
||||
"""
|
||||
Update status of active transfer and load inline if needed
|
||||
"""
|
||||
try:
|
||||
tr = list(filter(lambda m: m.message_id == before_message_id, self._corr))[0]
|
||||
i = self._corr.index(tr)
|
||||
if inline: # inline was loaded
|
||||
self._corr.insert(i, inline)
|
||||
return i - len(self._corr)
|
||||
except:
|
||||
return -1
|
||||
|
||||
def get_unsent_files(self):
|
||||
messages = filter(lambda m: type(m) is UnsentFileMessage, self._corr)
|
||||
return list(messages)
|
||||
|
||||
def clear_unsent_files(self):
|
||||
self._corr = list(filter(lambda m: type(m) is not UnsentFileMessage, self._corr))
|
||||
|
||||
def remove_invalid_unsent_files(self):
|
||||
def is_valid(message):
|
||||
if type(message) is not UnsentFileMessage:
|
||||
return True
|
||||
if message.data is not None:
|
||||
return True
|
||||
return os.path.exists(message.path)
|
||||
|
||||
self._corr = list(filter(is_valid, self._corr))
|
||||
|
||||
def delete_one_unsent_file(self, message_id):
|
||||
self._corr = list(filter(lambda m: not (type(m) is UnsentFileMessage and m.message_id == message_id),
|
||||
self._corr))
|
||||
|
||||
# Full status
|
||||
|
||||
def get_full_status(self):
|
||||
return self._status_message
|
||||
|
||||
# Typing notifications
|
||||
|
||||
def get_typing_notification_handler(self):
|
||||
return self._typing_notification_handler
|
||||
|
||||
# Context menu support
|
||||
|
||||
def get_context_menu_generator(self):
|
||||
return FriendMenuGenerator(self)
|
@ -0,0 +1,43 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
from contacts.friend import Friend
|
||||
from common.tox_save import ToxSave
|
||||
|
||||
class FriendFactory(ToxSave):
|
||||
|
||||
def __init__(self, profile_manager, settings, tox, db, items_factory):
|
||||
super().__init__(tox)
|
||||
self._profile_manager = profile_manager
|
||||
self._settings = settings
|
||||
self._db = db
|
||||
self._items_factory = items_factory
|
||||
|
||||
def create_friend_by_public_key(self, public_key):
|
||||
friend_number = self._tox.friend_by_public_key(public_key)
|
||||
return self.create_friend_by_number(friend_number)
|
||||
|
||||
def create_friend_by_number(self, friend_number:int):
|
||||
aliases = self._settings['friends_aliases']
|
||||
sToxPk = self._tox.friend_get_public_key(friend_number)
|
||||
assert sToxPk, sToxPk
|
||||
try:
|
||||
alias = list(filter(lambda x: x[0] == sToxPk, aliases))[0][1]
|
||||
except:
|
||||
alias = ''
|
||||
item = self._create_friend_item()
|
||||
name = alias or self._tox.friend_get_name(friend_number) or sToxPk
|
||||
status_message = self._tox.friend_get_status_message(friend_number)
|
||||
message_getter = self._db.messages_getter(sToxPk)
|
||||
friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, sToxPk)
|
||||
friend.set_alias(alias)
|
||||
|
||||
return friend
|
||||
|
||||
# Private methods
|
||||
|
||||
def _create_friend_item(self):
|
||||
"""
|
||||
Method-factory
|
||||
:return: new widget for friend instance
|
||||
"""
|
||||
return self._items_factory.create_contact_item()
|
@ -0,0 +1,161 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
from contacts import contact
|
||||
from contacts.contact_menu import GroupMenuGenerator
|
||||
import utils.util as util
|
||||
from groups.group_peer import GroupChatPeer
|
||||
from toxygen_wrapper import toxcore_enums_and_consts as constants
|
||||
from common.tox_save import ToxSave
|
||||
from groups.group_ban import GroupBan
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger(__name__)
|
||||
from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
|
||||
|
||||
class GroupChat(contact.Contact, ToxSave):
|
||||
|
||||
def __init__(self, tox, profile_manager, message_getter, number, name, status_message, widget, tox_id, is_private):
|
||||
super().__init__(profile_manager, message_getter, number, name, status_message, widget, tox_id)
|
||||
ToxSave.__init__(self, tox)
|
||||
|
||||
self._is_private = is_private
|
||||
self._password = str()
|
||||
self._peers_limit = 512
|
||||
self._peers = []
|
||||
self._add_self_to_gc()
|
||||
|
||||
def remove_invalid_unsent_files(self):
|
||||
pass
|
||||
|
||||
def get_context_menu_generator(self):
|
||||
return GroupMenuGenerator(self)
|
||||
|
||||
# Properties
|
||||
|
||||
def get_is_private(self):
|
||||
return self._is_private
|
||||
|
||||
def set_is_private(self, is_private):
|
||||
self._is_private = is_private
|
||||
|
||||
is_private = property(get_is_private, set_is_private)
|
||||
|
||||
def get_password(self):
|
||||
return self._password
|
||||
|
||||
def set_password(self, password):
|
||||
self._password = password
|
||||
|
||||
password = property(get_password, set_password)
|
||||
|
||||
def get_peers_limit(self):
|
||||
return self._peers_limit
|
||||
|
||||
def set_peers_limit(self, peers_limit):
|
||||
self._peers_limit = peers_limit
|
||||
|
||||
peers_limit = property(get_peers_limit, set_peers_limit)
|
||||
|
||||
# Peers methods
|
||||
|
||||
def get_self_peer(self):
|
||||
return self._peers[0]
|
||||
|
||||
def get_self_name(self):
|
||||
return self._peers[0].name
|
||||
|
||||
def get_self_role(self):
|
||||
return self._peers[0].role
|
||||
|
||||
def is_self_moderator_or_founder(self):
|
||||
return self.get_self_role() <= constants.TOX_GROUP_ROLE['MODERATOR']
|
||||
|
||||
def is_self_founder(self):
|
||||
return self.get_self_role() == constants.TOX_GROUP_ROLE['FOUNDER']
|
||||
|
||||
def add_peer(self, peer_id, is_current_user=False):
|
||||
"called from callbacks"
|
||||
if peer_id > self._peers_limit:
|
||||
LOG_WARN(f"add_peer id={peer_id} > {self._peers_limit}")
|
||||
return
|
||||
|
||||
status_message = f"Private in {self.name}"
|
||||
LOG_TRACE(f"GC.add_peer id={peer_id} status_message={status_message}")
|
||||
peer = GroupChatPeer(peer_id,
|
||||
self._tox.group_peer_get_name(self._number, peer_id),
|
||||
self._tox.group_peer_get_status(self._number, peer_id),
|
||||
self._tox.group_peer_get_role(self._number, peer_id),
|
||||
self._tox.group_peer_get_public_key(self._number, peer_id),
|
||||
is_current_user,
|
||||
status_message=status_message)
|
||||
self._peers.append(peer)
|
||||
|
||||
def remove_peer(self, peer_id):
|
||||
if peer_id == self.get_self_peer().id: # we were kicked or banned
|
||||
self.remove_all_peers_except_self()
|
||||
else:
|
||||
peer = self.get_peer_by_id(peer_id)
|
||||
if peer: # broken
|
||||
self._peers.remove(peer)
|
||||
else:
|
||||
LOG_WARN(f"remove_peer empty peers for {peer_id}")
|
||||
|
||||
def get_peer_by_id(self, peer_id):
|
||||
peers = list(filter(lambda p: p.id == peer_id, self._peers))
|
||||
if peers:
|
||||
return peers[0]
|
||||
else:
|
||||
LOG_WARN(f"get_peer_by_id empty peers for {peer_id}")
|
||||
return None
|
||||
|
||||
def get_peer_by_public_key(self, public_key):
|
||||
peers = list(filter(lambda p: p.public_key == public_key, self._peers))
|
||||
# DEBUGc: group_moderation #0 mod_id=4294967295 event_type=3
|
||||
# WARN_: get_peer_by_id empty peers for 4294967295
|
||||
if peers:
|
||||
return peers[0]
|
||||
else:
|
||||
LOG_WARN(f"get_peer_by_public_key empty peers for {public_key}")
|
||||
return None
|
||||
|
||||
def remove_all_peers_except_self(self):
|
||||
self._peers = self._peers[:1]
|
||||
|
||||
def get_peers_names(self):
|
||||
peers_names = map(lambda p: p.name, self._peers)
|
||||
if peers_names: # broken
|
||||
return list(peers_names)
|
||||
else:
|
||||
LOG_WARN(f"get_peers_names empty peers")
|
||||
#? broken
|
||||
return []
|
||||
|
||||
def get_peers(self):
|
||||
return self._peers[:]
|
||||
|
||||
peers = property(get_peers)
|
||||
|
||||
def get_bans(self):
|
||||
return []
|
||||
# ban_ids = self._tox.group_ban_get_list(self._number)
|
||||
# bans = []
|
||||
# for ban_id in ban_ids:
|
||||
# ban = GroupBan(ban_id,
|
||||
# self._tox.group_ban_get_target(self._number, ban_id),
|
||||
# self._tox.group_ban_get_time_set(self._number, ban_id))
|
||||
# bans.append(ban)
|
||||
#
|
||||
# return bans
|
||||
#
|
||||
bans = property(get_bans)
|
||||
|
||||
# Private methods
|
||||
|
||||
@staticmethod
|
||||
def _get_default_avatar_path():
|
||||
return util.join_path(util.get_images_directory(), 'group.png')
|
||||
|
||||
def _add_self_to_gc(self):
|
||||
peer_id = self._tox.group_self_get_peer_id(self._number)
|
||||
self.add_peer(peer_id, True)
|
@ -0,0 +1,59 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
from contacts.group_chat import GroupChat
|
||||
from common.tox_save import ToxSave
|
||||
import toxygen_wrapper.toxcore_enums_and_consts as constants
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
class GroupFactory(ToxSave):
|
||||
|
||||
def __init__(self, profile_manager, settings, tox, db, items_factory):
|
||||
super().__init__(tox)
|
||||
self._profile_manager = profile_manager
|
||||
self._settings = settings
|
||||
self._db = db
|
||||
self._items_factory = items_factory
|
||||
|
||||
def create_group_by_chat_id(self, chat_id):
|
||||
return self.create_group_by_public_key(chat_id)
|
||||
|
||||
def create_group_by_public_key(self, public_key):
|
||||
group_number = self._get_group_number_by_chat_id(public_key)
|
||||
return self.create_group_by_number(group_number)
|
||||
|
||||
def create_group_by_number(self, group_number):
|
||||
LOG.info(f"create_group_by_number {group_number}")
|
||||
aliases = self._settings['friends_aliases']
|
||||
tox_id = self._tox.group_get_chat_id(group_number)
|
||||
try:
|
||||
alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1]
|
||||
except:
|
||||
alias = ''
|
||||
item = self._create_group_item()
|
||||
name = alias or self._tox.group_get_name(group_number) or tox_id
|
||||
status_message = self._tox.group_get_topic(group_number)
|
||||
message_getter = self._db.messages_getter(tox_id)
|
||||
is_private = self._tox.group_get_privacy_state(group_number) == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE']
|
||||
group = GroupChat(self._tox, self._profile_manager, message_getter, group_number, name, status_message,
|
||||
item, tox_id, is_private)
|
||||
group.set_alias(alias)
|
||||
|
||||
return group
|
||||
|
||||
# Private methods
|
||||
|
||||
def _create_group_item(self):
|
||||
"""
|
||||
Method-factory
|
||||
:return: new widget for group instance
|
||||
"""
|
||||
return self._items_factory.create_contact_item()
|
||||
|
||||
def _get_group_number_by_chat_id(self, chat_id):
|
||||
for i in range(self._tox.group_get_number_groups()+100):
|
||||
if self._tox.group_get_chat_id(i) == chat_id:
|
||||
return i
|
||||
return -1
|
@ -0,0 +1,22 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import contacts.contact
|
||||
from contacts.contact_menu import GroupPeerMenuGenerator
|
||||
|
||||
class GroupPeerContact(contacts.contact.Contact):
|
||||
|
||||
def __init__(self, profile_manager, message_getter, peer_number, name, widget, tox_id, group_pk, status_message=None):
|
||||
if status_message is None: status_message=str()
|
||||
super().__init__(profile_manager, message_getter, peer_number, name, status_message, widget, tox_id)
|
||||
self._group_pk = group_pk
|
||||
|
||||
def get_group_pk(self):
|
||||
return self._group_pk
|
||||
|
||||
group_pk = property(get_group_pk)
|
||||
|
||||
def remove_invalid_unsent_files(self):
|
||||
pass
|
||||
|
||||
def get_context_menu_generator(self):
|
||||
return GroupPeerMenuGenerator(self)
|
@ -0,0 +1,26 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
from common.tox_save import ToxSave
|
||||
from contacts.group_peer_contact import GroupPeerContact
|
||||
|
||||
class GroupPeerFactory(ToxSave):
|
||||
|
||||
def __init__(self, tox, profile_manager, db, items_factory):
|
||||
super().__init__(tox)
|
||||
self._profile_manager = profile_manager
|
||||
self._db = db
|
||||
self._items_factory = items_factory
|
||||
|
||||
def create_group_peer(self, group, peer):
|
||||
item = self._create_group_peer_item()
|
||||
message_getter = self._db.messages_getter(peer.public_key)
|
||||
group_peer_contact = GroupPeerContact(self._profile_manager, message_getter, peer.id, peer.name,
|
||||
item,
|
||||
peer.public_key,
|
||||
group.tox_id,
|
||||
status_message=peer.status_message)
|
||||
group_peer_contact.status = peer.status
|
||||
|
||||
return group_peer_contact
|
||||
|
||||
def _create_group_peer_item(self):
|
||||
return self._items_factory.create_contact_item()
|
@ -0,0 +1,107 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
from contacts import basecontact
|
||||
import random
|
||||
import threading
|
||||
import common.tox_save as tox_save
|
||||
from middleware.threads import invoke_in_main_thread
|
||||
|
||||
iUMAXINT = 4294967295
|
||||
iRECONNECT = 50
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger('app.'+__name__)
|
||||
|
||||
class Profile(basecontact.BaseContact, tox_save.ToxSave):
|
||||
"""
|
||||
Profile of current toxygen user.
|
||||
"""
|
||||
def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action, app=None):
|
||||
"""
|
||||
:param tox: tox instance
|
||||
:param screen: ref to main screen
|
||||
"""
|
||||
assert tox
|
||||
basecontact.BaseContact.__init__(self,
|
||||
profile_manager,
|
||||
tox.self_get_name(),
|
||||
tox.self_get_status_message(),
|
||||
screen,
|
||||
tox.self_get_address())
|
||||
tox_save.ToxSave.__init__(self, tox)
|
||||
self._screen = screen
|
||||
self._messages = screen.messages
|
||||
self._contacts_provider = contacts_provider
|
||||
self._reset_action = reset_action
|
||||
self._waiting_for_reconnection = False
|
||||
self._timer = None
|
||||
self._app = app
|
||||
|
||||
# Edit current user's data
|
||||
|
||||
def change_status(self) -> None:
|
||||
"""
|
||||
Changes status of user (online, away, busy)
|
||||
"""
|
||||
if self._status is not None:
|
||||
self.set_status((self._status + 1) % 3)
|
||||
|
||||
def set_status(self, status) -> None:
|
||||
super().set_status(status)
|
||||
if status is not None:
|
||||
self._tox.self_set_status(status)
|
||||
elif not self._waiting_for_reconnection:
|
||||
self._waiting_for_reconnection = True
|
||||
self._timer = threading.Timer(iRECONNECT, self._reconnect)
|
||||
self._timer.start()
|
||||
|
||||
def set_name(self, value) -> None:
|
||||
if self.name == value:
|
||||
return
|
||||
super().set_name(value)
|
||||
self._tox.self_set_name(self._name)
|
||||
|
||||
def set_status_message(self, value) -> None:
|
||||
super().set_status_message(value)
|
||||
self._tox.self_set_status_message(self._status_message)
|
||||
|
||||
def set_new_nospam(self):
|
||||
"""Sets new nospam part of tox id"""
|
||||
self._tox.self_set_nospam(random.randint(0, iUMAXINT)) # no spam - uint32
|
||||
self._tox_id = self._tox.self_get_address()
|
||||
self._sToxId = self._tox.self_get_address()
|
||||
return self._sToxId
|
||||
|
||||
# Reset
|
||||
|
||||
def restart(self) -> None:
|
||||
"""
|
||||
Recreate tox instance
|
||||
"""
|
||||
self.status = None
|
||||
invoke_in_main_thread(self._reset_action)
|
||||
|
||||
def _reconnect(self) -> None:
|
||||
self._waiting_for_reconnection = False
|
||||
if self._app and self._app.bAppExiting:
|
||||
# dont do anything after the app has been shipped
|
||||
# there's a segv that results
|
||||
return
|
||||
contacts = self._contacts_provider.get_all_friends()
|
||||
all_friends_offline = all(list(map(lambda x: x.status is None, contacts)))
|
||||
if self.status is None or (all_friends_offline and len(contacts)):
|
||||
self._waiting_for_reconnection = True
|
||||
self.restart()
|
||||
self._timer = threading.Timer(iRECONNECT, self._reconnect)
|
||||
self._timer.start()
|
||||
|
||||
# Current thread 0x00007901a13ccb80 (most recent call first):
|
||||
# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 826 in self_get_friend_list_size
|
||||
# File "/usr/local/lib/python3.11/site-packages/toxygen_wrapper/tox.py", line 838 in self_get_friend_list
|
||||
# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact_provider.py", line 45 in get_all_friends
|
||||
# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/profile.py", line 90 in _reconnect
|
||||
# File "/usr/lib/python3.11/threading.py", line 1401 in run
|
||||
# File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner
|
||||
# File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap
|
||||
#
|
||||
|
@ -0,0 +1,371 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from messenger.messages import *
|
||||
from file_transfers.file_transfers import SendAvatar, is_inline
|
||||
from ui.contact_items import *
|
||||
import utils.util as util
|
||||
from common.tox_save import ToxSave
|
||||
|
||||
from middleware.callbacks import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
|
||||
|
||||
# LOG=util.log
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+__name__)
|
||||
log = lambda x: LOG.info(x)
|
||||
|
||||
class FileTransfersHandler(ToxSave):
|
||||
lBlockAvatars = []
|
||||
def __init__(self, tox, settings, contact_provider, file_transfers_message_service, profile):
|
||||
super().__init__(tox)
|
||||
self._settings = settings
|
||||
self._contact_provider = contact_provider
|
||||
self._file_transfers_message_service = file_transfers_message_service
|
||||
self._file_transfers = {}
|
||||
# key = (friend number, file number), value - transfer instance
|
||||
self._paused_file_transfers = dict(settings['paused_file_transfers'])
|
||||
# key - file id, value: [path, friend number, is incoming, start position]
|
||||
self._insert_inline_before = {}
|
||||
# key = (friend number, file number), value - message id
|
||||
|
||||
profile.avatar_changed_event.add_callback(self._send_avatar_to_contacts)
|
||||
self. lBlockAvatars = []
|
||||
|
||||
def stop(self) -> None:
|
||||
self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {}
|
||||
self._settings.save()
|
||||
|
||||
# File transfers support
|
||||
|
||||
def incoming_file_transfer(self, friend_number, file_number, size, file_name) -> None:
|
||||
# main thread
|
||||
"""
|
||||
New transfer
|
||||
:param friend_number: number of friend who sent file
|
||||
:param file_number: file number
|
||||
:param size: file size in bytes
|
||||
:param file_name: file name without path
|
||||
"""
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None:
|
||||
LOG.info(f'incoming_file_handler Friend NULL friend_number={friend_number}')
|
||||
return
|
||||
auto = self._settings['allow_auto_accept'] and friend.tox_id in self._settings['auto_accept_from_friends']
|
||||
inline = False # ?is_inline(file_name) and self._settings['allow_inline']
|
||||
file_id = self._tox.file_get_file_id(friend_number, file_number)
|
||||
accepted = True
|
||||
if file_id in self._paused_file_transfers:
|
||||
LOG_INFO(f'incoming_file_handler paused friend_number={friend_number}')
|
||||
(path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[file_id]
|
||||
pos = start_position if os.path.exists(path) else 0
|
||||
if pos >= size:
|
||||
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL'])
|
||||
return
|
||||
self._tox.file_seek(friend_number, file_number, pos)
|
||||
self._file_transfers_message_service.add_incoming_transfer_message(
|
||||
friend, accepted, size, file_name, file_number)
|
||||
self.accept_transfer(path, friend_number, file_number, size, False, pos)
|
||||
elif inline and size < 1024 * 1024:
|
||||
LOG_INFO(f'incoming_file_handler small friend_number={friend_number}')
|
||||
self._file_transfers_message_service.add_incoming_transfer_message(
|
||||
friend, accepted, size, file_name, file_number)
|
||||
self.accept_transfer('', friend_number, file_number, size, True)
|
||||
elif auto:
|
||||
# accepted is really started
|
||||
LOG_INFO(f'incoming_file_handler auto friend_number={friend_number}')
|
||||
path = self._settings['auto_accept_path'] or util.curr_directory()
|
||||
self._file_transfers_message_service.add_incoming_transfer_message(
|
||||
friend, accepted, size, file_name, file_number)
|
||||
self.accept_transfer(path + '/' + file_name, friend_number, file_number, size)
|
||||
else:
|
||||
LOG_INFO(f'incoming_file_handler reject friend_number={friend_number}')
|
||||
accepted = False
|
||||
# FixME: need GUI ask
|
||||
# accepted is really started
|
||||
self._file_transfers_message_service.add_incoming_transfer_message(
|
||||
friend, accepted, size, file_name, file_number)
|
||||
|
||||
def cancel_transfer(self, friend_number, file_number, already_cancelled=False) -> None:
|
||||
"""
|
||||
Stop transfer
|
||||
:param friend_number: number of friend
|
||||
:param file_number: file number
|
||||
:param already_cancelled: was cancelled by friend
|
||||
"""
|
||||
# callback
|
||||
if (friend_number, file_number) in self._file_transfers:
|
||||
tr = self._file_transfers[(friend_number, file_number)]
|
||||
if not already_cancelled:
|
||||
tr.cancel()
|
||||
else:
|
||||
tr.cancelled()
|
||||
if (friend_number, file_number) in self._file_transfers:
|
||||
del tr
|
||||
del self._file_transfers[(friend_number, file_number)]
|
||||
elif not already_cancelled:
|
||||
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL'])
|
||||
|
||||
def cancel_not_started_transfer(self, friend_number, message_id) -> None:
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return None
|
||||
friend.delete_one_unsent_file(message_id)
|
||||
|
||||
def pause_transfer(self, friend_number, file_number, by_friend=False) -> None:
|
||||
"""
|
||||
Pause transfer with specified data
|
||||
"""
|
||||
tr = self._file_transfers[(friend_number, file_number)]
|
||||
tr.pause(by_friend)
|
||||
|
||||
def resume_transfer(self, friend_number, file_number, by_friend=False) -> None:
|
||||
"""
|
||||
Resume transfer with specified data
|
||||
"""
|
||||
tr = self._file_transfers[(friend_number, file_number)]
|
||||
if by_friend:
|
||||
tr.state = FILE_TRANSFER_STATE['RUNNING']
|
||||
else:
|
||||
tr.send_control(TOX_FILE_CONTROL['RESUME'])
|
||||
|
||||
def accept_transfer(self, path, friend_number, file_number, size, inline=False, from_position=0) -> None:
|
||||
"""
|
||||
:param path: path for saving
|
||||
:param friend_number: friend number
|
||||
:param file_number: file number
|
||||
:param size: file size
|
||||
:param inline: is inline image
|
||||
:param from_position: position for start
|
||||
"""
|
||||
path = self._generate_valid_path(path, from_position)
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return None
|
||||
if not inline:
|
||||
rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number, from_position)
|
||||
else:
|
||||
rt = ReceiveToBuffer(self._tox, friend_number, size, file_number)
|
||||
rt.set_transfer_finished_handler(self.transfer_finished)
|
||||
message = friend.get_message(lambda m: m.type == MESSAGE_TYPE['FILE_TRANSFER']
|
||||
and m.state in (FILE_TRANSFER_STATE['INCOMING_NOT_STARTED'],
|
||||
FILE_TRANSFER_STATE['RUNNING'])
|
||||
and m.file_number == file_number)
|
||||
rt.set_state_changed_handler(message.transfer_updated)
|
||||
self._file_transfers[(friend_number, file_number)] = rt
|
||||
rt.send_control(TOX_FILE_CONTROL['RESUME'])
|
||||
if inline:
|
||||
self._insert_inline_before[(friend_number, file_number)] = message.message_id
|
||||
|
||||
def send_screenshot(self, data, friend_number) -> None:
|
||||
"""
|
||||
Send screenshot
|
||||
:param data: raw data - png format
|
||||
:param friend_number: friend number
|
||||
"""
|
||||
self.send_inline(data, 'toxygen_inline.png', friend_number)
|
||||
|
||||
def send_sticker(self, path, friend_number) -> None:
|
||||
with open(path, 'rb') as fl:
|
||||
data = fl.read()
|
||||
self.send_inline(data, 'sticker.png', friend_number)
|
||||
|
||||
def send_inline(self, data, file_name, friend_number, is_resend=False) -> None:
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None:
|
||||
LOG_WARN("fsend_inline Error friend is None file_name: {file_name}")
|
||||
return
|
||||
if friend.status is None and not is_resend:
|
||||
self._file_transfers_message_service.add_unsent_file_message(friend, file_name, data)
|
||||
return
|
||||
elif friend.status is None and is_resend:
|
||||
LOG_WARN("fsend_inline Error friend.status is None file_name: {file_name}")
|
||||
return
|
||||
st = SendFromBuffer(self._tox, friend.number, data, file_name)
|
||||
self._send_file_add_set_handlers(st, friend, file_name, True)
|
||||
|
||||
def send_file(self, path, friend_number, is_resend=False, file_id=None) -> None:
|
||||
"""
|
||||
Send file to current active friend
|
||||
:param path: file path
|
||||
:param friend_number: friend_number
|
||||
:param is_resend: is 'offline' message
|
||||
:param file_id: file id of transfer
|
||||
"""
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return None
|
||||
if friend.status is None and not is_resend:
|
||||
self._file_transfers_message_service.add_unsent_file_message(friend, path, None)
|
||||
return
|
||||
elif friend.status is None and is_resend:
|
||||
LOG_WARN('Error in sending')
|
||||
return
|
||||
st = SendTransfer(path, self._tox, friend_number, TOX_FILE_KIND['DATA'], file_id)
|
||||
file_name = os.path.basename(path)
|
||||
self._send_file_add_set_handlers(st, friend, file_name)
|
||||
|
||||
def incoming_chunk(self, friend_number, file_number, position, data) -> None:
|
||||
"""
|
||||
Incoming chunk
|
||||
"""
|
||||
self._file_transfers[(friend_number, file_number)].write_chunk(position, data)
|
||||
|
||||
def outgoing_chunk(self, friend_number, file_number, position, size) -> None:
|
||||
"""
|
||||
Outgoing chunk
|
||||
"""
|
||||
self._file_transfers[(friend_number, file_number)].send_chunk(position, size)
|
||||
|
||||
def transfer_finished(self, friend_number, file_number) -> None:
|
||||
transfer = self._file_transfers[(friend_number, file_number)]
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return None
|
||||
t = type(transfer)
|
||||
if t is ReceiveAvatar:
|
||||
friend.load_avatar()
|
||||
elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings['allow_inline']): # inline image
|
||||
LOG.debug('inline')
|
||||
inline = InlineImageMessage(transfer.data)
|
||||
message_id = self._insert_inline_before[(friend_number, file_number)]
|
||||
del self._insert_inline_before[(friend_number, file_number)]
|
||||
if friend is None: return None
|
||||
index = friend.insert_inline(message_id, inline)
|
||||
self._file_transfers_message_service.add_inline_message(transfer, index)
|
||||
del self._file_transfers[(friend_number, file_number)]
|
||||
|
||||
def send_files(self, friend_number:int) -> None:
|
||||
try:
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return
|
||||
friend.remove_invalid_unsent_files()
|
||||
files = friend.get_unsent_files()
|
||||
for fl in files:
|
||||
data, path = fl.data, fl.path
|
||||
if data is not None:
|
||||
self.send_inline(data, path, friend_number, True)
|
||||
else:
|
||||
self.send_file(path, friend_number, True)
|
||||
friend.clear_unsent_files()
|
||||
for key in self._paused_file_transfers.keys():
|
||||
# RuntimeError: dictionary changed size during iteration
|
||||
(path, ft_friend_number, is_incoming, start_position) = self._paused_file_transfers[key]
|
||||
if not os.path.exists(path):
|
||||
del self._paused_file_transfers[key]
|
||||
elif ft_friend_number == friend_number and not is_incoming:
|
||||
self.send_file(path, friend_number, True, key)
|
||||
del self._paused_file_transfers[key]
|
||||
except Exception as ex:
|
||||
LOG_ERROR('send_files EXCEPTION in file sending: ' + str(ex))
|
||||
|
||||
def friend_exit(self, friend_number:int) -> None:
|
||||
# RuntimeError: dictionary changed size during iteration
|
||||
lMayChangeDynamically = self._file_transfers.copy()
|
||||
for friend_num, file_num in lMayChangeDynamically:
|
||||
if friend_num != friend_number:
|
||||
continue
|
||||
if (friend_num, file_num) not in self._file_transfers:
|
||||
continue
|
||||
ft = self._file_transfers[(friend_num, file_num)]
|
||||
if type(ft) is SendTransfer:
|
||||
try:
|
||||
file_id = ft.file_id
|
||||
except Exception as e:
|
||||
LOG_WARN("friend_exit SendTransfer Error getting file_id: {e}")
|
||||
# drop through
|
||||
else:
|
||||
self._paused_file_transfers[file_id] = [ft.path, friend_num, False, -1]
|
||||
elif type(ft) is ReceiveTransfer and ft.state != FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']:
|
||||
try:
|
||||
file_id = ft.file_id
|
||||
except Exception as e:
|
||||
LOG_WARN("friend_exit ReceiveTransfer Error getting file_id: {e}")
|
||||
# drop through
|
||||
else:
|
||||
self._paused_file_transfers[file_id] = [ft.path, friend_num, True, ft.total_size()]
|
||||
self.cancel_transfer(friend_num, file_num, True)
|
||||
|
||||
# Avatars support
|
||||
|
||||
def send_avatar(self, friend_number, avatar_path=None) -> None:
|
||||
"""
|
||||
:param friend_number: number of friend who should get new avatar
|
||||
:param avatar_path: path to avatar or None if reset
|
||||
"""
|
||||
return
|
||||
if (avatar_path, friend_number,) in self.lBlockAvatars:
|
||||
return
|
||||
if friend_number is None:
|
||||
LOG_WARN(f"send_avatar friend_number NULL {friend_number}")
|
||||
return
|
||||
if avatar_path and type(avatar_path) != str:
|
||||
LOG_WARN(f"send_avatar avatar_path type {type(avatar_path)}")
|
||||
return
|
||||
LOG_INFO(f"send_avatar avatar_path={avatar_path} friend_number={friend_number}")
|
||||
try:
|
||||
# self NOT missing - who's self?
|
||||
sa = SendAvatar(avatar_path, self._tox, friend_number)
|
||||
LOG_INFO(f"send_avatar avatar_path={avatar_path} sa={sa}")
|
||||
self._file_transfers[(friend_number, sa.file_number)] = sa
|
||||
except Exception as e:
|
||||
# ArgumentError('This client is currently not connected to the friend.')
|
||||
LOG_WARN(f"send_avatar EXCEPTION {e}")
|
||||
self.lBlockAvatars.append( (avatar_path, friend_number,) )
|
||||
|
||||
def incoming_avatar(self, friend_number, file_number, size) -> None:
|
||||
"""
|
||||
Friend changed avatar
|
||||
:param friend_number: friend number
|
||||
:param file_number: file number
|
||||
:param size: size of avatar or 0 (default avatar)
|
||||
"""
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return
|
||||
ra = ReceiveAvatar(friend.get_contact_avatar_path(), self._tox, friend_number, size, file_number)
|
||||
if ra.state != FILE_TRANSFER_STATE['CANCELLED']:
|
||||
self._file_transfers[(friend_number, file_number)] = ra
|
||||
ra.set_transfer_finished_handler(self.transfer_finished)
|
||||
elif not size:
|
||||
friend.reset_avatar(self._settings['identicons'])
|
||||
|
||||
def _send_avatar_to_contacts(self, _) -> None:
|
||||
# from a callback
|
||||
friends = self._get_all_friends()
|
||||
for friend in filter(self._is_friend_online, friends):
|
||||
self.send_avatar(friend.number)
|
||||
|
||||
# Private methods
|
||||
|
||||
def _is_friend_online(self, friend_number:int) -> bool:
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
if friend is None: return None
|
||||
|
||||
return friend.status is not None
|
||||
|
||||
def _get_friend_by_number(self, friend_number:int):
|
||||
return self._contact_provider.get_friend_by_number(friend_number)
|
||||
|
||||
def _get_all_friends(self):
|
||||
return self._contact_provider.get_all_friends()
|
||||
|
||||
def _send_file_add_set_handlers(self, st, friend, file_name, inline=False):
|
||||
st.set_transfer_finished_handler(self.transfer_finished)
|
||||
file_number = st.get_file_number()
|
||||
self._file_transfers[(friend.number, file_number)] = st
|
||||
tm = self._file_transfers_message_service.add_outgoing_transfer_message(friend, st.size, file_name, file_number)
|
||||
st.set_state_changed_handler(tm.transfer_updated)
|
||||
if inline:
|
||||
self._insert_inline_before[(friend.number, file_number)] = tm.message_id
|
||||
|
||||
@staticmethod
|
||||
def _generate_valid_path(path, from_position):
|
||||
path, file_name = os.path.split(path)
|
||||
new_file_name, i = file_name, 1
|
||||
if not from_position:
|
||||
while os.path.isfile(join_path(path, new_file_name)): # file with same name already exists
|
||||
if '.' in file_name: # has extension
|
||||
d = file_name.rindex('.')
|
||||
else: # no extension
|
||||
d = len(file_name)
|
||||
new_file_name = file_name[:d] + ' ({})'.format(i) + file_name[d:]
|
||||
i += 1
|
||||
path = join_path(path, new_file_name)
|
||||
|
||||
return path
|
@ -0,0 +1,94 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from messenger.messenger import *
|
||||
import utils.util as util
|
||||
from file_transfers.file_transfers import *
|
||||
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+__name__)
|
||||
|
||||
from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
|
||||
|
||||
class FileTransfersMessagesService:
|
||||
|
||||
def __init__(self, contacts_manager, messages_items_factory, profile, main_screen):
|
||||
self._contacts_manager = contacts_manager
|
||||
self._messages_items_factory = messages_items_factory
|
||||
self._profile = profile
|
||||
self._messages = main_screen.messages
|
||||
|
||||
def add_incoming_transfer_message(self, friend, accepted, size, file_name, file_number):
|
||||
assert friend
|
||||
author = MessageAuthor(friend.name, MESSAGE_AUTHOR['FRIEND'])
|
||||
# accepted is really started
|
||||
status = FILE_TRANSFER_STATE['RUNNING'] if accepted else FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']
|
||||
tm = TransferMessage(author, util.get_unix_time(), status, size, file_name, friend.number, file_number)
|
||||
|
||||
if self._is_friend_active(friend.number):
|
||||
self._create_file_transfer_item(tm)
|
||||
self._messages.scrollToBottom()
|
||||
else:
|
||||
friend.actions = True
|
||||
|
||||
friend.append_message(tm)
|
||||
|
||||
return tm
|
||||
|
||||
def add_outgoing_transfer_message(self, friend, size, file_name, file_number):
|
||||
assert friend
|
||||
author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME'])
|
||||
status = FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
|
||||
tm = TransferMessage(author, util.get_unix_time(), status, size, file_name, friend.number, file_number)
|
||||
|
||||
if self._is_friend_active(friend.number):
|
||||
self._create_file_transfer_item(tm)
|
||||
self._messages.scrollToBottom()
|
||||
|
||||
friend.append_message(tm)
|
||||
|
||||
return tm
|
||||
|
||||
def add_inline_message(self, transfer, index) -> None:
|
||||
"""callback"""
|
||||
if not self._is_friend_active(transfer.friend_number):
|
||||
return
|
||||
if transfer is None or not hasattr(transfer, 'data') or \
|
||||
not transfer.data:
|
||||
LOG_ERROR(f"add_inline_message empty data")
|
||||
return
|
||||
count = self._messages.count()
|
||||
if count + index + 1 >= 0:
|
||||
# assumes .data
|
||||
self._create_inline_item(transfer, count + index + 1)
|
||||
|
||||
def add_unsent_file_message(self, friend, file_path, data):
|
||||
assert friend
|
||||
author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['ME'])
|
||||
size = os.path.getsize(file_path) if data is None else len(data)
|
||||
tm = UnsentFileMessage(file_path, data, util.get_unix_time(), author, size, friend.number)
|
||||
friend.append_message(tm)
|
||||
|
||||
if self._is_friend_active(friend.number):
|
||||
self._create_unsent_file_item(tm)
|
||||
self._messages.scrollToBottom()
|
||||
|
||||
return tm
|
||||
|
||||
# Private methods
|
||||
|
||||
def _is_friend_active(self, friend_number:int) -> bool:
|
||||
if not self._contacts_manager.is_active_a_friend():
|
||||
return False
|
||||
|
||||
return friend_number == self._contacts_manager.get_active_number()
|
||||
|
||||
def _create_file_transfer_item(self, tm):
|
||||
return self._messages_items_factory.create_file_transfer_item(tm)
|
||||
|
||||
def _create_inline_item(self, data, position):
|
||||
return self._messages_items_factory.create_inline_item(data, False, position)
|
||||
|
||||
def _create_unsent_file_item(self, tm):
|
||||
return self._messages_items_factory.create_unsent_file_item(tm)
|
@ -1,75 +0,0 @@
|
||||
import contact
|
||||
from messages import *
|
||||
import os
|
||||
|
||||
|
||||
class Friend(contact.Contact):
|
||||
"""
|
||||
Friend in list of friends.
|
||||
"""
|
||||
|
||||
def __init__(self, message_getter, number, name, status_message, widget, tox_id):
|
||||
super().__init__(message_getter, number, name, status_message, widget, tox_id)
|
||||
self._receipts = 0
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# 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 remove_invalid_unsent_files(self):
|
||||
def is_valid(message):
|
||||
if type(message) is not UnsentFile:
|
||||
return True
|
||||
if message.get_data()[1] is not None:
|
||||
return True
|
||||
return os.path.exists(message.get_data()[0])
|
||||
self._corr = list(filter(is_valid, 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))
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# 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()
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Full status
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_full_status(self):
|
||||
return self._status_message
|
@ -1,49 +0,0 @@
|
||||
import contact
|
||||
import util
|
||||
from PyQt5 import QtGui, QtCore
|
||||
import toxcore_enums_and_consts as constants
|
||||
|
||||
|
||||
class GroupChat(contact.Contact):
|
||||
|
||||
def __init__(self, name, status_message, widget, tox, group_number):
|
||||
super().__init__(None, group_number, name, status_message, widget, None)
|
||||
self._tox = tox
|
||||
self.set_status(constants.TOX_USER_STATUS['NONE'])
|
||||
|
||||
def set_name(self, name):
|
||||
self._tox.group_set_title(self._number, name)
|
||||
super().set_name(name)
|
||||
|
||||
def send_message(self, message):
|
||||
self._tox.group_message_send(self._number, message.encode('utf-8'))
|
||||
|
||||
def new_title(self, title):
|
||||
super().set_name(title)
|
||||
|
||||
def load_avatar(self):
|
||||
path = util.curr_directory() + '/images/group.png'
|
||||
width = self._widget.avatar_label.width()
|
||||
pixmap = QtGui.QPixmap(path)
|
||||
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation))
|
||||
self._widget.avatar_label.repaint()
|
||||
|
||||
def remove_invalid_unsent_files(self):
|
||||
pass
|
||||
|
||||
def get_names(self):
|
||||
peers_count = self._tox.group_number_peers(self._number)
|
||||
names = []
|
||||
for i in range(peers_count):
|
||||
name = self._tox.group_peername(self._number, i)
|
||||
names.append(name)
|
||||
names = sorted(names, key=lambda n: n.lower())
|
||||
return names
|
||||
|
||||
def get_full_status(self):
|
||||
names = self.get_names()
|
||||
return '\n'.join(names)
|
||||
|
||||
def get_peer_name(self, peer_number):
|
||||
return self._tox.group_peername(self._number, peer_number)
|
@ -0,0 +1,23 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class GroupBan:
|
||||
|
||||
def __init__(self, ban_id, ban_target, ban_time):
|
||||
self._ban_id = ban_id
|
||||
self._ban_target = ban_target
|
||||
self._ban_time = ban_time
|
||||
|
||||
def get_ban_id(self):
|
||||
return self._ban_id
|
||||
|
||||
ban_id = property(get_ban_id)
|
||||
|
||||
def get_ban_target(self):
|
||||
return self._ban_target
|
||||
|
||||
ban_target = property(get_ban_target)
|
||||
|
||||
def get_ban_time(self):
|
||||
return self._ban_time
|
||||
|
||||
ban_time = property(get_ban_time)
|
@ -0,0 +1,23 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class GroupInvite:
|
||||
|
||||
def __init__(self, friend_public_key, chat_name, invite_data):
|
||||
self._friend_public_key = friend_public_key
|
||||
self._chat_name = chat_name
|
||||
self._invite_data = invite_data[:]
|
||||
|
||||
def get_friend_public_key(self):
|
||||
return self._friend_public_key
|
||||
|
||||
friend_public_key = property(get_friend_public_key)
|
||||
|
||||
def get_chat_name(self):
|
||||
return self._chat_name
|
||||
|
||||
chat_name = property(get_chat_name)
|
||||
|
||||
def get_invite_data(self):
|
||||
return self._invite_data[:]
|
||||
|
||||
invite_data = property(get_invite_data)
|
@ -0,0 +1,73 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
class GroupChatPeer:
|
||||
"""
|
||||
Represents peer in group chat.
|
||||
"""
|
||||
|
||||
def __init__(self, peer_id, name, status, role, public_key, is_current_user=False, is_muted=False, status_message=None):
|
||||
self._peer_id = peer_id
|
||||
self._name = name
|
||||
self._status = status
|
||||
self._status_message = status_message
|
||||
self._role = role
|
||||
self._public_key = public_key
|
||||
self._is_current_user = is_current_user
|
||||
self._is_muted = is_muted
|
||||
self._kind = 'grouppeer'
|
||||
|
||||
# Readonly properties
|
||||
|
||||
def get_id(self):
|
||||
return self._peer_id
|
||||
|
||||
id = property(get_id)
|
||||
|
||||
def get_public_key(self):
|
||||
return self._public_key
|
||||
|
||||
public_key = property(get_public_key)
|
||||
|
||||
def get_is_current_user(self):
|
||||
return self._is_current_user
|
||||
|
||||
is_current_user = property(get_is_current_user)
|
||||
|
||||
def get_status_message(self):
|
||||
return self._status_message
|
||||
|
||||
status_message = property(get_status_message)
|
||||
|
||||
# Read-write properties
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def set_name(self, name):
|
||||
self._name = name
|
||||
|
||||
name = property(get_name, set_name)
|
||||
|
||||
def get_status(self):
|
||||
return self._status
|
||||
|
||||
def set_status(self, status):
|
||||
self._status = status
|
||||
|
||||
status = property(get_status, set_status)
|
||||
|
||||
def get_role(self):
|
||||
return self._role
|
||||
|
||||
def set_role(self, role):
|
||||
self._role = role
|
||||
|
||||
role = property(get_role, set_role)
|
||||
|
||||
def get_is_muted(self):
|
||||
return self._is_muted
|
||||
|
||||
def set_is_muted(self, is_muted):
|
||||
self._is_muted = is_muted
|
||||
|
||||
is_muted = property(get_is_muted, set_is_muted)
|
@ -0,0 +1,291 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
import common.tox_save as tox_save
|
||||
import utils.ui as util_ui
|
||||
from groups.peers_list import PeersListGenerator
|
||||
from groups.group_invite import GroupInvite
|
||||
import toxygen_wrapper.toxcore_enums_and_consts as constants
|
||||
from toxygen_wrapper.toxcore_enums_and_consts import *
|
||||
from toxygen_wrapper.tox import UINT32_MAX
|
||||
|
||||
global LOG
|
||||
LOG = logging.getLogger('app.'+'gs')
|
||||
|
||||
class GroupsService(tox_save.ToxSave):
|
||||
|
||||
def __init__(self, tox, contacts_manager, contacts_provider, main_screen, widgets_factory_provider):
|
||||
super().__init__(tox)
|
||||
self._contacts_manager = contacts_manager
|
||||
self._contacts_provider = contacts_provider
|
||||
self._main_screen = main_screen
|
||||
self._peers_list_widget = main_screen.peers_list
|
||||
self._widgets_factory_provider = widgets_factory_provider
|
||||
self._group_invites = []
|
||||
self._screen = None
|
||||
# maybe just use self
|
||||
self._tox = tox
|
||||
|
||||
def set_tox(self, tox) -> None:
|
||||
super().set_tox(tox)
|
||||
for group in self._get_all_groups():
|
||||
group.set_tox(tox)
|
||||
|
||||
# Groups creation
|
||||
|
||||
def create_new_gc(self, name, privacy_state, nick, status) -> None:
|
||||
try:
|
||||
group_number = self._tox.group_new(privacy_state, name, nick, status)
|
||||
except Exception as e:
|
||||
LOG.error(f"create_new_gc {e}")
|
||||
return
|
||||
if group_number == -1:
|
||||
return
|
||||
|
||||
self._add_new_group_by_number(group_number)
|
||||
group = self._get_group_by_number(group_number)
|
||||
group.status = constants.TOX_USER_STATUS['NONE']
|
||||
self._contacts_manager.update_filtration()
|
||||
|
||||
def join_gc_by_id(self, chat_id, password, nick, status) -> None:
|
||||
try:
|
||||
group_number = self._tox.group_join(chat_id, password, nick, status)
|
||||
assert type(group_number) == int, group_number
|
||||
assert group_number < UINT32_MAX, group_number
|
||||
except Exception as e:
|
||||
# gui
|
||||
title = f"join_gc_by_id {chat_id}"
|
||||
util_ui.message_box(title +'\n' +str(e), title)
|
||||
LOG.error(f"_join_gc_via_id {e}")
|
||||
return
|
||||
LOG.debug(f"_join_gc_via_id {group_number}")
|
||||
self._add_new_group_by_number(group_number)
|
||||
group = self._get_group_by_number(group_number)
|
||||
try:
|
||||
assert group and hasattr(group, 'status')
|
||||
except Exception as e:
|
||||
# gui
|
||||
title = f"join_gc_by_id {chat_id}"
|
||||
util_ui.message_box(title +'\n' +str(e), title)
|
||||
LOG.error(f"_join_gc_via_id {e}")
|
||||
return
|
||||
group.status = constants.TOX_USER_STATUS['NONE']
|
||||
self._contacts_manager.update_filtration()
|
||||
|
||||
# Groups reconnect and leaving
|
||||
|
||||
def leave_group(self, group_number) -> None:
|
||||
if type(group_number) == int:
|
||||
self._tox.group_leave(group_number)
|
||||
self._contacts_manager.delete_group(group_number)
|
||||
|
||||
def disconnect_from_group(self, group_number) -> None:
|
||||
self._tox.group_disconnect(group_number)
|
||||
group = self._get_group_by_number(group_number)
|
||||
group.status = None
|
||||
self._clear_peers_list(group)
|
||||
|
||||
def reconnect_to_group(self, group_number) -> None:
|
||||
self._tox.group_reconnect(group_number)
|
||||
group = self._get_group_by_number(group_number)
|
||||
group.status = constants.TOX_USER_STATUS['NONE']
|
||||
self._clear_peers_list(group)
|
||||
|
||||
# Group invites
|
||||
|
||||
def invite_friend(self, friend_number, group_number) -> None:
|
||||
if self._tox.friend_get_connection_status(friend_number) == TOX_CONNECTION['NONE']:
|
||||
title = f"Error in group_invite_friend {friend_number}"
|
||||
e = f"Friend not connected friend_number={friend_number}"
|
||||
util_ui.message_box(title +'\n' +str(e), title)
|
||||
return
|
||||
|
||||
try:
|
||||
self._tox.group_invite_friend(group_number, friend_number)
|
||||
except Exception as e:
|
||||
title = f"Error in group_invite_friend {group_number} {friend_number}"
|
||||
util_ui.message_box(title +'\n' +str(e), title)
|
||||
|
||||
def process_group_invite(self, friend_number, group_name, invite_data) -> None:
|
||||
friend = self._get_friend_by_number(friend_number)
|
||||
# binary {invite_data}
|
||||
LOG.debug(f"process_group_invite {friend_number} {group_name}")
|
||||
invite = GroupInvite(friend.tox_id, group_name, invite_data)
|
||||
self._group_invites.append(invite)
|
||||
self._update_invites_button_state()
|
||||
|
||||
def accept_group_invite(self, invite, name, status, password) -> None:
|
||||
pk = invite.friend_public_key
|
||||
friend = self._get_friend_by_public_key(pk)
|
||||
LOG.debug(f"accept_group_invite {name}")
|
||||
self._join_gc_via_invite(invite.invite_data, friend.number, name, status, password)
|
||||
self._delete_group_invite(invite)
|
||||
self._update_invites_button_state()
|
||||
|
||||
def decline_group_invite(self, invite) -> None:
|
||||
self._delete_group_invite(invite)
|
||||
self._main_screen.update_gc_invites_button_state()
|
||||
|
||||
def get_group_invites(self):
|
||||
return self._group_invites[:]
|
||||
|
||||
group_invites = property(get_group_invites)
|
||||
|
||||
def get_group_invites_count(self):
|
||||
return len(self._group_invites)
|
||||
|
||||
group_invites_count = property(get_group_invites_count)
|
||||
|
||||
# Group info methods
|
||||
|
||||
def update_group_info(self, group):
|
||||
group.name = self._tox.group_get_name(group.number)
|
||||
group.status_message = self._tox.group_get_topic(group.number)
|
||||
|
||||
def set_group_topic(self, group) -> None:
|
||||
if not group.is_self_moderator_or_founder():
|
||||
return
|
||||
text = util_ui.tr('New topic for group "{}":'.format(group.name))
|
||||
title = util_ui.tr('Set group topic')
|
||||
topic, ok = util_ui.text_dialog(text, title, group.status_message)
|
||||
if not ok or not topic:
|
||||
return
|
||||
self._tox.group_set_topic(group.number, topic)
|
||||
group.status_message = topic
|
||||
|
||||
def show_group_management_screen(self, group) -> None:
|
||||
widgets_factory = self._get_widgets_factory()
|
||||
self._screen = widgets_factory.create_group_management_screen(group)
|
||||
self._screen.show()
|
||||
|
||||
def show_group_settings_screen(self, group) -> None:
|
||||
widgets_factory = self._get_widgets_factory()
|
||||
self._screen = widgets_factory.create_group_settings_screen(group)
|
||||
self._screen.show()
|
||||
|
||||
def set_group_password(self, group, password) -> None:
|
||||
if group.password == password:
|
||||
return
|
||||
self._tox.group_founder_set_password(group.number, password)
|
||||
group.password = password
|
||||
|
||||
def set_group_peers_limit(self, group, peers_limit) -> None:
|
||||
if group.peers_limit == peers_limit:
|
||||
return
|
||||
self._tox.group_founder_set_peer_limit(group.number, peers_limit)
|
||||
group.peers_limit = peers_limit
|
||||
|
||||
def set_group_privacy_state(self, group, privacy_state) -> None:
|
||||
is_private = privacy_state == constants.TOX_GROUP_PRIVACY_STATE['PRIVATE']
|
||||
if group.is_private == is_private:
|
||||
return
|
||||
self._tox.group_founder_set_privacy_state(group.number, privacy_state)
|
||||
group.is_private = is_private
|
||||
|
||||
# Peers list
|
||||
|
||||
def generate_peers_list(self) -> None:
|
||||
if not self._contacts_manager.is_active_a_group():
|
||||
return
|
||||
group = self._contacts_manager.get_curr_contact()
|
||||
PeersListGenerator().generate(group.peers, self, self._peers_list_widget, group.tox_id)
|
||||
|
||||
def peer_selected(self, chat_id, peer_id) -> None:
|
||||
widgets_factory = self._get_widgets_factory()
|
||||
group = self._get_group_by_public_key(chat_id)
|
||||
self_peer = group.get_self_peer()
|
||||
if self_peer.id != peer_id:
|
||||
self._screen = widgets_factory.create_peer_screen_window(group, peer_id)
|
||||
else:
|
||||
self._screen = widgets_factory.create_self_peer_screen_window(group)
|
||||
self._screen.show()
|
||||
|
||||
# Peers actions
|
||||
|
||||
def set_new_peer_role(self, group, peer, role) -> None:
|
||||
self._tox.group_mod_set_role(group.number, peer.id, role)
|
||||
peer.role = role
|
||||
self.generate_peers_list()
|
||||
|
||||
def toggle_ignore_peer(self, group, peer, ignore) -> None:
|
||||
self._tox.group_toggle_ignore(group.number, peer.id, ignore)
|
||||
peer.is_muted = ignore
|
||||
|
||||
def set_self_info(self, group, name, status) -> None:
|
||||
self._tox.group_self_set_name(group.number, name)
|
||||
self._tox.group_self_set_status(group.number, status)
|
||||
self_peer = group.get_self_peer()
|
||||
self_peer.name = name
|
||||
self_peer.status = status
|
||||
self.generate_peers_list()
|
||||
|
||||
# Bans support
|
||||
|
||||
def show_bans_list(self, group) -> None:
|
||||
return
|
||||
widgets_factory = self._get_widgets_factory()
|
||||
self._screen = widgets_factory.create_groups_bans_screen(group)
|
||||
self._screen.show()
|
||||
|
||||
def ban_peer(self, group, peer_id, ban_type) -> None:
|
||||
self._tox.group_mod_ban_peer(group.number, peer_id, ban_type)
|
||||
|
||||
def kick_peer(self, group, peer_id) -> None:
|
||||
self._tox.group_mod_remove_peer(group.number, peer_id)
|
||||
|
||||
def cancel_ban(self, group_number, ban_id) -> None:
|
||||
self._tox.group_mod_remove_ban(group_number, ban_id)
|
||||
|
||||
# Private methods
|
||||
|
||||
def _add_new_group_by_number(self, group_number) -> None:
|
||||
LOG.debug(f"_add_new_group_by_number group_number={group_number}")
|
||||
self._contacts_manager.add_group(group_number)
|
||||
|
||||
def _get_group_by_number(self, group_number):
|
||||
return self._contacts_provider.get_group_by_number(group_number)
|
||||
|
||||
def _get_group_by_public_key(self, public_key):
|
||||
return self._contacts_provider.get_group_by_public_key(public_key)
|
||||
|
||||
def _get_all_groups(self):
|
||||
return self._contacts_provider.get_all_groups()
|
||||
|
||||
def _get_friend_by_number(self, friend_number:int):
|
||||
return self._contacts_provider.get_friend_by_number(friend_number)
|
||||
|
||||
def _get_friend_by_public_key(self, public_key):
|
||||
return self._contacts_provider.get_friend_by_public_key(public_key)
|
||||
|
||||
def _clear_peers_list(self, group) -> None:
|
||||
group.remove_all_peers_except_self()
|
||||
self.generate_peers_list()
|
||||
|
||||
def _delete_group_invite(self, invite) -> None:
|
||||
if invite in self._group_invites:
|
||||
self._group_invites.remove(invite)
|
||||
|
||||
# status should be dropped
|
||||
def _join_gc_via_invite(self, invite_data, friend_number, nick, status='', password='') -> None:
|
||||
LOG.debug(f"_join_gc_via_invite friend_number={friend_number} nick={nick} datalen={len(invite_data)}")
|
||||
if nick is None:
|
||||
nick = ''
|
||||
if invite_data is None:
|
||||
invite_data = b''
|
||||
try:
|
||||
# status should be dropped
|
||||
group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, password=password)
|
||||
except Exception as e:
|
||||
LOG.error(f"_join_gc_via_invite ERROR {e}")
|
||||
return
|
||||
try:
|
||||
self._add_new_group_by_number(group_number)
|
||||
except Exception as e:
|
||||
LOG.error(f"_join_gc_via_invite group_number={group_number} {e}")
|
||||
return
|
||||
|
||||
def _update_invites_button_state(self) -> None:
|
||||
self._main_screen.update_gc_invites_button_state()
|
||||
|
||||
def _get_widgets_factory(self) -> None:
|
||||
return self._widgets_factory_provider.get_item()
|
@ -0,0 +1,101 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
from ui.group_peers_list import PeerItem, PeerTypeItem
|
||||
from toxygen_wrapper.toxcore_enums_and_consts import *
|
||||
from ui.widgets import *
|
||||
|
||||
# Builder
|
||||
|
||||
|
||||
class PeerListBuilder:
|
||||
|
||||
def __init__(self):
|
||||
self._peers = {}
|
||||
self._titles = {}
|
||||
self._index = 0
|
||||
self._handler = None
|
||||
|
||||
def with_click_handler(self, handler):
|
||||
self._handler = handler
|
||||
|
||||
return self
|
||||
|
||||
def with_title(self, title):
|
||||
self._titles[self._index] = title
|
||||
self._index += 1
|
||||
|
||||
return self
|
||||
|
||||
def with_peers(self, peers):
|
||||
for peer in peers:
|
||||
self._add_peer(peer)
|
||||
|
||||
return self
|
||||
|
||||
def build(self, list_widget):
|
||||
list_widget.clear()
|
||||
|
||||
for i in range(self._index):
|
||||
if i in self._peers:
|
||||
peer = self._peers[i]
|
||||
self._add_peer_item(peer, list_widget)
|
||||
else:
|
||||
title = self._titles[i]
|
||||
self._add_peer_type_item(title, list_widget)
|
||||
|
||||
def _add_peer_item(self, peer, parent):
|
||||
item = PeerItem(peer, self._handler, parent.width(), parent)
|
||||
self._add_item(parent, item)
|
||||
|
||||
def _add_peer_type_item(self, text, parent):
|
||||
item = PeerTypeItem(text, parent.width(), parent)
|
||||
self._add_item(parent, item)
|
||||
|
||||
@staticmethod
|
||||
def _add_item(parent, item):
|
||||
elem = QtWidgets.QListWidgetItem(parent)
|
||||
elem.setSizeHint(QtCore.QSize(parent.width(), item.height()))
|
||||
parent.addItem(elem)
|
||||
parent.setItemWidget(elem, item)
|
||||
|
||||
def _add_peer(self, peer):
|
||||
self._peers[self._index] = peer
|
||||
self._index += 1
|
||||
|
||||
# Generators
|
||||
|
||||
|
||||
class PeersListGenerator:
|
||||
|
||||
@staticmethod
|
||||
def generate(peers_list, groups_service, list_widget, chat_id):
|
||||
admin_title = util_ui.tr('Administrator')
|
||||
moderators_title = util_ui.tr('Moderators')
|
||||
users_title = util_ui.tr('Users')
|
||||
observers_title = util_ui.tr('Observers')
|
||||
|
||||
admins = list(filter(lambda p: p.role == TOX_GROUP_ROLE['FOUNDER'], peers_list))
|
||||
moderators = list(filter(lambda p: p.role == TOX_GROUP_ROLE['MODERATOR'], peers_list))
|
||||
users = list(filter(lambda p: p.role == TOX_GROUP_ROLE['USER'], peers_list))
|
||||
observers = list(filter(lambda p: p.role == TOX_GROUP_ROLE['OBSERVER'], peers_list))
|
||||
|
||||
builder = (PeerListBuilder()
|
||||
.with_click_handler(lambda peer_id: groups_service.peer_selected(chat_id, peer_id)))
|
||||
if len(admins):
|
||||
(builder
|
||||
.with_title(admin_title)
|
||||
.with_peers(admins))
|
||||
if len(moderators):
|
||||
(builder
|
||||
.with_title(moderators_title)
|
||||
.with_peers(moderators))
|
||||
if len(users):
|
||||
(builder
|
||||
.with_title(users_title)
|
||||
.with_peers(users))
|
||||
if len(observers):
|
||||
(builder
|
||||
.with_title(observers_title)
|
||||
.with_peers(observers))
|
||||
|
||||
builder.build(list_widget)
|
@ -1,215 +0,0 @@
|
||||
from sqlite3 import connect
|
||||
import settings
|
||||
from os import chdir
|
||||
import os.path
|
||||
from toxes import ToxES
|
||||
|
||||
|
||||
PAGE_SIZE = 42
|
||||
|
||||
TIMEOUT = 11
|
||||
|
||||
SAVE_MESSAGES = 250
|
||||
|
||||
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 = ToxES.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', timeout=TIMEOUT)
|
||||
cursor = db.cursor()
|
||||
cursor.execute('CREATE TABLE IF NOT EXISTS friends('
|
||||
' tox_id TEXT PRIMARY KEY'
|
||||
')')
|
||||
db.close()
|
||||
|
||||
def save(self):
|
||||
encr = ToxES.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 = ToxES.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', timeout=TIMEOUT)
|
||||
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:
|
||||
print('Database is locked!')
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def delete_friend_from_db(self, tox_id):
|
||||
chdir(settings.ProfileHelper.get_path())
|
||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('DELETE FROM friends WHERE tox_id=?;', (tox_id, ))
|
||||
cursor.execute('DROP TABLE id' + tox_id + ';')
|
||||
db.commit()
|
||||
except:
|
||||
print('Database is locked!')
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def friend_exists_in_db(self, tox_id):
|
||||
chdir(settings.ProfileHelper.get_path())
|
||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
||||
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', timeout=TIMEOUT)
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.executemany('INSERT INTO id' + tox_id + '(message, owner, unix_time, message_type) '
|
||||
'VALUES (?, ?, ?, ?);', messages_iter)
|
||||
db.commit()
|
||||
except:
|
||||
print('Database is locked!')
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def update_messages(self, tox_id, unsent_time):
|
||||
chdir(settings.ProfileHelper.get_path())
|
||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
||||
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:
|
||||
print('Database is locked!')
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def delete_message(self, tox_id, time):
|
||||
start, end = str(time - 0.01), str(time + 0.01)
|
||||
chdir(settings.ProfileHelper.get_path())
|
||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time < ' + end + ' AND unix_time > ' +
|
||||
start + ';')
|
||||
db.commit()
|
||||
except:
|
||||
print('Database is locked!')
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def delete_messages(self, tox_id):
|
||||
chdir(settings.ProfileHelper.get_path())
|
||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('DELETE FROM id' + tox_id + ';')
|
||||
db.commit()
|
||||
except:
|
||||
print('Database is locked!')
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def messages_getter(self, tox_id):
|
||||
return History.MessageGetter(self._name, tox_id)
|
||||
|
||||
class MessageGetter:
|
||||
|
||||
def __init__(self, name, tox_id):
|
||||
self._count = 0
|
||||
self._name = name
|
||||
self._tox_id = tox_id
|
||||
self._db = self._cursor = None
|
||||
|
||||
def connect(self):
|
||||
chdir(settings.ProfileHelper.get_path())
|
||||
self._db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
||||
self._cursor = self._db.cursor()
|
||||
self._cursor.execute('SELECT message, owner, unix_time, message_type FROM id' + self._tox_id +
|
||||
' ORDER BY unix_time DESC;')
|
||||
|
||||
def disconnect(self):
|
||||
self._db.close()
|
||||
|
||||
def get_one(self):
|
||||
self.connect()
|
||||
self.skip()
|
||||
data = self._cursor.fetchone()
|
||||
self._count += 1
|
||||
self.disconnect()
|
||||
return data
|
||||
|
||||
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
|
@ -0,0 +1,227 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
from sqlite3 import connect
|
||||
import os.path
|
||||
import utils.util as util
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger('h.database')
|
||||
|
||||
TIMEOUT = 11
|
||||
SAVE_MESSAGES = 500
|
||||
MESSAGE_AUTHOR = {
|
||||
'ME': 0,
|
||||
'FRIEND': 1,
|
||||
'NOT_SENT': 2,
|
||||
'GC_PEER': 3
|
||||
}
|
||||
CONTACT_TYPE = {
|
||||
'FRIEND': 0,
|
||||
'GC_PEER': 1,
|
||||
'GC_PEER_PRIVATE': 2
|
||||
}
|
||||
|
||||
|
||||
class Database:
|
||||
|
||||
def __init__(self, path, toxes):
|
||||
self._path = path
|
||||
self._toxes = toxes
|
||||
self._name = os.path.basename(path)
|
||||
|
||||
def open(self):
|
||||
path = self._path
|
||||
toxes = self._toxes
|
||||
if not os.path.exists(path):
|
||||
LOG.warn('Db not found: ' +path)
|
||||
return
|
||||
try:
|
||||
with open(path, 'rb') as fin:
|
||||
data = fin.read()
|
||||
except Exception as ex:
|
||||
LOG.error('Db reading error: ' +path +' ' +str(ex))
|
||||
raise
|
||||
try:
|
||||
if toxes.is_data_encrypted(data):
|
||||
data = toxes.pass_decrypt(data)
|
||||
with open(path, 'wb') as fout:
|
||||
fout.write(data)
|
||||
except Exception as ex:
|
||||
LOG.error('Db writing error: ' +path +' ' + str(ex))
|
||||
os.remove(path)
|
||||
LOG.info('Db opened: ' +path)
|
||||
|
||||
# Public methods
|
||||
|
||||
def save(self):
|
||||
if self._toxes.has_password():
|
||||
with open(self._path, 'rb') as fin:
|
||||
data = fin.read()
|
||||
data = self._toxes.pass_encrypt(bytes(data))
|
||||
with open(self._path, 'wb') as fout:
|
||||
fout.write(data)
|
||||
|
||||
def export(self, directory):
|
||||
new_path = util.join_path(directory, self._name)
|
||||
with open(self._path, 'rb') as fin:
|
||||
data = fin.read()
|
||||
if self._toxes.has_password():
|
||||
data = self._toxes.pass_encrypt(data)
|
||||
with open(new_path, 'wb') as fout:
|
||||
fout.write(data)
|
||||
LOG.info('Db exported: ' +new_path)
|
||||
|
||||
def add_friend_to_db(self, tox_id):
|
||||
db = self._connect()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('CREATE TABLE IF NOT EXISTS id' + tox_id + '('
|
||||
' id INTEGER PRIMARY KEY,'
|
||||
' author_name TEXT,'
|
||||
' message TEXT,'
|
||||
' author_type INTEGER,'
|
||||
' unix_time REAL,'
|
||||
' message_type INTEGER'
|
||||
')')
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.error("dd_friend_to_db " +self._name +f" Database exception! {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
LOG.debug(f"add_friend_to_db {tox_id}")
|
||||
|
||||
def delete_friend_from_db(self, tox_id):
|
||||
db = self._connect()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('DROP TABLE id' + tox_id + ';')
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.error("delete_friend_from_db " +self._name +f" Database exception! {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
LOG.debug(f"delete_friend_from_db {tox_id}")
|
||||
|
||||
def save_messages_to_db(self, tox_id, messages_iter):
|
||||
db = self._connect()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.executemany('INSERT INTO id' + tox_id +
|
||||
'(message, author_name, author_type, unix_time, message_type) ' +
|
||||
'VALUES (?, ?, ?, ?, ?);', messages_iter)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.error("save_messages_to_db" +self._name +f" Database exception! {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
LOG.debug(f"save_messages_to_db {tox_id}")
|
||||
|
||||
def update_messages(self, tox_id, message_id):
|
||||
db = self._connect()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('UPDATE id' + tox_id + ' SET author = 0 '
|
||||
'WHERE id = ' + str(message_id) + ' AND author = 2;')
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.error("update_messages" +self._name +f" Database exception! {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
LOG.debug(f"update_messages {tox_id}")
|
||||
|
||||
def delete_message(self, tox_id, unique_id):
|
||||
db = self._connect()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('DELETE FROM id' + tox_id + ' WHERE id = ' + str(unique_id) + ';')
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.error("delete_message" +self._name +f" Database exception! {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
LOG.debug(f"delete_message {tox_id}")
|
||||
|
||||
def delete_messages(self, tox_id):
|
||||
db = self._connect()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('DELETE FROM id' + tox_id + ';')
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.error("delete_messages" +self._name +f" Database exception! {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
LOG.debug(f"delete_messages {tox_id}")
|
||||
|
||||
def messages_getter(self, tox_id):
|
||||
self.add_friend_to_db(tox_id)
|
||||
|
||||
return Database.MessageGetter(self._path, tox_id)
|
||||
|
||||
# Messages loading
|
||||
|
||||
class MessageGetter:
|
||||
|
||||
def __init__(self, path, tox_id):
|
||||
self._count = 0
|
||||
self._path = path
|
||||
self._tox_id = tox_id
|
||||
self._db = self._cursor = None
|
||||
|
||||
def get_one(self):
|
||||
return self.get(1)
|
||||
|
||||
def get_all(self):
|
||||
self._connect()
|
||||
data = self._cursor.fetchall()
|
||||
self._disconnect()
|
||||
self._count = len(data)
|
||||
return data
|
||||
|
||||
def get(self, count):
|
||||
self._connect()
|
||||
self.skip()
|
||||
data = self._cursor.fetchmany(count)
|
||||
self._disconnect()
|
||||
self._count += len(data)
|
||||
return data
|
||||
|
||||
def skip(self):
|
||||
if self._count:
|
||||
self._cursor.fetchmany(self._count)
|
||||
|
||||
def delete_one(self):
|
||||
if self._count:
|
||||
self._count -= 1
|
||||
|
||||
def _connect(self):
|
||||
self._db = connect(self._path, timeout=TIMEOUT)
|
||||
self._cursor = self._db.cursor()
|
||||
self._cursor.execute('SELECT message, author_type, author_name, unix_time, message_type, id FROM id' +
|
||||
self._tox_id + ' ORDER BY unix_time DESC;')
|
||||
|
||||
def _disconnect(self):
|
||||
self._db.close()
|
||||
|
||||
# Private methods
|
||||
|
||||
def _connect(self):
|
||||
return connect(self._path, timeout=TIMEOUT)
|
@ -0,0 +1,141 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
from history.history_logs_generators import *
|
||||
|
||||
global LOG
|
||||
import logging
|
||||
LOG = logging.getLogger('app.db')
|
||||
|
||||
class History:
|
||||
|
||||
def __init__(self, contact_provider, db, settings, main_screen, messages_items_factory):
|
||||
self._contact_provider = contact_provider
|
||||
self._db = db
|
||||
self._settings = settings
|
||||
self._messages = main_screen.messages
|
||||
self._messages_items_factory = messages_items_factory
|
||||
self._is_loading = False
|
||||
self._contacts_manager = None
|
||||
|
||||
def __del__(self):
|
||||
del self._db
|
||||
|
||||
def set_contacts_manager(self, contacts_manager):
|
||||
self._contacts_manager = contacts_manager
|
||||
|
||||
# History support
|
||||
|
||||
def save_history(self):
|
||||
"""
|
||||
Save history to db
|
||||
"""
|
||||
# me a mistake? was _db not _history
|
||||
if self._settings['save_history']:
|
||||
for friend in self._contact_provider.get_all_friends():
|
||||
self._db.add_friend_to_db(friend.tox_id)
|
||||
if not self._settings['save_unsent_only']:
|
||||
messages = friend.get_corr_for_saving()
|
||||
else:
|
||||
messages = friend.get_unsent_messages_for_saving()
|
||||
self._db.delete_messages(friend.tox_id)
|
||||
messages = map(lambda m: (m.text, m.author.name, m.author.type, m.time, m.type), messages)
|
||||
self._db.save_messages_to_db(friend.tox_id, messages)
|
||||
|
||||
self._db.save()
|
||||
|
||||
def clear_history(self, friend, save_unsent=False):
|
||||
"""
|
||||
Clear chat history
|
||||
"""
|
||||
friend.clear_corr(save_unsent)
|
||||
self._db.delete_friend_from_db(friend.tox_id)
|
||||
|
||||
def export_history(self, contact, as_text=True):
|
||||
extension = 'txt' if as_text else 'html'
|
||||
file_name, _ = util_ui.save_file_dialog(util_ui.tr('Choose file name'), extension)
|
||||
|
||||
if not file_name:
|
||||
return
|
||||
|
||||
if not file_name.endswith('.' + extension):
|
||||
file_name += '.' + extension
|
||||
|
||||
history = self.generate_history(contact, as_text)
|
||||
assert history
|
||||
with open(file_name, 'wt') as fl:
|
||||
fl.write(history)
|
||||
LOG.info(f"wrote history to {file_name}")
|
||||
|
||||
def delete_message(self, message):
|
||||
contact = self._contacts_manager.get_curr_contact()
|
||||
if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']):
|
||||
if message.is_saved():
|
||||
self._db.delete_message(contact.tox_id, message.id)
|
||||
contact.delete_message(message.message_id)
|
||||
|
||||
def load_history(self, friend):
|
||||
"""
|
||||
Tries to load next part of messages
|
||||
"""
|
||||
if self._is_loading:
|
||||
return
|
||||
self._is_loading = True
|
||||
friend.load_corr(False)
|
||||
messages = friend.get_corr()
|
||||
if not messages:
|
||||
self._is_loading = False
|
||||
return
|
||||
messages.reverse()
|
||||
messages = messages[self._messages.count():self._messages.count() + PAGE_SIZE]
|
||||
for message in messages:
|
||||
message_type = message.get_type()
|
||||
if message_type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']): # text message
|
||||
self._create_message_item(message)
|
||||
elif message_type == MESSAGE_TYPE['FILE_TRANSFER']: # file transfer
|
||||
if message.state == FILE_TRANSFER_STATE['UNSENT']:
|
||||
self._create_unsent_file_item(message)
|
||||
else:
|
||||
self._create_file_transfer_item(message)
|
||||
elif message_type == MESSAGE_TYPE['INLINE']: # inline image
|
||||
self._create_inline_item(message)
|
||||
else: # info message
|
||||
self._create_message_item(message)
|
||||
self._is_loading = False
|
||||
|
||||
def get_message_getter(self, friend_public_key):
|
||||
self._db.add_friend_to_db(friend_public_key)
|
||||
|
||||
return self._db.messages_getter(friend_public_key)
|
||||
|
||||
def delete_history(self, friend):
|
||||
self._db.delete_friend_from_db(friend.tox_id)
|
||||
|
||||
def add_friend_to_db(self, tox_id):
|
||||
self._db.add_friend_to_db(tox_id)
|
||||
|
||||
@staticmethod
|
||||
def generate_history(contact, as_text=True, _range=None):
|
||||
if _range is None:
|
||||
contact.load_all_corr()
|
||||
corr = contact.get_corr()
|
||||
elif _range[1] + 1:
|
||||
corr = contact.get_corr()[_range[0]:_range[1] + 1]
|
||||
else:
|
||||
corr = contact.get_corr()[_range[0]:]
|
||||
|
||||
generator = TextHistoryGenerator(corr, contact.name) if as_text else HtmlHistoryGenerator(corr, contact.name)
|
||||
|
||||
return generator.generate()
|
||||
|
||||
# Items creation
|
||||
|
||||
def _create_message_item(self, message):
|
||||
return self._messages_items_factory.create_message_item(message, False)
|
||||
|
||||
def _create_unsent_file_item(self, message):
|
||||
return self._messages_items_factory.create_unsent_file_item(message, False)
|
||||
|
||||
def _create_file_transfer_item(self, message):
|
||||
return self._messages_items_factory.create_file_transfer_item(message, False)
|
||||
|
||||
def _create_inline_item(self, message):
|
||||
return self._messages_items_factory.create_inline_item(message, False)
|
@ -0,0 +1,50 @@
|
||||
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
|
||||
|
||||
import utils.util as util
|
||||
from messenger.messages import *
|
||||
|
||||
|
||||
class HistoryLogsGenerator:
|
||||
|
||||
def __init__(self, history, contact_name):
|
||||
self._history = history
|
||||
self._contact_name = contact_name
|
||||
|
||||
def generate(self):
|
||||
return str()
|
||||
|
||||
@staticmethod
|
||||
def _get_message_time(message):
|
||||
return util.convert_time(message.time) if message.author.type != MESSAGE_AUTHOR['NOT_SENT'] else 'Unsent'
|
||||
|
||||
|
||||
class HtmlHistoryGenerator(HistoryLogsGenerator):
|
||||
|
||||
def __init__(self, history, contact_name):
|
||||
super().__init__(history, contact_name)
|
||||
|
||||
def generate(self):
|
||||
arr = []
|
||||
for message in self._history:
|
||||
if type(message) is TextMessage:
|
||||
x = '[{}] <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)
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 329 B After Width: | Height: | Size: 433 B |
Before Width: | Height: | Size: 609 B After Width: | Height: | Size: 556 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 816 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 119 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 816 B |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 461 B |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 911 B |
Before Width: | Height: | Size: 231 B After Width: | Height: | Size: 400 B |
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 816 B |