31 Commits

Author SHA1 Message Date
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
32 changed files with 1471 additions and 797 deletions

View File

@ -16,3 +16,4 @@ include toxygen/styles/*.qss
include toxygen/translations/*.qm
include toxygen/libs/libtox.dll
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
- Audio calls
- Video calls
- Group chats
- Plugins support
- Desktop sharing
- Chat history

View File

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

View File

@ -1,6 +1,6 @@
# 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

View File

@ -8,6 +8,6 @@ Animated smileys (.gif) are supported too.
# 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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,7 @@ class Call:
return self._out_video
def set_out_video(self, value):
self._in_video = value
self._out_video = value
out_video = property(get_out_video, set_out_video)
@ -144,8 +144,8 @@ class AV:
call = self._calls[friend_number]
call.is_active = True
call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A']
call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V']
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()
@ -153,6 +153,9 @@ class AV:
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
# -----------------------------------------------------------------------------------------------------------------
@ -281,10 +284,10 @@ class AV:
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:
print(e)
except Exception as e:
print(e)
except:
pass
except:
pass
time.sleep(0.01)

View File

@ -61,6 +61,8 @@ class Contact(basecontact.BaseContact):
"""
Get all chat history from db for current friend
"""
if self._message_getter is None:
return
data = list(self._message_getter.get_all())
if data is not None and len(data):
data.reverse()
@ -124,7 +126,7 @@ class Contact(basecontact.BaseContact):
# -----------------------------------------------------------------------------------------------------------------
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))
if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages:
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):
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):

View File

@ -66,3 +66,10 @@ class Friend(contact.Contact):
if self._receipts:
self._receipts -= 1
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):
return False

View File

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

View File

@ -5,7 +5,6 @@ from widgets import MultilineEdit, ComboBox
import plugin_support
from mainscreen_widgets import *
import settings
import platform
import toxes
@ -41,6 +40,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.menuAbout.setObjectName("menuAbout")
self.actionAdd_friend = QtWidgets.QAction(window)
self.actionAdd_gc = QtWidgets.QAction(window)
self.actionAdd_friend.setObjectName("actionAdd_friend")
self.actionprofilesettings = QtWidgets.QAction(window)
self.actionprofilesettings.setObjectName("actionprofilesettings")
@ -64,6 +64,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.reloadPlugins = QtWidgets.QAction(window)
self.lockApp = QtWidgets.QAction(window)
self.menuProfile.addAction(self.actionAdd_friend)
self.menuProfile.addAction(self.actionAdd_gc)
self.menuProfile.addAction(self.actionSettings)
self.menuProfile.addAction(self.lockApp)
self.menuSettings.addAction(self.actionPrivacy_settings)
@ -86,6 +87,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.actionAbout_program.triggered.connect(self.about_program)
self.actionNetwork.triggered.connect(self.network_settings)
self.actionAdd_friend.triggered.connect(self.add_contact)
self.actionAdd_gc.triggered.connect(self.create_gc)
self.actionSettings.triggered.connect(self.profile_settings)
self.actionPrivacy_settings.triggered.connect(self.privacy_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.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
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.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy"))
self.actionInterface_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Interface"))
@ -415,8 +418,10 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
import util
msgBox = QtWidgets.QMessageBox()
msgBox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
text = (QtWidgets.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.\nVersion: '))
msgBox.setText(text + util.program_version + '\nGitHub: https://github.com/toxygen-project/toxygen/')
text = (QtWidgets.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.<br>Version: '))
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_()
def network_settings(self):
@ -431,6 +436,9 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
self.a_c = AddContact(link or '')
self.a_c.show()
def create_gc(self):
self.profile.create_group_chat()
def profile_settings(self, *args):
self.p_s = ProfileSettings()
self.p_s.show()
@ -512,7 +520,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def send_file(self):
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')
name = QtWidgets.QFileDialog.getOpenFileName(self, choose, options=QtWidgets.QFileDialog.DontUseNativeDialog)
if name[0]:
@ -520,7 +528,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def send_screenshot(self, hide=False):
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.show()
if hide:
@ -538,7 +546,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def send_sticker(self):
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.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(),
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')
if item is not None:
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.triggered.connect(lambda: self.set_alias(num))
history_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Chat 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_name_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Name'))
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'))
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'))
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()
if plugins_loader is not None:
submenu = plugins_loader.get_menu(self.listMenu, num)
if len(submenu):
plug = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins'))
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))
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))
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_status_item.triggered.connect(lambda: self.copy_status(friend))
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):
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",
'Choose folder'),
'Choose file name'),
curr_directory(),
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
filter=extension,
options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
if directory:
name = 'exported_history_{}.{}'.format(convert_time(time.time()), 'txt' if as_text else 'html')
with open(directory + '/' + name, 'wt') as fl:
if file_name:
if not file_name.endswith('.' + extension):
file_name += '.' + extension
with open(file_name, 'wt') as fl:
fl.write(s)
def set_alias(self, num):
@ -675,6 +702,12 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
def clear_history(self, 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):
settings = Settings.get_instance()
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.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
# -----------------------------------------------------------------------------------------------------------------

View File

@ -3,6 +3,7 @@ from widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWi
from profile import Profile
import smileys
import util
import platform
class MessageArea(QtWidgets.QPlainTextEdit):
@ -34,6 +35,10 @@ class MessageArea(QtWidgets.QPlainTextEdit):
self.parent.send_message()
elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText():
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:
self.parent.profile.send_typing(True)
if self.timer.isActive():
@ -66,10 +71,18 @@ class MessageArea(QtWidgets.QPlainTextEdit):
def pasteEvent(self, text=None):
text = text or QtWidgets.QApplication.clipboard().text()
if text.startswith('file://'):
self.parent.profile.send_file(text[7:])
file_name = self.parse_file_name(text)
self.parent.profile.send_file(file_name)
else:
self.insertPlainText(text)
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):
@ -314,7 +327,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.')
elif num == 7:
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:
text = QtWidgets.QApplication.translate('WelcomeScreen',
'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):
self.setObjectName("NetworkSettings")
self.resize(300, 330)
self.setMinimumSize(QtCore.QSize(300, 330))
self.setMaximumSize(QtCore.QSize(300, 330))
self.setBaseSize(QtCore.QSize(300, 330))
self.resize(300, 400)
self.setMinimumSize(QtCore.QSize(300, 400))
self.setMaximumSize(QtCore.QSize(300, 400))
self.setBaseSize(QtCore.QSize(300, 400))
self.ipv = QtWidgets.QCheckBox(self)
self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22))
self.ipv.setObjectName("ipv")
@ -332,6 +332,9 @@ class NetworkSettings(CenteredWidget):
self.warning = QtWidgets.QLabel(self)
self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60))
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.proxy.stateChanged.connect(lambda x: self.activate())
self.activate()
@ -346,6 +349,7 @@ class NetworkSettings(CenteredWidget):
self.label_2.setText(QtWidgets.QApplication.translate("Form", "Port:"))
self.reconnect.setText(QtWidgets.QApplication.translate("NetworkSettings", "Restart TOX core"))
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"))
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_host'] = str(self.proxyip.text())
settings['proxy_port'] = int(self.proxyport.text())
settings['download_nodes_list'] = self.nodes.isChecked()
settings.save()
# recreate tox instance
Profile.get_instance().reset(self.reset)
@ -508,15 +513,17 @@ class NotificationsSettings(CenteredWidget):
def initUI(self):
self.setObjectName("notificationsForm")
self.resize(350, 180)
self.setMinimumSize(QtCore.QSize(350, 180))
self.setMaximumSize(QtCore.QSize(350, 180))
self.resize(350, 210)
self.setMinimumSize(QtCore.QSize(350, 210))
self.setMaximumSize(QtCore.QSize(350, 210))
self.enableNotifications = QtWidgets.QCheckBox(self)
self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18))
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.setGeometry(QtCore.QRect(10, 70, 340, 18))
self.groupNotifications = QtWidgets.QCheckBox(self)
self.groupNotifications.setGeometry(QtCore.QRect(10, 120, 340, 18))
font = QtGui.QFont()
s = Settings.get_instance()
font.setFamily(s['font'])
@ -524,8 +531,10 @@ class NotificationsSettings(CenteredWidget):
self.callsSound.setFont(font)
self.soundNotifications.setFont(font)
self.enableNotifications.setFont(font)
self.groupNotifications.setFont(font)
self.enableNotifications.setChecked(s['notifications'])
self.soundNotifications.setChecked(s['sound_notifications'])
self.groupNotifications.setChecked(s['group_notifications'])
self.callsSound.setChecked(s['calls_sound'])
self.retranslateUi()
QtCore.QMetaObject.connectSlotsByName(self)
@ -533,6 +542,7 @@ class NotificationsSettings(CenteredWidget):
def retranslateUi(self):
self.setWindowTitle(QtWidgets.QApplication.translate("notificationsForm", "Notification settings"))
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.soundNotifications.setText(QtWidgets.QApplication.translate("notificationsForm", "Enable sound notifications"))
@ -540,6 +550,7 @@ class NotificationsSettings(CenteredWidget):
settings = Settings.get_instance()
settings['notifications'] = self.enableNotifications.isChecked()
settings['sound_notifications'] = self.soundNotifications.isChecked()
settings['group_notifications'] = self.groupNotifications.isChecked()
settings['calls_sound'] = self.callsSound.isChecked()
settings.save()
@ -605,7 +616,7 @@ class InterfaceSettings(CenteredWidget):
self.messages_font_size_label.setFont(font)
self.messages_font_size = QtWidgets.QComboBox(self)
self.messages_font_size.setGeometry(QtCore.QRect(30, 330, 160, 30))
self.messages_font_size.addItems([str(x) for x in range(10, 19)])
self.messages_font_size.addItems([str(x) for x in range(10, 25)])
self.messages_font_size.setCurrentIndex(settings['message_font_size'] - 10)
self.unread = QtWidgets.QPushButton(self)
@ -810,7 +821,7 @@ class DesktopAreaSelectionWindow(RubberBandWindow):
rect = self.rubberband.geometry()
width, height = rect.width(), rect.height()
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()
@ -881,6 +892,8 @@ class VideoSettings(CenteredWidget):
self.desktopAreaSelection = DesktopAreaSelectionWindow(self)
def closeEvent(self, event):
if self.input.currentIndex() == 0:
return
try:
settings = Settings.get_instance()
settings.video['device'] = self.devices[self.input.currentIndex()]

View File

@ -5,7 +5,9 @@ MESSAGE_TYPE = {
'ACTION': 1,
'FILE_TRANSFER': 2,
'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
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):
"""
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 = []
for data in self._plugins.values():
try:
result.append([data[0].get_name(), # plugin full name
data[1], # is enabled
data[0].get_description(), # plugin description
data[0].get_short_name()]) # key - short unique name
except:
continue
return result
def plugin_window(self, key):

View File

@ -16,6 +16,8 @@ import basecontact
import items_factory
import cv2
import threading
from group_chat import *
import re
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.set_alias(alias)
self._contacts.append(friend)
if len(self._contacts):
self.set_active(0)
self.filtration_and_sorting(self._sorting)
# -----------------------------------------------------------------------------------------------------------------
@ -129,6 +133,7 @@ class Profile(basecontact.BaseContact, Singleton):
filter_str = filter_str.lower()
settings = Settings.get_instance()
number = self.get_active_number()
is_friend = self.is_active_a_friend()
if sorting > 1:
if sorting & 2:
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
settings['sorting'] = self._sorting
settings.save()
self.set_active_by_number(number)
self.set_active_by_number_and_type(number, is_friend)
def update_filtration(self):
"""
@ -177,7 +182,7 @@ class Profile(basecontact.BaseContact, Singleton):
# -----------------------------------------------------------------------------------------------------------------
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):
if num < 0 or num >= len(self._contacts):
@ -204,6 +209,7 @@ class Profile(basecontact.BaseContact, Singleton):
if value == -1: # all friends were deleted
self._screen.account_name.setText('')
self._screen.account_status.setText('')
self._screen.account_status.setToolTip('')
self._active_friend = -1
self._screen.account_avatar.setHidden(True)
self._messages.clear()
@ -251,12 +257,15 @@ class Profile(basecontact.BaseContact, Singleton):
print('Incoming not started transfer - no info found')
elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline
self.create_inline_item(message.get_data())
else: # info message
elif message.get_type() < 5: # info message
data = message.get_data()
self.create_message_item(data[0],
data[2],
'',
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._load_history = True
if value in self._call:
@ -270,6 +279,10 @@ class Profile(basecontact.BaseContact, Singleton):
self._screen.account_name.setText(friend.name)
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])
if not os.path.isfile(avatar_path): # load default image
avatar_path = curr_directory() + '/images/avatar.png'
@ -282,9 +295,10 @@ class Profile(basecontact.BaseContact, Singleton):
log('Error in set active: ' + str(ex))
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)):
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
break
@ -347,7 +361,7 @@ class Profile(basecontact.BaseContact, Singleton):
elif data[1] == friend_number and not data[2]:
self.send_file(data[0], friend_number, True, 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()
except Exception as ex:
print('Exception in file sending: ' + str(ex))
@ -389,7 +403,7 @@ class Profile(basecontact.BaseContact, Singleton):
"""
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)
# -----------------------------------------------------------------------------------------------------------------
@ -445,7 +459,7 @@ class Profile(basecontact.BaseContact, Singleton):
:param message_type: message type - plain text or action message (/me)
:param message: text of message
"""
if friend_num == self.get_active_number(): # add message to list
if friend_num == self.get_active_number()and self.is_active_a_friend(): # add message to list
t = time.time()
self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type)
self._messages.scrollToBottom()
@ -465,6 +479,9 @@ class Profile(basecontact.BaseContact, Singleton):
:param text: message text
:param friend_num: num of friend
"""
if not self.is_active_a_friend():
self.send_gc_message(text)
return
if friend_num is None:
friend_num = self.get_active_number()
if text.startswith('/plugin '):
@ -480,8 +497,8 @@ class Profile(basecontact.BaseContact, Singleton):
friend.inc_receipts()
if friend.status is not None:
self.split_and_send(friend.number, message_type, text.encode('utf-8'))
if friend.number == self.get_active_number():
t = time.time()
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._screen.messageEdit.clear()
self._messages.scrollToBottom()
@ -504,7 +521,7 @@ class Profile(basecontact.BaseContact, Singleton):
s = Settings.get_instance()
if hasattr(self, '_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):
self._history.add_friend_to_db(friend.tox_id)
if not s['save_unsent_only']:
@ -571,13 +588,16 @@ class Profile(basecontact.BaseContact, Singleton):
print('Incoming not started transfer - no info found')
elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline image
self.create_inline_item(message.get_data(), False)
else: # info message
elif message.get_type() < 5: # info message
data = message.get_data()
self.create_message_item(data[0],
data[2],
'',
data[3],
False)
else:
data = message.get_data()
self.create_gc_message_item(data[0], data[2], data[1], data[4], data[3])
self._load_history = True
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'],
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):
data = list(tm.get_data())
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:
pass
settings.save()
if num == self.get_active_number():
if num == self.get_active_number() and self.is_active_a_friend():
self.update()
def friend_public_key(self, num):
@ -843,8 +873,11 @@ class Profile(basecontact.BaseContact, Singleton):
Recreate tox instance
:param restart: method which calls restart and returns new tox instance
"""
for friend in self._contacts:
self.friend_exit(friend.number)
for contact in self._contacts:
if type(contact) is Friend:
self.friend_exit(contact.number)
else:
self.leave_gc(contact.number)
self._call.stop()
del self._call
del self._tox
@ -863,7 +896,7 @@ class Profile(basecontact.BaseContact, Singleton):
QtCore.QTimer.singleShot(50000, self.reconnect)
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)
for i in range(len(self._contacts)):
del self._contacts[0]
@ -936,7 +969,7 @@ class Profile(basecontact.BaseContact, Singleton):
friend_number,
file_number)
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)
if accepted:
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:
if not already_cancelled:
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
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)
def cancel_not_started_transfer(self, time):
self.get_curr_friend().delete_one_unsent_file(time)
def cancel_not_started_transfer(self, cancel_time):
self.get_curr_friend().delete_one_unsent_file(cancel_time)
self.update()
def pause_transfer(self, friend_number, file_number, by_friend=False):
@ -1121,7 +1155,7 @@ class Profile(basecontact.BaseContact, Singleton):
t = type(transfer)
if t is ReceiveAvatar:
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)
elif t is ReceiveToBuffer or (t is SendFromBuffer and Settings.get_instance()['allow_inline']): # inline image
print('inline')
@ -1129,7 +1163,7 @@ class Profile(basecontact.BaseContact, Singleton):
i = self.get_friend_by_number(friend_number).update_transfer_data(file_number,
TOX_FILE_TRANSFER_STATE['FINISHED'],
inline)
if friend_number == self.get_active_number():
if friend_number == self.get_active_number() and self.is_active_a_friend():
count = self._messages.count()
if count + i + 1 >= 0:
elem = QtWidgets.QListWidgetItem()
@ -1171,7 +1205,7 @@ class Profile(basecontact.BaseContact, Singleton):
ra.set_transfer_finished_handler(self.transfer_finished)
else:
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)
def reset_avatar(self):
@ -1196,6 +1230,8 @@ class Profile(basecontact.BaseContact, Singleton):
def call_click(self, audio=True, video=False):
"""User clicked audio button in main window"""
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 not Settings.get_instance().audio['enabled']:
return
@ -1213,7 +1249,7 @@ class Profile(basecontact.BaseContact, Singleton):
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']:
return
@ -1254,17 +1290,149 @@ class Profile(basecontact.BaseContact, Singleton):
else:
text = QtWidgets.QApplication.translate("incoming_call", "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
if hasattr(self, '_call_widget'):
self._call_widget[friend_number].close()
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.append_message(InfoMessage(text, time.time()))
if friend_number == self.get_active_number():
self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE'])
self._messages.scrollToBottom()
# -----------------------------------------------------------------------------------------------------------------
# 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):
"""
@ -1275,6 +1443,8 @@ def tox_factory(data=None, settings=None):
if settings is None:
settings = Settings.get_default_settings()
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.proxy_type = settings['proxy_type']
tox_options.contents.proxy_host = bytes(settings['proxy_host'], 'UTF-8')

View File

@ -104,7 +104,7 @@ class Settings(dict, Singleton):
"""
return {
'theme': 'dark',
'ipv6_enabled': True,
'ipv6_enabled': False,
'udp_enabled': True,
'proxy_type': 0,
'proxy_host': '127.0.0.1',
@ -144,7 +144,9 @@ class Settings(dict, Singleton):
'show_welcome_screen': True,
'close_to_tray': False,
'font': 'Times New Roman',
'update': 1
'update': 1,
'group_notifications': True,
'download_nodes_list': False
}
@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 create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8
from ctypes import *
from toxcore_enums_and_consts import *
from toxav import ToxAV
from libtox import LibToxCore
@ -91,6 +90,11 @@ class Tox:
self.file_recv_chunk_cb = None
self.friend_lossy_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)
@ -1509,3 +1513,89 @@ class Tox:
return result
elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
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,
}
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_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
program_version = '0.3.1'
program_version = '0.4.2'
def cached(func):