38 Commits

Author SHA1 Message Date
cc37485fc0 Removed dns test because toxme is down 2020-05-23 18:37:07 +03:00
028de8176b Used pyqt5 v5.14 2020-05-23 18:27:34 +03:00
2e285a8400 Used pip3 explicitly 2020-05-23 18:23:50 +03:00
cf912b137d Fixed travis build 2020-05-23 18:15:54 +03:00
5019535c0d Fixed bug with loading old messages for groups 2020-03-21 22:05:17 +03:00
1554d9e53a Fixed bug with sending faux offline inlines 2020-03-14 15:33:57 +03:00
a984b624b5 Added ability to paste image 2020-03-04 00:34:10 +03:00
2aea5df33c proper fix for gc history 2018-09-15 22:50:25 +03:00
1fa13db4e4 fixed bug with history loading and qtox screenshots autoaccept 2018-09-15 22:29:30 +03:00
3582722faa fixed 2 bugs with gc 2018-09-13 23:23:25 +03:00
74396834cf Calls bug fixes 2018-04-13 20:12:27 +03:00
ce84cc526b drag n drop fixes 2018-04-08 11:48:40 +03:00
98cc288bcd fix for ipv6 setting (#59) 2018-02-05 23:32:33 +03:00
9b5d768819 reconnect bug fixed 2018-01-30 20:36:59 +03:00
762eb89a46 clickable links in about dialog 2018-01-30 20:24:36 +03:00
b428bd54c4 export history fixed 2018-01-30 18:45:55 +03:00
f76a1c0fbe manifest.in updated 2018-01-27 19:53:07 +03:00
bb2a857ecf use opencv-python module on linux 2018-01-26 18:43:19 +03:00
62c5df751d fix and translations update 2018-01-24 23:42:03 +03:00
55a127a820 ability to use nodes from tox.chat added 2018-01-24 22:45:58 +03:00
32055050ee hide tray icon on exit 2017-11-05 12:13:28 +03:00
a6633f1e77 minor video changes 2017-10-24 21:43:12 +03:00
23b55522ba file transfer cancelling fix 2017-10-22 16:36:46 +03:00
5a5b0e9069 desktop sharing bug fix 2017-10-08 00:39:08 +03:00
24c8b18f7e minor fixes 2017-08-30 22:20:31 +03:00
3ddb7470fc v0.4.0 2017-07-21 18:39:10 +03:00
80b0ea4f0e history for gc fixes 2017-07-20 23:51:40 +03:00
6efb1790bb notifications fix 2017-07-19 19:39:56 +03:00
d5d1e616ba translations update and bug fix 2017-07-19 00:14:41 +03:00
1ea919bdc2 tab && bug fix 2017-07-18 23:36:40 +03:00
65167de1fe group notifications and bug fixes 2017-07-18 21:36:14 +03:00
db519e2608 bug fix and version++ 2017-07-17 22:27:52 +03:00
19893c5c28 chat menu 2017-07-17 22:15:29 +03:00
8e6d37e23c minimal working functionality 2017-07-17 21:53:35 +03:00
aae71d081f base backend for gc 2017-07-17 01:11:09 +03:00
9c129e925b base gc class, callbacks part1 2017-07-16 22:51:20 +03:00
87392ea95a wrapper for old gc (gen) 2017-07-16 20:02:33 +03:00
1bbd9a629c video calls fix 2017-07-15 23:11:49 +03:00
34 changed files with 1489 additions and 820 deletions

View File

@ -12,12 +12,13 @@ before_install:
- sudo apt-get install -y checkinstall build-essential - sudo apt-get install -y checkinstall build-essential
- sudo apt-get install portaudio19-dev - sudo apt-get install portaudio19-dev
- sudo apt-get install libsecret-1-dev - sudo apt-get install libsecret-1-dev
- sudo apt-get install libconfig-dev libvpx-dev check -qq - sudo apt-get install libconfig-dev libvpx-dev check -qq
install: install:
- pip install sip - pip3 install sip
- pip install pyqt5 - pip3 install pyaudio
- pip install pyaudio - pip3 install pyqt5==5.14
- pip install opencv-python - pip3 install opencv-python
before_script: before_script:
# Opus # Opus
- wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz - wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz

View File

@ -16,3 +16,4 @@ include toxygen/styles/*.qss
include toxygen/translations/*.qm include toxygen/translations/*.qm
include toxygen/libs/libtox.dll include toxygen/libs/libtox.dll
include toxygen/libs/libsodium.a include toxygen/libs/libsodium.a
include toxygen/nodes.json

View File

@ -18,6 +18,7 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
- File transfers - File transfers
- Audio calls - Audio calls
- Video calls - Video calls
- Group chats
- Plugins support - Plugins support
- Desktop sharing - Desktop sharing
- Chat history - Chat history

View File

@ -17,7 +17,7 @@ Run app using ``toxygen`` command.
2. Install PortAudio: 2. Install PortAudio:
``sudo apt-get install portaudio19-dev`` ``sudo apt-get install portaudio19-dev``
3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5`` 3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5``
4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo apt-get install python3-opencv`` 4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python``
5. Install toxygen: 5. Install toxygen:
``sudo pip3 install toxygen`` ``sudo pip3 install toxygen``
6. Run toxygen using ``toxygen`` command. 6. Run toxygen using ``toxygen`` command.
@ -63,7 +63,7 @@ Optional: install toxygen using setup.py: ``python setup.py install``
4. Install PyAudio: 4. Install PyAudio:
``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``sudo pip3 install pyaudio``) ``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``sudo pip3 install pyaudio``)
5. Install NumPy: ``sudo pip3 install numpy`` 5. Install NumPy: ``sudo pip3 install numpy``
6. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo apt-get install python3-opencv`` 6. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo pip3 install opencv-python``
7. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip) 7. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip)
8. Unpack archive 8. Unpack archive
9. Run app: 9. Run app:

View File

@ -1,6 +1,6 @@
# Plugins # Plugins
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 - 3.6 module (.py file) and directory with plugin's data which provide some additional functionality. Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.5 - 3.6 module (.py file) and directory with plugin's data which provide some additional functionality.
# How to write plugin # How to write plugin

View File

@ -8,6 +8,6 @@ Animated smileys (.gif) are supported too.
# Stickers # Stickers
Sticker is inline image. If you want to create your own smiley pack, create directory in src/stickers/ and place your stickers there. Sticker is inline image. If you want to create your own sticker pack, create directory in src/stickers/ and place your stickers there.
Users can import smileys and stickers using menu: Settings -> Interface Users can import smileys and stickers using menu: Settings -> Interface

View File

@ -25,6 +25,10 @@ else:
import numpy import numpy
except ImportError: except ImportError:
MODULES.append('numpy') MODULES.append('numpy')
try:
import cv2
except ImportError:
MODULES.append('opencv-python')
class InstallScript(install): class InstallScript(install):
@ -48,6 +52,7 @@ class InstallScript(install):
except: except:
pass pass
setup(name='Toxygen', setup(name='Toxygen',
version=version, version=version,
description='Toxygen - Tox client', description='Toxygen - Tox client',

View File

@ -36,21 +36,6 @@ class TestProfileHelper:
assert os.path.exists(path + 'avatars/') 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: class TestEncryption:
def test_encr_decr(self): def test_encr_decr(self):

View File

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

View File

@ -33,6 +33,7 @@ class Invoker(QtCore.QObject):
event.fn(*event.args, **event.kwargs) event.fn(*event.args, **event.kwargs)
return True return True
_invoker = Invoker() _invoker = Invoker()
@ -66,6 +67,7 @@ class FileTransfersThread(threading.Thread):
except Exception as ex: except Exception as ex:
util.log('Exception in _thread: ' + str(ex)) util.log('Exception in _thread: ' + str(ex))
_thread = FileTransfersThread() _thread = FileTransfersThread()
@ -346,7 +348,6 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u
width // 2 width // 2 width // 2 width // 2
It can be created from initial y, u, v using slices It can be created from initial y, u, v using slices
For more info see callback_video_receive_frame docs
""" """
try: try:
y_size = abs(max(width, abs(ystride))) y_size = abs(max(width, abs(ystride)))
@ -375,6 +376,55 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u
except Exception as ex: except Exception as ex:
print(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 # Callbacks - initialization
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -411,3 +461,9 @@ def init_callbacks(tox, window, tray):
tox.callback_friend_lossless_packet(lossless_packet, 0) tox.callback_friend_lossless_packet(lossless_packet, 0)
tox.callback_friend_lossy_packet(lossy_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)

View File

@ -63,7 +63,7 @@ class Call:
return self._out_video return self._out_video
def set_out_video(self, value): def set_out_video(self, value):
self._in_video = value self._out_video = value
out_video = property(get_out_video, set_out_video) out_video = property(get_out_video, set_out_video)
@ -144,8 +144,8 @@ class AV:
call = self._calls[friend_number] call = self._calls[friend_number]
call.is_active = True call.is_active = True
call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A'] call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A'] > 0
call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V'] call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V'] > 0
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio: if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio:
self.start_audio_thread() self.start_audio_thread()
@ -153,6 +153,9 @@ class AV:
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video: if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video:
self.start_video_thread() self.start_video_thread()
def is_video_call(self, number):
return number in self and self._calls[number].in_video
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Threads # Threads
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -281,10 +284,10 @@ class AV:
try: try:
y, u, v = self.convert_bgr_to_yuv(frame) y, u, v = self.convert_bgr_to_yuv(frame)
self._toxav.video_send_frame(friend_num, width, height, y, u, v) self._toxav.video_send_frame(friend_num, width, height, y, u, v)
except Exception as e: except:
print(e) pass
except Exception as e: except:
print(e) pass
time.sleep(0.01) time.sleep(0.01)

View File

@ -61,6 +61,8 @@ class Contact(basecontact.BaseContact):
""" """
Get all chat history from db for current friend Get all chat history from db for current friend
""" """
if self._message_getter is None:
return
data = list(self._message_getter.get_all()) data = list(self._message_getter.get_all())
if data is not None and len(data): if data is not None and len(data):
data.reverse() data.reverse()
@ -124,7 +126,7 @@ class Contact(basecontact.BaseContact):
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def delete_message(self, time): def delete_message(self, time):
elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0] elem = list(filter(lambda x: type(x) in (TextMessage, GroupChatMessage) and x.get_data()[2] == time, self._corr))[0]
tmp = list(filter(lambda x: x.get_type() <= 1, self._corr)) tmp = list(filter(lambda x: x.get_type() <= 1, self._corr))
if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages: if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages:
self._unsaved_messages -= 1 self._unsaved_messages -= 1

View File

@ -29,7 +29,7 @@ ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png')
def is_inline(file_name): def is_inline(file_name):
return file_name in ALLOWED_FILES or file_name.startswith('qTox_Screenshot_') return file_name in ALLOWED_FILES or file_name.startswith('qTox_Screenshot_') or file_name.startswith('qTox_Image_')
class StateSignal(QtCore.QObject): class StateSignal(QtCore.QObject):

View File

@ -66,3 +66,10 @@ class Friend(contact.Contact):
if self._receipts: if self._receipts:
self._receipts -= 1 self._receipts -= 1
self.mark_as_sent() self.mark_as_sent()
# -----------------------------------------------------------------------------------------------------------------
# Full status
# -----------------------------------------------------------------------------------------------------------------
def get_full_status(self):
return self._status_message

49
toxygen/group_chat.py Normal file
View File

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

BIN
toxygen/images/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -543,7 +543,3 @@ class InlineImageItem(QtWidgets.QScrollArea):
def mark_as_sent(self): def mark_as_sent(self):
return False return False

View File

@ -3,11 +3,11 @@ from loginscreen import LoginScreen
import profile import profile
from settings import * from settings import *
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from bootstrap import node_generator from bootstrap import generate_nodes, download_nodes_list
from mainscreen import MainWindow from mainscreen import MainWindow
from callbacks import init_callbacks, stop, start from callbacks import init_callbacks, stop, start
from util import curr_directory, program_version, remove, is_64_bit from util import curr_directory, program_version, remove
import styles.style import styles.style # reqired for styles loading
import platform import platform
import toxes import toxes
from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen
@ -106,7 +106,7 @@ class Toxygen:
return return
self.tox = profile.tox_factory() self.tox = profile.tox_factory()
self.tox.self_set_name(bytes(_login.name, 'utf-8') if _login.name else b'Toxygen User') self.tox.self_set_name(bytes(_login.name, 'utf-8') if _login.name else b'Toxygen User')
self.tox.self_set_status_message(b'Toxing on T03') self.tox.self_set_status_message(b'Toxing on Toxygen')
reply = QtWidgets.QMessageBox.question(None, reply = QtWidgets.QMessageBox.question(None,
'Profile {}'.format(name), 'Profile {}'.format(name),
QtWidgets.QApplication.translate("login", QtWidgets.QApplication.translate("login",
@ -327,6 +327,7 @@ class Toxygen:
self.mainloop.wait() self.mainloop.wait()
self.init.wait() self.init.wait()
self.avloop.wait() self.avloop.wait()
self.tray.hide()
data = self.tox.get_savedata() data = self.tox.get_savedata()
ProfileHelper.get_instance().save_profile(data) ProfileHelper.get_instance().save_profile(data)
settings.close() settings.close()
@ -378,9 +379,11 @@ class Toxygen:
def run(self): def run(self):
# initializing callbacks # initializing callbacks
init_callbacks(self.tox, self.ms, self.tray) init_callbacks(self.tox, self.ms, self.tray)
# download list of nodes if needed
download_nodes_list()
# bootstrap # bootstrap
try: try:
for data in node_generator(): for data in generate_nodes():
if self.stop: if self.stop:
return return
self.tox.bootstrap(*data) self.tox.bootstrap(*data)
@ -393,7 +396,7 @@ class Toxygen:
self.msleep(1000) self.msleep(1000)
while not self.tox.self_get_connection_status(): while not self.tox.self_get_connection_status():
try: try:
for data in node_generator(): for data in generate_nodes():
if self.stop: if self.stop:
return return
self.tox.bootstrap(*data) self.tox.bootstrap(*data)

View File

@ -5,7 +5,6 @@ from widgets import MultilineEdit, ComboBox
import plugin_support import plugin_support
from mainscreen_widgets import * from mainscreen_widgets import *
import settings import settings
import platform
import toxes import toxes
@ -41,6 +40,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.menuAbout.setObjectName("menuAbout") self.menuAbout.setObjectName("menuAbout")
self.actionAdd_friend = QtWidgets.QAction(window) self.actionAdd_friend = QtWidgets.QAction(window)
self.actionAdd_gc = QtWidgets.QAction(window)
self.actionAdd_friend.setObjectName("actionAdd_friend") self.actionAdd_friend.setObjectName("actionAdd_friend")
self.actionprofilesettings = QtWidgets.QAction(window) self.actionprofilesettings = QtWidgets.QAction(window)
self.actionprofilesettings.setObjectName("actionprofilesettings") self.actionprofilesettings.setObjectName("actionprofilesettings")
@ -64,6 +64,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.reloadPlugins = QtWidgets.QAction(window) self.reloadPlugins = QtWidgets.QAction(window)
self.lockApp = QtWidgets.QAction(window) self.lockApp = QtWidgets.QAction(window)
self.menuProfile.addAction(self.actionAdd_friend) self.menuProfile.addAction(self.actionAdd_friend)
self.menuProfile.addAction(self.actionAdd_gc)
self.menuProfile.addAction(self.actionSettings) self.menuProfile.addAction(self.actionSettings)
self.menuProfile.addAction(self.lockApp) self.menuProfile.addAction(self.lockApp)
self.menuSettings.addAction(self.actionPrivacy_settings) self.menuSettings.addAction(self.actionPrivacy_settings)
@ -86,6 +87,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.actionAbout_program.triggered.connect(self.about_program) self.actionAbout_program.triggered.connect(self.about_program)
self.actionNetwork.triggered.connect(self.network_settings) self.actionNetwork.triggered.connect(self.network_settings)
self.actionAdd_friend.triggered.connect(self.add_contact) self.actionAdd_friend.triggered.connect(self.add_contact)
self.actionAdd_gc.triggered.connect(self.create_gc)
self.actionSettings.triggered.connect(self.profile_settings) self.actionSettings.triggered.connect(self.profile_settings)
self.actionPrivacy_settings.triggered.connect(self.privacy_settings) self.actionPrivacy_settings.triggered.connect(self.privacy_settings)
self.actionInterface_settings.triggered.connect(self.interface_settings) self.actionInterface_settings.triggered.connect(self.interface_settings)
@ -115,6 +117,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings")) self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings"))
self.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About")) self.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
self.actionAdd_friend.setText(QtWidgets.QApplication.translate("MainWindow", "Add contact")) self.actionAdd_friend.setText(QtWidgets.QApplication.translate("MainWindow", "Add contact"))
self.actionAdd_gc.setText(QtWidgets.QApplication.translate("MainWindow", "Create group chat"))
self.actionprofilesettings.setText(QtWidgets.QApplication.translate("MainWindow", "Profile")) self.actionprofilesettings.setText(QtWidgets.QApplication.translate("MainWindow", "Profile"))
self.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy")) self.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy"))
self.actionInterface_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Interface")) self.actionInterface_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Interface"))
@ -415,8 +418,10 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
import util import util
msgBox = QtWidgets.QMessageBox() msgBox = QtWidgets.QMessageBox()
msgBox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "About")) msgBox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
text = (QtWidgets.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.\nVersion: ')) text = (QtWidgets.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.<br>Version: '))
msgBox.setText(text + util.program_version + '\nGitHub: https://github.com/toxygen-project/toxygen/') github = '<br><a href="https://github.com/toxygen-project/toxygen/">Github</a>'
submit_a_bug = '<br><a href="https://github.com/toxygen-project/toxygen/issues">Submit a bug</a>'
msgBox.setText(text + util.program_version + github + submit_a_bug)
msgBox.exec_() msgBox.exec_()
def network_settings(self): def network_settings(self):
@ -431,6 +436,9 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.a_c = AddContact(link or '') self.a_c = AddContact(link or '')
self.a_c.show() self.a_c.show()
def create_gc(self):
self.profile.create_group_chat()
def profile_settings(self, *args): def profile_settings(self, *args):
self.p_s = ProfileSettings() self.p_s = ProfileSettings()
self.p_s.show() self.p_s.show()
@ -512,7 +520,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def send_file(self): def send_file(self):
self.menu.hide() self.menu.hide()
if self.profile.active_friend + 1: if self.profile.active_friend + 1and self.profile.is_active_a_friend():
choose = QtWidgets.QApplication.translate("MainWindow", 'Choose file') choose = QtWidgets.QApplication.translate("MainWindow", 'Choose file')
name = QtWidgets.QFileDialog.getOpenFileName(self, choose, options=QtWidgets.QFileDialog.DontUseNativeDialog) name = QtWidgets.QFileDialog.getOpenFileName(self, choose, options=QtWidgets.QFileDialog.DontUseNativeDialog)
if name[0]: if name[0]:
@ -520,7 +528,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def send_screenshot(self, hide=False): def send_screenshot(self, hide=False):
self.menu.hide() self.menu.hide()
if self.profile.active_friend + 1: if self.profile.active_friend + 1 and self.profile.is_active_a_friend():
self.sw = ScreenShotWindow(self) self.sw = ScreenShotWindow(self)
self.sw.show() self.sw.show()
if hide: if hide:
@ -538,7 +546,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def send_sticker(self): def send_sticker(self):
self.menu.hide() self.menu.hide()
if self.profile.active_friend + 1: if self.profile.active_friend + 1 and self.profile.is_active_a_friend():
self.sticker = StickerWindow(self) self.sticker = StickerWindow(self)
self.sticker.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(), self.sticker.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(),
self.y() + self.height() - 200, self.y() + self.height() - 200,
@ -583,7 +591,10 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
auto = QtWidgets.QApplication.translate("MainWindow", 'Disallow auto accept') if allowed else QtWidgets.QApplication.translate("MainWindow", 'Allow auto accept') auto = QtWidgets.QApplication.translate("MainWindow", 'Disallow auto accept') if allowed else QtWidgets.QApplication.translate("MainWindow", 'Allow auto accept')
if item is not None: if item is not None:
self.listMenu = QtWidgets.QMenu() self.listMenu = QtWidgets.QMenu()
is_friend = type(friend) is Friend
if is_friend:
set_alias_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set alias')) set_alias_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set alias'))
set_alias_item.triggered.connect(lambda: self.set_alias(num))
history_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Chat history')) history_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Chat history'))
clear_history_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Clear history')) clear_history_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Clear history'))
@ -593,6 +604,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
copy_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Copy')) copy_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Copy'))
copy_name_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Name')) copy_name_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Name'))
copy_status_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Status message')) copy_status_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Status message'))
if is_friend:
copy_key_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Public key')) copy_key_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Public key'))
auto_accept_item = self.listMenu.addAction(auto) auto_accept_item = self.listMenu.addAction(auto)
@ -600,19 +612,31 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
block_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Block friend')) block_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Block friend'))
notes_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Notes')) notes_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Notes'))
chats = self.profile.get_group_chats()
if len(chats) and self.profile.is_active_online():
invite_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Invite to group chat'))
for i in range(len(chats)):
name, number = chats[i]
item = invite_menu.addAction(name)
item.triggered.connect(lambda number=number: self.invite_friend_to_gc(num, number))
plugins_loader = plugin_support.PluginLoader.get_instance() plugins_loader = plugin_support.PluginLoader.get_instance()
if plugins_loader is not None: if plugins_loader is not None:
submenu = plugins_loader.get_menu(self.listMenu, num) submenu = plugins_loader.get_menu(self.listMenu, num)
if len(submenu): if len(submenu):
plug = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins')) plug = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins'))
plug.addActions(submenu) plug.addActions(submenu)
set_alias_item.triggered.connect(lambda: self.set_alias(num)) copy_key_item.triggered.connect(lambda: self.copy_friend_key(num))
remove_item.triggered.connect(lambda: self.remove_friend(num)) remove_item.triggered.connect(lambda: self.remove_friend(num))
block_item.triggered.connect(lambda: self.block_friend(num)) block_item.triggered.connect(lambda: self.block_friend(num))
copy_key_item.triggered.connect(lambda: self.copy_friend_key(num))
clear_history_item.triggered.connect(lambda: self.clear_history(num))
auto_accept_item.triggered.connect(lambda: self.auto_accept(num, not allowed)) auto_accept_item.triggered.connect(lambda: self.auto_accept(num, not allowed))
notes_item.triggered.connect(lambda: self.show_note(friend)) notes_item.triggered.connect(lambda: self.show_note(friend))
else:
leave_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Leave chat'))
set_title_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set title'))
leave_item.triggered.connect(lambda: self.leave_gc(num))
set_title_item.triggered.connect(lambda: self.set_title(num))
clear_history_item.triggered.connect(lambda: self.clear_history(num))
copy_name_item.triggered.connect(lambda: self.copy_name(friend)) copy_name_item.triggered.connect(lambda: self.copy_name(friend))
copy_status_item.triggered.connect(lambda: self.copy_status(friend)) copy_status_item.triggered.connect(lambda: self.copy_status(friend))
export_to_text_item.triggered.connect(lambda: self.export_history(num)) export_to_text_item.triggered.connect(lambda: self.export_history(num))
@ -638,15 +662,18 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def export_history(self, num, as_text=True): def export_history(self, num, as_text=True):
s = self.profile.export_history(num, as_text) s = self.profile.export_history(num, as_text)
directory = QtWidgets.QFileDialog.getExistingDirectory(None, extension = 'txt' if as_text else 'html'
file_name, _ = QtWidgets.QFileDialog.getSaveFileName(None,
QtWidgets.QApplication.translate("MainWindow", QtWidgets.QApplication.translate("MainWindow",
'Choose folder'), 'Choose file name'),
curr_directory(), curr_directory(),
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) filter=extension,
options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
if directory: if file_name:
name = 'exported_history_{}.{}'.format(convert_time(time.time()), 'txt' if as_text else 'html') if not file_name.endswith('.' + extension):
with open(directory + '/' + name, 'wt') as fl: file_name += '.' + extension
with open(file_name, 'wt') as fl:
fl.write(s) fl.write(s)
def set_alias(self, num): def set_alias(self, num):
@ -675,6 +702,12 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def clear_history(self, num): def clear_history(self, num):
self.profile.clear_history(num) self.profile.clear_history(num)
def leave_gc(self, num):
self.profile.leave_gc(num)
def set_title(self, num):
self.profile.set_title(num)
def auto_accept(self, num, value): def auto_accept(self, num, value):
settings = Settings.get_instance() settings = Settings.get_instance()
tox_id = self.profile.friend_public_key(num) tox_id = self.profile.friend_public_key(num)
@ -684,6 +717,9 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
settings['auto_accept_from_friends'].remove(tox_id) settings['auto_accept_from_friends'].remove(tox_id)
settings.save() settings.save()
def invite_friend_to_gc(self, friend_number, group_number):
self.profile.invite_friend(friend_number, group_number)
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Functions which called when user click somewhere else # Functions which called when user click somewhere else
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------

View File

@ -3,6 +3,7 @@ from widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWi
from profile import Profile from profile import Profile
import smileys import smileys
import util import util
import platform
class MessageArea(QtWidgets.QPlainTextEdit): class MessageArea(QtWidgets.QPlainTextEdit):
@ -34,6 +35,10 @@ class MessageArea(QtWidgets.QPlainTextEdit):
self.parent.send_message() self.parent.send_message()
elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText(): elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText():
self.appendPlainText(Profile.get_instance().get_last_message()) self.appendPlainText(Profile.get_instance().get_last_message())
elif event.key() == QtCore.Qt.Key_Tab and not self.parent.profile.is_active_a_friend():
text = self.toPlainText()
pos = self.textCursor().position()
self.insertPlainText(Profile.get_instance().get_gc_peer_name(text[:pos]))
else: else:
self.parent.profile.send_typing(True) self.parent.profile.send_typing(True)
if self.timer.isActive(): if self.timer.isActive():
@ -66,9 +71,25 @@ class MessageArea(QtWidgets.QPlainTextEdit):
def pasteEvent(self, text=None): def pasteEvent(self, text=None):
text = text or QtWidgets.QApplication.clipboard().text() text = text or QtWidgets.QApplication.clipboard().text()
if text.startswith('file://'): if text.startswith('file://'):
self.parent.profile.send_file(text[7:]) file_name = self.parse_file_name(text)
else: self.parent.profile.send_file(file_name)
elif text:
self.insertPlainText(text) self.insertPlainText(text)
else:
image = QtWidgets.QApplication.clipboard().image()
if image is not None:
byte_array = QtCore.QByteArray()
buffer = QtCore.QBuffer(byte_array)
buffer.open(QtCore.QIODevice.WriteOnly)
image.save(buffer, 'PNG')
self.parent.profile.send_screenshot(bytes(byte_array.data()))
def parse_file_name(self, file_name):
import urllib
if file_name.endswith('\r\n'):
file_name = file_name[:-2]
file_name = urllib.parse.unquote(file_name)
return file_name[8 if platform.system() == 'Windows' else 7:]
class ScreenShotWindow(RubberBandWindow): class ScreenShotWindow(RubberBandWindow):
@ -314,7 +335,7 @@ class WelcomeScreen(CenteredWidget):
'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.') 'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.')
elif num == 7: elif num == 7:
text = QtWidgets.QApplication.translate('WelcomeScreen', text = QtWidgets.QApplication.translate('WelcomeScreen',
'New in Toxygen 0.3.0:<br>Video calls<br>Python3.6 support<br>Migration to PyQt5') 'New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes')
elif num == 8: elif num == 8:
text = QtWidgets.QApplication.translate('WelcomeScreen', text = QtWidgets.QApplication.translate('WelcomeScreen',
'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu') 'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu')

View File

@ -294,10 +294,10 @@ class NetworkSettings(CenteredWidget):
def initUI(self): def initUI(self):
self.setObjectName("NetworkSettings") self.setObjectName("NetworkSettings")
self.resize(300, 330) self.resize(300, 400)
self.setMinimumSize(QtCore.QSize(300, 330)) self.setMinimumSize(QtCore.QSize(300, 400))
self.setMaximumSize(QtCore.QSize(300, 330)) self.setMaximumSize(QtCore.QSize(300, 400))
self.setBaseSize(QtCore.QSize(300, 330)) self.setBaseSize(QtCore.QSize(300, 400))
self.ipv = QtWidgets.QCheckBox(self) self.ipv = QtWidgets.QCheckBox(self)
self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22)) self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22))
self.ipv.setObjectName("ipv") self.ipv.setObjectName("ipv")
@ -332,6 +332,9 @@ class NetworkSettings(CenteredWidget):
self.warning = QtWidgets.QLabel(self) self.warning = QtWidgets.QLabel(self)
self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60)) self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60))
self.warning.setStyleSheet('QLabel { color: #BC1C1C; }') self.warning.setStyleSheet('QLabel { color: #BC1C1C; }')
self.nodes = QtWidgets.QCheckBox(self)
self.nodes.setGeometry(QtCore.QRect(20, 350, 270, 22))
self.nodes.setChecked(settings['download_nodes_list'])
self.retranslateUi() self.retranslateUi()
self.proxy.stateChanged.connect(lambda x: self.activate()) self.proxy.stateChanged.connect(lambda x: self.activate())
self.activate() self.activate()
@ -346,6 +349,7 @@ class NetworkSettings(CenteredWidget):
self.label_2.setText(QtWidgets.QApplication.translate("Form", "Port:")) self.label_2.setText(QtWidgets.QApplication.translate("Form", "Port:"))
self.reconnect.setText(QtWidgets.QApplication.translate("NetworkSettings", "Restart TOX core")) self.reconnect.setText(QtWidgets.QApplication.translate("NetworkSettings", "Restart TOX core"))
self.http.setText(QtWidgets.QApplication.translate("Form", "HTTP")) self.http.setText(QtWidgets.QApplication.translate("Form", "HTTP"))
self.nodes.setText(QtWidgets.QApplication.translate("Form", "Download nodes list from tox.chat"))
self.warning.setText(QtWidgets.QApplication.translate("Form", "WARNING:\nusing proxy with enabled UDP\ncan produce IP leak")) self.warning.setText(QtWidgets.QApplication.translate("Form", "WARNING:\nusing proxy with enabled UDP\ncan produce IP leak"))
def activate(self): def activate(self):
@ -362,6 +366,7 @@ class NetworkSettings(CenteredWidget):
settings['proxy_type'] = 2 - int(self.http.isChecked()) if self.proxy.isChecked() else 0 settings['proxy_type'] = 2 - int(self.http.isChecked()) if self.proxy.isChecked() else 0
settings['proxy_host'] = str(self.proxyip.text()) settings['proxy_host'] = str(self.proxyip.text())
settings['proxy_port'] = int(self.proxyport.text()) settings['proxy_port'] = int(self.proxyport.text())
settings['download_nodes_list'] = self.nodes.isChecked()
settings.save() settings.save()
# recreate tox instance # recreate tox instance
Profile.get_instance().reset(self.reset) Profile.get_instance().reset(self.reset)
@ -508,15 +513,17 @@ class NotificationsSettings(CenteredWidget):
def initUI(self): def initUI(self):
self.setObjectName("notificationsForm") self.setObjectName("notificationsForm")
self.resize(350, 180) self.resize(350, 210)
self.setMinimumSize(QtCore.QSize(350, 180)) self.setMinimumSize(QtCore.QSize(350, 210))
self.setMaximumSize(QtCore.QSize(350, 180)) self.setMaximumSize(QtCore.QSize(350, 210))
self.enableNotifications = QtWidgets.QCheckBox(self) self.enableNotifications = QtWidgets.QCheckBox(self)
self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18)) self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18))
self.callsSound = QtWidgets.QCheckBox(self) self.callsSound = QtWidgets.QCheckBox(self)
self.callsSound.setGeometry(QtCore.QRect(10, 120, 340, 18)) self.callsSound.setGeometry(QtCore.QRect(10, 170, 340, 18))
self.soundNotifications = QtWidgets.QCheckBox(self) self.soundNotifications = QtWidgets.QCheckBox(self)
self.soundNotifications.setGeometry(QtCore.QRect(10, 70, 340, 18)) self.soundNotifications.setGeometry(QtCore.QRect(10, 70, 340, 18))
self.groupNotifications = QtWidgets.QCheckBox(self)
self.groupNotifications.setGeometry(QtCore.QRect(10, 120, 340, 18))
font = QtGui.QFont() font = QtGui.QFont()
s = Settings.get_instance() s = Settings.get_instance()
font.setFamily(s['font']) font.setFamily(s['font'])
@ -524,8 +531,10 @@ class NotificationsSettings(CenteredWidget):
self.callsSound.setFont(font) self.callsSound.setFont(font)
self.soundNotifications.setFont(font) self.soundNotifications.setFont(font)
self.enableNotifications.setFont(font) self.enableNotifications.setFont(font)
self.groupNotifications.setFont(font)
self.enableNotifications.setChecked(s['notifications']) self.enableNotifications.setChecked(s['notifications'])
self.soundNotifications.setChecked(s['sound_notifications']) self.soundNotifications.setChecked(s['sound_notifications'])
self.groupNotifications.setChecked(s['group_notifications'])
self.callsSound.setChecked(s['calls_sound']) self.callsSound.setChecked(s['calls_sound'])
self.retranslateUi() self.retranslateUi()
QtCore.QMetaObject.connectSlotsByName(self) QtCore.QMetaObject.connectSlotsByName(self)
@ -533,6 +542,7 @@ class NotificationsSettings(CenteredWidget):
def retranslateUi(self): def retranslateUi(self):
self.setWindowTitle(QtWidgets.QApplication.translate("notificationsForm", "Notification settings")) self.setWindowTitle(QtWidgets.QApplication.translate("notificationsForm", "Notification settings"))
self.enableNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable notifications")) self.enableNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable notifications"))
self.groupNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Notify about all messages in groups"))
self.callsSound.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable call\'s sound")) self.callsSound.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable call\'s sound"))
self.soundNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable sound notifications")) self.soundNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable sound notifications"))
@ -540,6 +550,7 @@ class NotificationsSettings(CenteredWidget):
settings = Settings.get_instance() settings = Settings.get_instance()
settings['notifications'] = self.enableNotifications.isChecked() settings['notifications'] = self.enableNotifications.isChecked()
settings['sound_notifications'] = self.soundNotifications.isChecked() settings['sound_notifications'] = self.soundNotifications.isChecked()
settings['group_notifications'] = self.groupNotifications.isChecked()
settings['calls_sound'] = self.callsSound.isChecked() settings['calls_sound'] = self.callsSound.isChecked()
settings.save() settings.save()
@ -605,7 +616,7 @@ class InterfaceSettings(CenteredWidget):
self.messages_font_size_label.setFont(font) self.messages_font_size_label.setFont(font)
self.messages_font_size = QtWidgets.QComboBox(self) self.messages_font_size = QtWidgets.QComboBox(self)
self.messages_font_size.setGeometry(QtCore.QRect(30, 330, 160, 30)) self.messages_font_size.setGeometry(QtCore.QRect(30, 330, 160, 30))
self.messages_font_size.addItems([str(x) for x in range(10, 19)]) self.messages_font_size.addItems([str(x) for x in range(10, 25)])
self.messages_font_size.setCurrentIndex(settings['message_font_size'] - 10) self.messages_font_size.setCurrentIndex(settings['message_font_size'] - 10)
self.unread = QtWidgets.QPushButton(self) self.unread = QtWidgets.QPushButton(self)
@ -810,7 +821,7 @@ class DesktopAreaSelectionWindow(RubberBandWindow):
rect = self.rubberband.geometry() rect = self.rubberband.geometry()
width, height = rect.width(), rect.height() width, height = rect.width(), rect.height()
if width >= 8 and height >= 8: if width >= 8 and height >= 8:
self.parent.save(rect.x(), rect.y(), width, height) self.parent.save(rect.x(), rect.y(), width - (width % 4), height - (height % 4))
self.close() self.close()
@ -881,6 +892,8 @@ class VideoSettings(CenteredWidget):
self.desktopAreaSelection = DesktopAreaSelectionWindow(self) self.desktopAreaSelection = DesktopAreaSelectionWindow(self)
def closeEvent(self, event): def closeEvent(self, event):
if self.input.currentIndex() == 0:
return
try: try:
settings = Settings.get_instance() settings = Settings.get_instance()
settings.video['device'] = self.devices[self.input.currentIndex()] settings.video['device'] = self.devices[self.input.currentIndex()]

View File

@ -5,7 +5,9 @@ MESSAGE_TYPE = {
'ACTION': 1, 'ACTION': 1,
'FILE_TRANSFER': 2, 'FILE_TRANSFER': 2,
'INLINE': 3, 'INLINE': 3,
'INFO_MESSAGE': 4 'INFO_MESSAGE': 4,
'GC_TEXT': 5,
'GC_ACTION': 6
} }
@ -39,6 +41,16 @@ class TextMessage(Message):
return self._message, self._owner, self._time, self._type return self._message, self._owner, self._time, self._type
class GroupChatMessage(TextMessage):
def __init__(self, message, owner, time, message_type, name):
super().__init__(message, owner, time, message_type)
self._user_name = name
def get_data(self):
return self._message, self._owner, self._time, self._type, self._user_name
class TransferMessage(Message): class TransferMessage(Message):
""" """
Message with info about file transfer Message with info about file transfer

1
toxygen/nodes.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -97,10 +97,13 @@ class PluginLoader(util.Singleton):
""" """
result = [] result = []
for data in self._plugins.values(): for data in self._plugins.values():
try:
result.append([data[0].get_name(), # plugin full name result.append([data[0].get_name(), # plugin full name
data[1], # is enabled data[1], # is enabled
data[0].get_description(), # plugin description data[0].get_description(), # plugin description
data[0].get_short_name()]) # key - short unique name data[0].get_short_name()]) # key - short unique name
except:
continue
return result return result
def plugin_window(self, key): def plugin_window(self, key):

View File

@ -16,6 +16,8 @@ import basecontact
import items_factory import items_factory
import cv2 import cv2
import threading import threading
from group_chat import *
import re
class Profile(basecontact.BaseContact, Singleton): class Profile(basecontact.BaseContact, Singleton):
@ -70,6 +72,8 @@ class Profile(basecontact.BaseContact, Singleton):
friend = Friend(message_getter, i, name, status_message, item, tox_id) friend = Friend(message_getter, i, name, status_message, item, tox_id)
friend.set_alias(alias) friend.set_alias(alias)
self._contacts.append(friend) self._contacts.append(friend)
if len(self._contacts):
self.set_active(0)
self.filtration_and_sorting(self._sorting) self.filtration_and_sorting(self._sorting)
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -129,6 +133,7 @@ class Profile(basecontact.BaseContact, Singleton):
filter_str = filter_str.lower() filter_str = filter_str.lower()
settings = Settings.get_instance() settings = Settings.get_instance()
number = self.get_active_number() number = self.get_active_number()
is_friend = self.is_active_a_friend()
if sorting > 1: if sorting > 1:
if sorting & 2: if sorting & 2:
self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True) self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True)
@ -164,7 +169,7 @@ class Profile(basecontact.BaseContact, Singleton):
self._sorting, self._filter_string = sorting, filter_str self._sorting, self._filter_string = sorting, filter_str
settings['sorting'] = self._sorting settings['sorting'] = self._sorting
settings.save() settings.save()
self.set_active_by_number(number) self.set_active_by_number_and_type(number, is_friend)
def update_filtration(self): def update_filtration(self):
""" """
@ -177,7 +182,7 @@ class Profile(basecontact.BaseContact, Singleton):
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def get_friend_by_number(self, num): def get_friend_by_number(self, num):
return list(filter(lambda x: x.number == num, self._contacts))[0] return list(filter(lambda x: x.number == num and type(x) is Friend, self._contacts))[0]
def get_friend(self, num): def get_friend(self, num):
if num < 0 or num >= len(self._contacts): if num < 0 or num >= len(self._contacts):
@ -204,6 +209,7 @@ class Profile(basecontact.BaseContact, Singleton):
if value == -1: # all friends were deleted if value == -1: # all friends were deleted
self._screen.account_name.setText('') self._screen.account_name.setText('')
self._screen.account_status.setText('') self._screen.account_status.setText('')
self._screen.account_status.setToolTip('')
self._active_friend = -1 self._active_friend = -1
self._screen.account_avatar.setHidden(True) self._screen.account_avatar.setHidden(True)
self._messages.clear() self._messages.clear()
@ -251,12 +257,15 @@ class Profile(basecontact.BaseContact, Singleton):
print('Incoming not started transfer - no info found') print('Incoming not started transfer - no info found')
elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline
self.create_inline_item(message.get_data()) self.create_inline_item(message.get_data())
else: # info message elif message.get_type() < 5: # info message
data = message.get_data() data = message.get_data()
self.create_message_item(data[0], self.create_message_item(data[0],
data[2], data[2],
'', '',
data[3]) data[3])
else:
data = message.get_data()
self.create_gc_message_item(data[0], data[2], data[1], data[4], data[3])
self._messages.scrollToBottom() self._messages.scrollToBottom()
self._load_history = True self._load_history = True
if value in self._call: if value in self._call:
@ -270,6 +279,10 @@ class Profile(basecontact.BaseContact, Singleton):
self._screen.account_name.setText(friend.name) self._screen.account_name.setText(friend.name)
self._screen.account_status.setText(friend.status_message) self._screen.account_status.setText(friend.status_message)
self._screen.account_status.setToolTip(friend.get_full_status())
if friend.tox_id is None:
avatar_path = curr_directory() + '/images/group.png'
else:
avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(friend.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(friend.tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
if not os.path.isfile(avatar_path): # load default image if not os.path.isfile(avatar_path): # load default image
avatar_path = curr_directory() + '/images/avatar.png' avatar_path = curr_directory() + '/images/avatar.png'
@ -282,9 +295,10 @@ class Profile(basecontact.BaseContact, Singleton):
log('Error in set active: ' + str(ex)) log('Error in set active: ' + str(ex))
raise raise
def set_active_by_number(self, number): def set_active_by_number_and_type(self, number, is_friend):
for i in range(len(self._contacts)): for i in range(len(self._contacts)):
if self._contacts[i].number == number: c = self._contacts[i]
if c.number == number and (type(c) is Friend == is_friend):
self._active_friend = i self._active_friend = i
break break
@ -347,7 +361,7 @@ class Profile(basecontact.BaseContact, Singleton):
elif data[1] == friend_number and not data[2]: elif data[1] == friend_number and not data[2]:
self.send_file(data[0], friend_number, True, key) self.send_file(data[0], friend_number, True, key)
del self._paused_file_transfers[key] del self._paused_file_transfers[key]
if friend_number == self.get_active_number(): if friend_number == self.get_active_number() and self.is_active_a_friend():
self.update() self.update()
except Exception as ex: except Exception as ex:
print('Exception in file sending: ' + str(ex)) print('Exception in file sending: ' + str(ex))
@ -389,7 +403,7 @@ class Profile(basecontact.BaseContact, Singleton):
""" """
Display incoming typing notification Display incoming typing notification
""" """
if friend_number == self.get_active_number(): if friend_number == self.get_active_number() and self.is_active_a_friend():
self._screen.typing.setVisible(typing) self._screen.typing.setVisible(typing)
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -445,7 +459,7 @@ class Profile(basecontact.BaseContact, Singleton):
:param message_type: message type - plain text or action message (/me) :param message_type: message type - plain text or action message (/me)
:param message: text of message :param message: text of message
""" """
if friend_num == self.get_active_number(): # add message to list if friend_num == self.get_active_number()and self.is_active_a_friend(): # add message to list
t = time.time() t = time.time()
self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type)
self._messages.scrollToBottom() self._messages.scrollToBottom()
@ -465,6 +479,9 @@ class Profile(basecontact.BaseContact, Singleton):
:param text: message text :param text: message text
:param friend_num: num of friend :param friend_num: num of friend
""" """
if not self.is_active_a_friend():
self.send_gc_message(text)
return
if friend_num is None: if friend_num is None:
friend_num = self.get_active_number() friend_num = self.get_active_number()
if text.startswith('/plugin '): if text.startswith('/plugin '):
@ -480,8 +497,8 @@ class Profile(basecontact.BaseContact, Singleton):
friend.inc_receipts() friend.inc_receipts()
if friend.status is not None: if friend.status is not None:
self.split_and_send(friend.number, message_type, text.encode('utf-8')) self.split_and_send(friend.number, message_type, text.encode('utf-8'))
if friend.number == self.get_active_number():
t = time.time() t = time.time()
if friend.number == self.get_active_number() and self.is_active_a_friend():
self.create_message_item(text, t, MESSAGE_OWNER['NOT_SENT'], message_type) self.create_message_item(text, t, MESSAGE_OWNER['NOT_SENT'], message_type)
self._screen.messageEdit.clear() self._screen.messageEdit.clear()
self._messages.scrollToBottom() self._messages.scrollToBottom()
@ -504,7 +521,7 @@ class Profile(basecontact.BaseContact, Singleton):
s = Settings.get_instance() s = Settings.get_instance()
if hasattr(self, '_history'): if hasattr(self, '_history'):
if s['save_history']: if s['save_history']:
for friend in self._contacts: for friend in filter(lambda x: type(x) is Friend, self._contacts):
if not self._history.friend_exists_in_db(friend.tox_id): if not self._history.friend_exists_in_db(friend.tox_id):
self._history.add_friend_to_db(friend.tox_id) self._history.add_friend_to_db(friend.tox_id)
if not s['save_unsent_only']: if not s['save_unsent_only']:
@ -571,13 +588,16 @@ class Profile(basecontact.BaseContact, Singleton):
print('Incoming not started transfer - no info found') print('Incoming not started transfer - no info found')
elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline image elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline image
self.create_inline_item(message.get_data(), False) self.create_inline_item(message.get_data(), False)
else: # info message elif message.get_type() < 5: # info message
data = message.get_data() data = message.get_data()
self.create_message_item(data[0], self.create_message_item(data[0],
data[2], data[2],
'', '',
data[3], data[3],
False) False)
else:
data = message.get_data()
self.create_gc_message_item(data[0], data[2], data[1], data[4], data[3], False)
self._load_history = True self._load_history = True
def export_db(self, directory): def export_db(self, directory):
@ -636,6 +656,16 @@ class Profile(basecontact.BaseContact, Singleton):
return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'],
message_type, append, pixmap) message_type, append, pixmap)
def create_gc_message_item(self, text, time, owner, name, message_type, append=True):
pixmap = None
if self._show_avatars:
if owner == MESSAGE_OWNER['FRIEND']:
pixmap = self.get_curr_friend().get_pixmap()
else:
pixmap = self.get_pixmap()
return self._factory.message_item(text, time, name, True,
message_type - 5, append, pixmap)
def create_file_transfer_item(self, tm, append=True): def create_file_transfer_item(self, tm, append=True):
data = list(tm.get_data()) data = list(tm.get_data())
data[3] = self.get_friend_by_number(data[4]).name if data[3] else self._name data[3] = self.get_friend_by_number(data[4]).name if data[3] else self._name
@ -692,7 +722,7 @@ class Profile(basecontact.BaseContact, Singleton):
except: except:
pass pass
settings.save() settings.save()
if num == self.get_active_number(): if num == self.get_active_number() and self.is_active_a_friend():
self.update() self.update()
def friend_public_key(self, num): def friend_public_key(self, num):
@ -843,8 +873,11 @@ class Profile(basecontact.BaseContact, Singleton):
Recreate tox instance Recreate tox instance
:param restart: method which calls restart and returns new tox instance :param restart: method which calls restart and returns new tox instance
""" """
for friend in self._contacts: for contact in self._contacts:
self.friend_exit(friend.number) if type(contact) is Friend:
self.friend_exit(contact.number)
else:
self.leave_gc(contact.number)
self._call.stop() self._call.stop()
del self._call del self._call
del self._tox del self._tox
@ -863,7 +896,7 @@ class Profile(basecontact.BaseContact, Singleton):
QtCore.QTimer.singleShot(50000, self.reconnect) QtCore.QTimer.singleShot(50000, self.reconnect)
def close(self): def close(self):
for friend in self._contacts: for friend in filter(lambda x: type(x) is Friend, self._contacts):
self.friend_exit(friend.number) self.friend_exit(friend.number)
for i in range(len(self._contacts)): for i in range(len(self._contacts)):
del self._contacts[0] del self._contacts[0]
@ -936,7 +969,7 @@ class Profile(basecontact.BaseContact, Singleton):
friend_number, friend_number,
file_number) file_number)
accepted = False accepted = False
if friend_number == self.get_active_number(): if friend_number == self.get_active_number() and self.is_active_a_friend():
item = self.create_file_transfer_item(tm) item = self.create_file_transfer_item(tm)
if accepted: if accepted:
self._file_transfers[(friend_number, file_number)].set_state_changed_handler(item.update_transfer_state) self._file_transfers[(friend_number, file_number)].set_state_changed_handler(item.update_transfer_state)
@ -967,14 +1000,15 @@ class Profile(basecontact.BaseContact, Singleton):
else: else:
if not already_cancelled: if not already_cancelled:
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL']) self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL'])
if friend_number == self.get_active_number(): if friend_number == self.get_active_number() and self.is_active_a_friend():
tmp = self._messages.count() + i tmp = self._messages.count() + i
if tmp >= 0: if tmp >= 0:
self._messages.itemWidget(self._messages.item(tmp)).update(TOX_FILE_TRANSFER_STATE['CANCELLED'], self._messages.itemWidget(
self._messages.item(tmp)).update_transfer_state(TOX_FILE_TRANSFER_STATE['CANCELLED'],
0, -1) 0, -1)
def cancel_not_started_transfer(self, time): def cancel_not_started_transfer(self, cancel_time):
self.get_curr_friend().delete_one_unsent_file(time) self.get_curr_friend().delete_one_unsent_file(cancel_time)
self.update() self.update()
def pause_transfer(self, friend_number, file_number, by_friend=False): def pause_transfer(self, friend_number, file_number, by_friend=False):
@ -1065,8 +1099,9 @@ class Profile(basecontact.BaseContact, Singleton):
file_name, file_name,
friend.number, friend.number,
st.get_file_number()) st.get_file_number())
item = self.create_file_transfer_item(tm)
friend.append_message(tm) friend.append_message(tm)
if friend_number == self.get_active_number():
item = self.create_file_transfer_item(tm)
st.set_state_changed_handler(item.update_transfer_state) st.set_state_changed_handler(item.update_transfer_state)
self._messages.scrollToBottom() self._messages.scrollToBottom()
@ -1121,7 +1156,7 @@ class Profile(basecontact.BaseContact, Singleton):
t = type(transfer) t = type(transfer)
if t is ReceiveAvatar: if t is ReceiveAvatar:
self.get_friend_by_number(friend_number).load_avatar() self.get_friend_by_number(friend_number).load_avatar()
if friend_number == self.get_active_number(): if friend_number == self.get_active_number() and self.is_active_a_friend():
self.set_active(None) self.set_active(None)
elif t is ReceiveToBuffer or (t is SendFromBuffer and Settings.get_instance()['allow_inline']): # inline image elif t is ReceiveToBuffer or (t is SendFromBuffer and Settings.get_instance()['allow_inline']): # inline image
print('inline') print('inline')
@ -1129,7 +1164,7 @@ class Profile(basecontact.BaseContact, Singleton):
i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, i = self.get_friend_by_number(friend_number).update_transfer_data(file_number,
TOX_FILE_TRANSFER_STATE['FINISHED'], TOX_FILE_TRANSFER_STATE['FINISHED'],
inline) inline)
if friend_number == self.get_active_number(): if friend_number == self.get_active_number() and self.is_active_a_friend():
count = self._messages.count() count = self._messages.count()
if count + i + 1 >= 0: if count + i + 1 >= 0:
elem = QtWidgets.QListWidgetItem() elem = QtWidgets.QListWidgetItem()
@ -1171,7 +1206,7 @@ class Profile(basecontact.BaseContact, Singleton):
ra.set_transfer_finished_handler(self.transfer_finished) ra.set_transfer_finished_handler(self.transfer_finished)
else: else:
self.get_friend_by_number(friend_number).load_avatar() self.get_friend_by_number(friend_number).load_avatar()
if self.get_active_number() == friend_number: if self.get_active_number() == friend_number and self.is_active_a_friend():
self.set_active(None) self.set_active(None)
def reset_avatar(self): def reset_avatar(self):
@ -1196,6 +1231,8 @@ class Profile(basecontact.BaseContact, Singleton):
def call_click(self, audio=True, video=False): def call_click(self, audio=True, video=False):
"""User clicked audio button in main window""" """User clicked audio button in main window"""
num = self.get_active_number() num = self.get_active_number()
if not self.is_active_a_friend():
return
if num not in self._call and self.is_active_online(): # start call if num not in self._call and self.is_active_online(): # start call
if not Settings.get_instance().audio['enabled']: if not Settings.get_instance().audio['enabled']:
return return
@ -1213,7 +1250,7 @@ class Profile(basecontact.BaseContact, Singleton):
def incoming_call(self, audio, video, friend_number): def incoming_call(self, audio, video, friend_number):
""" """
Incoming call from friend. Only audio is supported now Incoming call from friend.
""" """
if not Settings.get_instance().audio['enabled']: if not Settings.get_instance().audio['enabled']:
return return
@ -1254,17 +1291,149 @@ class Profile(basecontact.BaseContact, Singleton):
else: else:
text = QtWidgets.QApplication.translate("incoming_call", "Call finished") text = QtWidgets.QApplication.translate("incoming_call", "Call finished")
self._screen.call_finished() self._screen.call_finished()
is_video = self._call.is_video_call(friend_number)
self._call.finish_call(friend_number, by_friend) # finish or decline call self._call.finish_call(friend_number, by_friend) # finish or decline call
if hasattr(self, '_call_widget'): if hasattr(self, '_call_widget'):
self._call_widget[friend_number].close() self._call_widget[friend_number].close()
del self._call_widget[friend_number] del self._call_widget[friend_number]
threading.Timer(2.0, lambda: cv2.destroyWindow(str(friend_number))).start()
def destroy_window():
if is_video:
cv2.destroyWindow(str(friend_number))
threading.Timer(2.0, destroy_window).start()
friend = self.get_friend_by_number(friend_number) friend = self.get_friend_by_number(friend_number)
friend.append_message(InfoMessage(text, time.time())) friend.append_message(InfoMessage(text, time.time()))
if friend_number == self.get_active_number(): if friend_number == self.get_active_number():
self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE'])
self._messages.scrollToBottom() self._messages.scrollToBottom()
# -----------------------------------------------------------------------------------------------------------------
# GC support
# -----------------------------------------------------------------------------------------------------------------
def is_active_a_friend(self):
return type(self.get_curr_friend()) is Friend
def get_group_by_number(self, number):
groups = filter(lambda x: type(x) is GroupChat and x.number == number, self._contacts)
return list(groups)[0]
def add_gc(self, number):
if number == -1:
return
widget = self.create_friend_item()
gc = GroupChat('Group chat #' + str(number), '', widget, self._tox, number)
self._contacts.append(gc)
def create_group_chat(self):
number = self._tox.add_av_groupchat()
self.add_gc(number)
def leave_gc(self, num):
gc = self._contacts[num]
self._tox.del_groupchat(gc.number)
del self._contacts[num]
self._screen.friends_list.takeItem(num)
if num == self._active_friend: # active friend was deleted
if not len(self._contacts): # last friend was deleted
self.set_active(-1)
else:
self.set_active(0)
def group_invite(self, friend_number, gc_type, data):
text = QtWidgets.QApplication.translate('MainWindow', 'User {} invites you to group chat. Accept?')
title = QtWidgets.QApplication.translate('MainWindow', 'Group chat invite')
friend = self.get_friend_by_number(friend_number)
reply = QtWidgets.QMessageBox.question(None, title, text.format(friend.name), QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes: # accepted
if gc_type == TOX_GROUPCHAT_TYPE['TEXT']:
number = self._tox.join_groupchat(friend_number, data)
else:
number = self._tox.join_av_groupchat(friend_number, data)
self.add_gc(number)
def new_gc_message(self, group_number, peer_number, message_type, message):
name = self._tox.group_peername(group_number, peer_number)
message_type += 5
if group_number == self.get_active_number() and not self.is_active_a_friend(): # add message to list
t = time.time()
self.create_gc_message_item(message, t, MESSAGE_OWNER['FRIEND'], name, message_type)
self._messages.scrollToBottom()
self.get_curr_friend().append_message(
GroupChatMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type, name))
else:
gc = self.get_group_by_number(group_number)
gc.inc_messages()
gc.append_message(
GroupChatMessage(message, MESSAGE_OWNER['FRIEND'], time.time(), message_type, name))
if not gc.visibility:
self.update_filtration()
def new_gc_title(self, group_number, title):
gc = self.get_group_by_number(group_number)
gc.new_title(title)
if not self.is_active_a_friend() and self.get_active_number() == group_number:
self.update()
def update_gc(self, group_number):
count = self._tox.group_number_peers(group_number)
gc = self.get_group_by_number(group_number)
text = QtWidgets.QApplication.translate('MainWindow', '{} users in chat')
gc.status_message = text.format(str(count)).encode('utf-8')
if not self.is_active_a_friend() and self.get_active_number() == group_number:
self.update()
def send_gc_message(self, text):
group_number = self.get_active_number()
if text.startswith('/me '):
text = text[4:]
self._tox.group_action_send(group_number, text.encode('utf-8'))
else:
self._tox.group_message_send(group_number, text.encode('utf-8'))
self._screen.messageEdit.clear()
def set_title(self, num):
"""
Set new title for gc
"""
gc = self._contacts[num]
name = gc.name
dialog = QtWidgets.QApplication.translate('MainWindow',
"Enter new title for group {}:")
dialog = dialog.format(name)
title = QtWidgets.QApplication.translate('MainWindow',
'Set title')
text, ok = QtWidgets.QInputDialog.getText(None,
title,
dialog,
QtWidgets.QLineEdit.Normal,
name)
if ok:
text = text.encode('utf-8')
self._tox.group_set_title(gc.number, text)
self.new_gc_title(gc.number, text)
def get_group_chats(self):
chats = filter(lambda x: type(x) is GroupChat, self._contacts)
chats = map(lambda c: (c.name, c.number), chats)
return list(chats)
def invite_friend(self, friend_num, group_number):
friend = self._contacts[friend_num]
self._tox.invite_friend(friend.number, group_number)
def get_gc_peer_name(self, text):
gc = self.get_curr_friend()
if type(gc) is not GroupChat:
return '\t'
names = gc.get_names()
name = re.split("\s+", text)[-1]
suggested_names = list(filter(lambda x: x.startswith(name), names))
if not len(suggested_names):
return '\t'
return suggested_names[0][len(name):] + ': '
def tox_factory(data=None, settings=None): def tox_factory(data=None, settings=None):
""" """
@ -1275,6 +1444,8 @@ def tox_factory(data=None, settings=None):
if settings is None: if settings is None:
settings = Settings.get_default_settings() settings = Settings.get_default_settings()
tox_options = Tox.options_new() tox_options = Tox.options_new()
# see <https://github.com/irungentoo/toxcore/blob/master/toxcore/tox.h> lines 393-401
tox_options.contents.ipv6_enabled = settings['ipv6_enabled']
tox_options.contents.udp_enabled = settings['udp_enabled'] tox_options.contents.udp_enabled = settings['udp_enabled']
tox_options.contents.proxy_type = settings['proxy_type'] tox_options.contents.proxy_type = settings['proxy_type']
tox_options.contents.proxy_host = bytes(settings['proxy_host'], 'UTF-8') tox_options.contents.proxy_host = bytes(settings['proxy_host'], 'UTF-8')

View File

@ -104,7 +104,7 @@ class Settings(dict, Singleton):
""" """
return { return {
'theme': 'dark', 'theme': 'dark',
'ipv6_enabled': True, 'ipv6_enabled': False,
'udp_enabled': True, 'udp_enabled': True,
'proxy_type': 0, 'proxy_type': 0,
'proxy_host': '127.0.0.1', 'proxy_host': '127.0.0.1',
@ -144,7 +144,9 @@ class Settings(dict, Singleton):
'show_welcome_screen': True, 'show_welcome_screen': True,
'close_to_tray': False, 'close_to_tray': False,
'font': 'Times New Roman', 'font': 'Times New Roman',
'update': 1 'update': 1,
'group_notifications': True,
'download_nodes_list': False
} }
@staticmethod @staticmethod

View File

@ -1,5 +1,4 @@
from ctypes import c_char_p, Structure, c_bool, byref, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 from ctypes import *
from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8
from toxcore_enums_and_consts import * from toxcore_enums_and_consts import *
from toxav import ToxAV from toxav import ToxAV
from libtox import LibToxCore from libtox import LibToxCore
@ -91,6 +90,11 @@ class Tox:
self.file_recv_chunk_cb = None self.file_recv_chunk_cb = None
self.friend_lossy_packet_cb = None self.friend_lossy_packet_cb = None
self.friend_lossless_packet_cb = None self.friend_lossless_packet_cb = None
self.group_namelist_change_cb = None
self.group_title_cb = None
self.group_action_cb = None
self.group_message_cb = None
self.group_invite_cb = None
self.AV = ToxAV(self._tox_pointer) self.AV = ToxAV(self._tox_pointer)
@ -1509,3 +1513,89 @@ class Tox:
return result return result
elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
raise RuntimeError('The instance was not bound to any port.') raise RuntimeError('The instance was not bound to any port.')
# -----------------------------------------------------------------------------------------------------------------
# Group chats
# -----------------------------------------------------------------------------------------------------------------
def del_groupchat(self, groupnumber):
result = Tox.libtoxcore.tox_del_groupchat(self._tox_pointer, c_int(groupnumber), None)
return result
def group_peername(self, groupnumber, peernumber):
buffer = create_string_buffer(TOX_MAX_NAME_LENGTH)
result = Tox.libtoxcore.tox_group_peername(self._tox_pointer, c_int(groupnumber), c_int(peernumber),
buffer, None)
return str(buffer[:result], 'utf-8')
def invite_friend(self, friendnumber, groupnumber):
result = Tox.libtoxcore.tox_invite_friend(self._tox_pointer, c_int(friendnumber),
c_int(groupnumber), None)
return result
def join_groupchat(self, friendnumber, data):
result = Tox.libtoxcore.tox_join_groupchat(self._tox_pointer,
c_int(friendnumber), c_char_p(data), c_uint16(len(data)), None)
return result
def group_message_send(self, groupnumber, message):
result = Tox.libtoxcore.tox_group_message_send(self._tox_pointer, c_int(groupnumber), c_char_p(message),
c_uint16(len(message)), None)
return result
def group_action_send(self, groupnumber, action):
result = Tox.libtoxcore.tox_group_action_send(self._tox_pointer,
c_int(groupnumber), c_char_p(action),
c_uint16(len(action)), None)
return result
def group_set_title(self, groupnumber, title):
result = Tox.libtoxcore.tox_group_set_title(self._tox_pointer, c_int(groupnumber),
c_char_p(title), c_uint8(len(title)), None)
return result
def group_get_title(self, groupnumber):
buffer = create_string_buffer(TOX_MAX_NAME_LENGTH)
result = Tox.libtoxcore.tox_group_get_title(self._tox_pointer,
c_int(groupnumber), buffer,
c_uint32(TOX_MAX_NAME_LENGTH), None)
return str(buffer[:result], 'utf-8')
def group_number_peers(self, groupnumber):
result = Tox.libtoxcore.tox_group_number_peers(self._tox_pointer, c_int(groupnumber), None)
return result
def add_av_groupchat(self):
result = self.AV.libtoxav.toxav_add_av_groupchat(self._tox_pointer, None, None)
return result
def join_av_groupchat(self, friendnumber, data):
result = self.AV.libtoxav.toxav_join_av_groupchat(self._tox_pointer, c_int32(friendnumber),
c_char_p(data), c_uint16(len(data)),
None, None)
return result
def callback_group_invite(self, callback, user_data=None):
c_callback = CFUNCTYPE(None, c_void_p, c_int32, c_uint8, POINTER(c_uint8), c_uint16, c_void_p)
self.group_invite_cb = c_callback(callback)
Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, self.group_invite_cb, user_data)
def callback_group_message(self, callback, user_data=None):
c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p, c_uint16, c_void_p)
self.group_message_cb = c_callback(callback)
Tox.libtoxcore.tox_callback_group_message(self._tox_pointer, self.group_message_cb, user_data)
def callback_group_action(self, callback, user_data=None):
c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p, c_uint16, c_void_p)
self.group_action_cb = c_callback(callback)
Tox.libtoxcore.tox_callback_group_action(self._tox_pointer, self.group_action_cb, user_data)
def callback_group_title(self, callback, user_data=None):
c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p, c_uint8, c_void_p)
self.group_title_cb = c_callback(callback)
Tox.libtoxcore.tox_callback_group_title(self._tox_pointer, self.group_title_cb, user_data)
def callback_group_namelist_change(self, callback, user_data=None):
c_callback = CFUNCTYPE(None, c_void_p, c_int, c_int, c_uint8, c_void_p)
self.group_namelist_change_cb = c_callback(callback)
Tox.libtoxcore.tox_callback_group_namelist_change(self._tox_pointer, self.group_namelist_change_cb, user_data)

View File

@ -188,6 +188,17 @@ TOX_ERR_GET_PORT = {
'NOT_BOUND': 1, 'NOT_BOUND': 1,
} }
TOX_CHAT_CHANGE = {
'PEER_ADD': 0,
'PEER_DEL': 1,
'PEER_NAME': 2
}
TOX_GROUPCHAT_TYPE = {
'TEXT': 0,
'AV': 1
}
TOX_PUBLIC_KEY_SIZE = 32 TOX_PUBLIC_KEY_SIZE = 32
TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6 TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import sys
import re import re
program_version = '0.3.1' program_version = '0.4.3'
def cached(func): def cached(func):