from list_items import *
from PyQt5 import QtGui, QtWidgets
from friend import *
from settings import *
from toxcore_enums_and_consts import *
from ctypes import *
from util import log, Singleton, curr_directory
from tox_dns import tox_dns
from history import *
from file_transfers import *
import time
import calls
import avwidgets
import plugin_support
import basecontact
import items_factory
import cv2
import threading
from group_chat import *
import re
class Profile(basecontact.BaseContact, Singleton):
"""
Profile of current toxygen user. Contains friends list, tox instance
"""
def __init__(self, tox, screen):
"""
:param tox: tox instance
:param screen: ref to main screen
"""
basecontact.BaseContact.__init__(self,
tox.self_get_name(),
tox.self_get_status_message(),
screen.user_info,
tox.self_get_address())
Singleton.__init__(self)
self._screen = screen
self._messages = screen.messages
self._tox = tox
self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number)
self._call = calls.AV(tox.AV) # object with data about calls
self._call_widgets = {} # dict of incoming call widgets
self._incoming_calls = set()
self._load_history = True
self._waiting_for_reconnection = False
self._factory = items_factory.ItemsFactory(self._screen.friends_list, self._messages)
settings = Settings.get_instance()
self._sorting = settings['sorting']
self._show_avatars = settings['show_avatars']
self._filter_string = ''
self._friend_item_height = 40 if settings['compact_mode'] else 70
self._paused_file_transfers = dict(settings['paused_file_transfers'])
# key - file id, value: [path, friend number, is incoming, start position]
screen.online_contacts.setCurrentIndex(int(self._sorting))
aliases = settings['friends_aliases']
data = tox.self_get_friend_list()
self._history = History(tox.self_get_public_key()) # connection to db
self._contacts, self._active_friend = [], -1
for i in data: # creates list of friends
tox_id = tox.friend_get_public_key(i)
try:
alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1]
except:
alias = ''
item = self.create_friend_item()
name = alias or tox.friend_get_name(i) or tox_id
status_message = tox.friend_get_status_message(i)
if not self._history.friend_exists_in_db(tox_id):
self._history.add_friend_to_db(tox_id)
message_getter = self._history.messages_getter(tox_id)
friend = Friend(message_getter, i, name, status_message, item, tox_id)
friend.set_alias(alias)
self._contacts.append(friend)
if len(self._contacts):
self.set_active(0)
self.filtration_and_sorting(self._sorting)
# -----------------------------------------------------------------------------------------------------------------
# Edit current user's data
# -----------------------------------------------------------------------------------------------------------------
def change_status(self):
"""
Changes status of user (online, away, busy)
"""
if self._status is not None:
self.set_status((self._status + 1) % 3)
def set_status(self, status):
super(Profile, self).set_status(status)
if status is not None:
self._tox.self_set_status(status)
elif not self._waiting_for_reconnection:
self._waiting_for_reconnection = True
QtCore.QTimer.singleShot(50000, self.reconnect)
def set_name(self, value):
if self.name == value:
return
tmp = self.name
super(Profile, self).set_name(value.encode('utf-8'))
self._tox.self_set_name(self._name.encode('utf-8'))
message = QtWidgets.QApplication.translate("MainWindow", 'User {} is now known as {}')
message = message.format(tmp, value)
for friend in self._contacts:
friend.append_message(InfoMessage(message, time.time()))
if self._active_friend + 1:
self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE'])
self._messages.scrollToBottom()
def set_status_message(self, value):
super(Profile, self).set_status_message(value)
self._tox.self_set_status_message(self._status_message.encode('utf-8'))
def new_nospam(self):
"""Sets new nospam part of tox id"""
import random
self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32
self._tox_id = self._tox.self_get_address()
return self._tox_id
# -----------------------------------------------------------------------------------------------------------------
# Filtration
# -----------------------------------------------------------------------------------------------------------------
def filtration_and_sorting(self, sorting=0, filter_str=''):
"""
Filtration of friends list
:param sorting: 0 - no sort, 1 - online only, 2 - online first, 4 - by name
:param filter_str: show contacts which name contains this substring
"""
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)
if sorting & 4:
if not sorting & 2:
self._contacts = sorted(self._contacts, key=lambda x: x.name.lower())
else: # save results of prev sorting
online_friends = filter(lambda x: x.status is not None, self._contacts)
count = len(list(online_friends))
part1 = self._contacts[:count]
part2 = self._contacts[count:]
part1 = sorted(part1, key=lambda x: x.name.lower())
part2 = sorted(part2, key=lambda x: x.name.lower())
self._contacts = part1 + part2
else: # sort by number
online_friends = filter(lambda x: x.status is not None, self._contacts)
count = len(list(online_friends))
part1 = self._contacts[:count]
part2 = self._contacts[count:]
part1 = sorted(part1, key=lambda x: x.number)
part2 = sorted(part2, key=lambda x: x.number)
self._contacts = part1 + part2
self._screen.friends_list.clear()
for contact in self._contacts:
contact.set_widget(self.create_friend_item())
for index, friend in enumerate(self._contacts):
friend.visibility = (friend.status is not None or not (sorting & 1)) and (filter_str in friend.name.lower())
friend.visibility = friend.visibility or friend.messages or friend.actions
if friend.visibility:
self._screen.friends_list.item(index).setSizeHint(QtCore.QSize(250, self._friend_item_height))
else:
self._screen.friends_list.item(index).setSizeHint(QtCore.QSize(250, 0))
self._sorting, self._filter_string = sorting, filter_str
settings['sorting'] = self._sorting
settings.save()
self.set_active_by_number_and_type(number, is_friend)
def update_filtration(self):
"""
Update list of contacts when 1 of friends change connection status
"""
self.filtration_and_sorting(self._sorting, self._filter_string)
# -----------------------------------------------------------------------------------------------------------------
# Friend getters
# -----------------------------------------------------------------------------------------------------------------
def get_friend_by_number(self, num):
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):
return None
return self._contacts[num]
def get_curr_friend(self):
return self._contacts[self._active_friend] if self._active_friend + 1 else None
# -----------------------------------------------------------------------------------------------------------------
# Work with active friend
# -----------------------------------------------------------------------------------------------------------------
def get_active(self):
return self._active_friend
def set_active(self, value=None):
"""
Change current active friend or update info
:param value: number of new active friend in friend's list or None to update active user's data
"""
if value is None and self._active_friend == -1: # nothing to update
return
if value == -1: # all friends were deleted
self._screen.account_name.setText('')
self._screen.account_status.setText('')
self._screen.account_status.setToolTip('')
self._active_friend = -1
self._screen.account_avatar.setHidden(True)
self._messages.clear()
self._screen.messageEdit.clear()
return
try:
self.send_typing(False)
self._screen.typing.setVisible(False)
if value is not None:
if self._active_friend + 1 and self._active_friend != value:
try:
self.get_curr_friend().curr_text = self._screen.messageEdit.toPlainText()
except:
pass
friend = self._contacts[value]
friend.remove_invalid_unsent_files()
if self._active_friend != value:
self._screen.messageEdit.setPlainText(friend.curr_text)
self._active_friend = value
friend.reset_messages()
if not Settings.get_instance()['save_history']:
friend.delete_old_messages()
self._messages.clear()
friend.load_corr()
messages = friend.get_corr()[-PAGE_SIZE:]
self._load_history = False
for message in messages:
if message.get_type() <= 1:
data = message.get_data()
self.create_message_item(data[0],
data[2],
data[1],
data[3])
elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']:
if message.get_status() is None:
self.create_unsent_file_item(message)
continue
item = self.create_file_transfer_item(message)
if message.get_status() in ACTIVE_FILE_TRANSFERS: # active file transfer
try:
ft = self._file_transfers[(message.get_friend_number(), message.get_file_number())]
ft.set_state_changed_handler(item.update_transfer_state)
ft.signal()
except:
print('Incoming not started transfer - no info found')
elif message.get_type() == MESSAGE_TYPE['INLINE']: # inline
self.create_inline_item(message.get_data())
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:
self._screen.active_call()
elif value in self._incoming_calls:
self._screen.incoming_call()
else:
self._screen.call_finished()
else:
friend = self.get_curr_friend()
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'
os.chdir(os.path.dirname(avatar_path))
pixmap = QtGui.QPixmap(avatar_path)
self._screen.account_avatar.setPixmap(pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
except Exception as ex: # no friend found. ignore
log('Friend value: ' + str(value))
log('Error in set active: ' + str(ex))
raise
def set_active_by_number_and_type(self, number, is_friend):
for i in range(len(self._contacts)):
c = self._contacts[i]
if c.number == number and (type(c) is Friend == is_friend):
self._active_friend = i
break
active_friend = property(get_active, set_active)
def get_last_message(self):
if self._active_friend + 1:
return self.get_curr_friend().get_last_message_text()
else:
return ''
def get_active_number(self):
return self.get_curr_friend().number if self._active_friend + 1 else -1
def get_active_name(self):
return self.get_curr_friend().name if self._active_friend + 1 else ''
def is_active_online(self):
return self._active_friend + 1 and self.get_curr_friend().status is not None
def new_name(self, number, name):
friend = self.get_friend_by_number(number)
tmp = friend.name
friend.set_name(name)
name = str(name, 'utf-8')
if friend.name == name and tmp != name:
message = QtWidgets.QApplication.translate("MainWindow", 'User {} is now known as {}')
message = message.format(tmp, name)
friend.append_message(InfoMessage(message, time.time()))
friend.actions = True
if number == self.get_active_number():
self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE'])
self._messages.scrollToBottom()
self.set_active(None)
def update(self):
if self._active_friend + 1:
self.set_active(self._active_friend)
# -----------------------------------------------------------------------------------------------------------------
# Friend connection status callbacks
# -----------------------------------------------------------------------------------------------------------------
def send_files(self, friend_number):
friend = self.get_friend_by_number(friend_number)
friend.remove_invalid_unsent_files()
files = friend.get_unsent_files()
try:
for fl in files:
data = fl.get_data()
if data[1] is not None:
self.send_inline(data[1], data[0], friend_number, True)
else:
self.send_file(data[0], friend_number, True)
friend.clear_unsent_files()
for key in list(self._paused_file_transfers.keys()):
data = self._paused_file_transfers[key]
if not os.path.exists(data[0]):
del self._paused_file_transfers[key]
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() and self.is_active_a_friend():
self.update()
except Exception as ex:
print('Exception in file sending: ' + str(ex))
def friend_exit(self, friend_number):
"""
Friend with specified number quit
"""
self.get_friend_by_number(friend_number).status = None
self.friend_typing(friend_number, False)
if friend_number in self._call:
self._call.finish_call(friend_number, True)
for friend_num, file_num in list(self._file_transfers.keys()):
if friend_num == friend_number:
ft = self._file_transfers[(friend_num, file_num)]
if type(ft) is SendTransfer:
self._paused_file_transfers[ft.get_id()] = [ft.get_path(), friend_num, False, -1]
elif type(ft) is ReceiveTransfer and ft.state != TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']:
self._paused_file_transfers[ft.get_id()] = [ft.get_path(), friend_num, True, ft.total_size()]
self.cancel_transfer(friend_num, file_num, True)
# -----------------------------------------------------------------------------------------------------------------
# Typing notifications
# -----------------------------------------------------------------------------------------------------------------
def send_typing(self, typing):
"""
Send typing notification to a friend
"""
if Settings.get_instance()['typing_notifications'] and self._active_friend + 1:
try:
friend = self.get_curr_friend()
if friend.status is not None:
self._tox.self_set_typing(friend.number, typing)
except:
pass
def friend_typing(self, friend_number, typing):
"""
Display incoming typing notification
"""
if friend_number == self.get_active_number() and self.is_active_a_friend():
self._screen.typing.setVisible(typing)
# -----------------------------------------------------------------------------------------------------------------
# Private messages
# -----------------------------------------------------------------------------------------------------------------
def receipt(self):
i = 0
while i < self._messages.count() and not self._messages.itemWidget(self._messages.item(i)).mark_as_sent():
i += 1
def send_messages(self, friend_number):
"""
Send 'offline' messages to friend
"""
friend = self.get_friend_by_number(friend_number)
friend.load_corr()
messages = friend.get_unsent_messages()
try:
for message in messages:
self.split_and_send(friend_number, message.get_data()[-1], message.get_data()[0].encode('utf-8'))
friend.inc_receipts()
except Exception as ex:
log('Sending pending messages failed with ' + str(ex))
def split_and_send(self, number, message_type, message):
"""
Message splitting. Message length cannot be > TOX_MAX_MESSAGE_LENGTH
:param number: friend's number
:param message_type: type of message
:param message: message text
"""
while len(message) > TOX_MAX_MESSAGE_LENGTH:
size = TOX_MAX_MESSAGE_LENGTH * 4 // 5
last_part = message[size:TOX_MAX_MESSAGE_LENGTH]
if b' ' in last_part:
index = last_part.index(b' ')
elif b',' in last_part:
index = last_part.index(b',')
elif b'.' in last_part:
index = last_part.index(b'.')
else:
index = TOX_MAX_MESSAGE_LENGTH - size - 1
index += size + 1
self._tox.friend_send_message(number, message_type, message[:index])
message = message[index:]
self._tox.friend_send_message(number, message_type, message)
def new_message(self, friend_num, message_type, message):
"""
Current user gets new message
:param friend_num: friend_num of friend who sent message
:param message_type: message type - plain text or action message (/me)
:param message: text of message
"""
if friend_num == self.get_active_number()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()
self.get_curr_friend().append_message(
TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type))
else:
friend = self.get_friend_by_number(friend_num)
friend.inc_messages()
friend.append_message(
TextMessage(message, MESSAGE_OWNER['FRIEND'], time.time(), message_type))
if not friend.visibility:
self.update_filtration()
def send_message(self, text, friend_num=None):
"""
Send message
:param text: message text
:param friend_num: num of friend
"""
if 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 '):
plugin_support.PluginLoader.get_instance().command(text[8:])
self._screen.messageEdit.clear()
elif text and friend_num + 1:
if text.startswith('/me '):
message_type = TOX_MESSAGE_TYPE['ACTION']
text = text[4:]
else:
message_type = TOX_MESSAGE_TYPE['NORMAL']
friend = self.get_friend_by_number(friend_num)
friend.inc_receipts()
if friend.status is not None:
self.split_and_send(friend.number, message_type, text.encode('utf-8'))
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()
friend.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type))
def delete_message(self, time):
friend = self.get_curr_friend()
friend.delete_message(time)
self._history.delete_message(friend.tox_id, time)
self.update()
# -----------------------------------------------------------------------------------------------------------------
# History support
# -----------------------------------------------------------------------------------------------------------------
def save_history(self):
"""
Save history to db
"""
s = Settings.get_instance()
if hasattr(self, '_history'):
if s['save_history']:
for friend in 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']:
messages = friend.get_corr_for_saving()
else:
messages = friend.get_unsent_messages_for_saving()
self._history.delete_messages(friend.tox_id)
self._history.save_messages_to_db(friend.tox_id, messages)
unsent_messages = friend.get_unsent_messages()
unsent_time = unsent_messages[0].get_data()[2] if len(unsent_messages) else time.time() + 1
self._history.update_messages(friend.tox_id, unsent_time)
self._history.save()
del self._history
def clear_history(self, num=None, save_unsent=False):
"""
Clear chat history
"""
if num is not None:
friend = self._contacts[num]
friend.clear_corr(save_unsent)
if self._history.friend_exists_in_db(friend.tox_id):
self._history.delete_messages(friend.tox_id)
self._history.delete_friend_from_db(friend.tox_id)
else: # clear all history
for number in range(len(self._contacts)):
self.clear_history(number, save_unsent)
if num is None or num == self.get_active_number():
self.update()
def load_history(self):
"""
Tries to load next part of messages
"""
if not self._load_history:
return
self._load_history = False
friend = self.get_curr_friend()
friend.load_corr(False)
data = friend.get_corr()
if not data:
return
data.reverse()
data = data[self._messages.count():self._messages.count() + PAGE_SIZE]
for message in data:
if message.get_type() <= 1: # text message
data = message.get_data()
self.create_message_item(data[0],
data[2],
data[1],
data[3],
False)
elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']: # file transfer
if message.get_status() is None:
self.create_unsent_file_item(message)
continue
item = self.create_file_transfer_item(message, False)
if message.get_status() in ACTIVE_FILE_TRANSFERS: # active file transfer
try:
ft = self._file_transfers[(message.get_friend_number(), message.get_file_number())]
ft.set_state_changed_handler(item.update_transfer_state)
ft.signal()
except:
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)
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):
self._history.export(directory)
def export_history(self, num, as_text=True, _range=None):
friend = self._contacts[num]
if _range is None:
friend.load_all_corr()
corr = friend.get_corr()
elif _range[1] + 1:
corr = friend.get_corr()[_range[0]:_range[1] + 1]
else:
corr = friend.get_corr()[_range[0]:]
arr = []
new_line = '\n' if as_text else '
'
for message in corr:
if type(message) is TextMessage:
data = message.get_data()
if as_text:
x = '[{}] {}: {}\n'
else:
x = '[{}] {}: {}
'
arr.append(x.format(convert_time(data[2]) if data[1] != MESSAGE_OWNER['NOT_SENT'] else 'Unsent',
friend.name if data[1] == MESSAGE_OWNER['FRIEND'] else self.name,
data[0]))
s = new_line.join(arr)
if not as_text:
s = '