diff --git a/toxygen/basecontact.py b/toxygen/basecontact.py index 1ae1a2b..a484173 100644 --- a/toxygen/basecontact.py +++ b/toxygen/basecontact.py @@ -91,10 +91,11 @@ class BaseContact: if not os.path.isfile(avatar_path): # load default image avatar_path = default_path os.chdir(curr_directory() + '/images/') - pixmap = QtGui.QPixmap(QtCore.QSize(64, 64)) + width = self._widget.avatar_label.width() + pixmap = QtGui.QPixmap(QtCore.QSize(width, width)) pixmap.load(avatar_path) self._widget.avatar_label.setScaledContents(False) - self._widget.avatar_label.setPixmap(pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio)) + self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio)) self._widget.avatar_label.repaint() def reset_avatar(self): diff --git a/toxygen/callbacks.py b/toxygen/callbacks.py index fc13b99..0ce0fd6 100644 --- a/toxygen/callbacks.py +++ b/toxygen/callbacks.py @@ -287,6 +287,20 @@ def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, aud rate) +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - group chats +# ----------------------------------------------------------------------------------------------------------------- + +def group_message(tox, group_number, peer_id, message, length, user_data): + pass + + +def group_invite(tox, friend_number, invite_data, length, user_data): + invoke_in_main_thread(Profile.get_instance().process_group_invite, + friend_number, + invite_data[:length]) + + # ----------------------------------------------------------------------------------------------------------------- # Callbacks - initialization # ----------------------------------------------------------------------------------------------------------------- diff --git a/toxygen/contact.py b/toxygen/contact.py index 0743e70..ac41dbe 100644 --- a/toxygen/contact.py +++ b/toxygen/contact.py @@ -5,9 +5,6 @@ except ImportError: import basecontact from messages import * from history import * -from settings import ProfileHelper -from toxcore_enums_and_consts import * -from util import curr_directory class Contact(basecontact.BaseContact): @@ -17,7 +14,7 @@ class Contact(basecontact.BaseContact): widget - widget for update """ - def __init__(self, message_getter, name, status_message, widget, tox_id): + def __init__(self, number, message_getter, name, status_message, widget, tox_id): """ :param name: name, example: 'Toxygen user' :param status_message: status message, example: 'Toxing on Toxygen' @@ -28,6 +25,7 @@ class Contact(basecontact.BaseContact): self._message_getter = message_getter self._new_messages = False self._visible = True + self._number = number self._corr = [] self._unsaved_messages = 0 self._history_loaded = self._new_actions = False @@ -150,40 +148,5 @@ class Contact(basecontact.BaseContact): self._widget.messages.update(self._new_messages) self._widget.connection_status.update(self.status, False) - # ----------------------------------------------------------------------------------------------------------------- - # Avatars - # ----------------------------------------------------------------------------------------------------------------- - - def load_avatar(self): - """ - Tries to load avatar of contact or uses default avatar - """ - avatar_path = '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - os.chdir(ProfileHelper.get_path() + 'avatars/') - if not os.path.isfile(avatar_path): # load default image - avatar_path = 'avatar.png' - os.chdir(curr_directory() + '/images/') - width = self._widget.avatar_label.width() - pixmap = QtGui.QPixmap(QtCore.QSize(width, width)) - pixmap.load(avatar_path) - self._widget.avatar_label.setScaledContents(False) - self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio)) - self._widget.avatar_label.repaint() - - def reset_avatar(self): - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - if os.path.isfile(avatar_path): - os.remove(avatar_path) - self.load_avatar() - - def set_avatar(self, avatar): - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) - with open(avatar_path, 'wb') as f: - f.write(avatar) - self.load_avatar() - - def get_pixmap(self): - return self._widget.avatar_label.pixmap() - messages = property(get_messages) diff --git a/toxygen/friend.py b/toxygen/friend.py index 6f2dcf4..7347671 100644 --- a/toxygen/friend.py +++ b/toxygen/friend.py @@ -10,13 +10,11 @@ class Friend(contact.Contact): Friend in list of friends. Can be hidden, properties 'has unread messages' and 'has alias' added """ - def __init__(self, message_getter, number, *args): + def __init__(self, *args): """ - :param message_getter: gets messages from db :param number: number of friend. """ - super(Friend, self).__init__(message_getter, *args) - self._number = number + super(Friend, self).__init__(*args) self._alias = False self._receipts = 0 diff --git a/toxygen/groupchat.py b/toxygen/groupchat.py index fbc6633..1038407 100644 --- a/toxygen/groupchat.py +++ b/toxygen/groupchat.py @@ -3,10 +3,11 @@ import contact class GroupChat(contact.Contact): - def __init__(self, group_id, tox, *args): + def __init__(self, tox, *args): super().__init__(*args) - self._id = group_id self._tox = tox def load_avatar(self, default_path='group.png'): super().load_avatar(default_path) + +# TODO: get peers list and add other methods diff --git a/toxygen/mainscreen.py b/toxygen/mainscreen.py index 047ddc9..cc4f648 100644 --- a/toxygen/mainscreen.py +++ b/toxygen/mainscreen.py @@ -82,7 +82,7 @@ class MainWindow(QtGui.QMainWindow): self.actionAbout_program.triggered.connect(self.about_program) self.actionNetwork.triggered.connect(self.network_settings) self.actionAdd_friend.triggered.connect(self.add_contact) - self.actionSettings.triggered.connect(self.profilesettings) + self.actionSettings.triggered.connect(self.profile_settings) self.actionPrivacy_settings.triggered.connect(self.privacy_settings) self.actionInterface_settings.triggered.connect(self.interface_settings) self.actionNotifications.triggered.connect(self.notification_settings) @@ -205,9 +205,9 @@ class MainWindow(QtGui.QMainWindow): Form.status_message.setObjectName("status_message") self.connection_status = Form.connection_status = StatusCircle(Form) Form.connection_status.setGeometry(QtCore.QRect(230, 35, 32, 32)) - self.avatar_label.mouseReleaseEvent = self.profilesettings - self.status_message.mouseReleaseEvent = self.profilesettings - self.name.mouseReleaseEvent = self.profilesettings + self.avatar_label.mouseReleaseEvent = self.profile_settings + self.status_message.mouseReleaseEvent = self.profile_settings + self.name.mouseReleaseEvent = self.profile_settings self.connection_status.raise_() Form.connection_status.setObjectName("connection_status") @@ -232,6 +232,11 @@ class MainWindow(QtGui.QMainWindow): font.setBold(False) self.account_status.setFont(font) self.account_status.setObjectName("account_status") + + self.account_status.mouseReleaseEvent = self.show_chat_menu + self.account_name.mouseReleaseEvent = self.show_chat_menu + self.account_avatar.mouseReleaseEvent = self.show_chat_menu + self.callButton = QtGui.QPushButton(Form) self.callButton.setGeometry(QtCore.QRect(550, 30, 50, 50)) self.callButton.setObjectName("callButton") @@ -389,7 +394,7 @@ class MainWindow(QtGui.QMainWindow): self.a_c = AddContact(link) self.a_c.show() - def profilesettings(self, *args): + def profile_settings(self, *args): self.p_s = ProfileSettings() self.p_s.show() @@ -455,6 +460,11 @@ class MainWindow(QtGui.QMainWindow): self.gc = AddGroupchat() self.gc.show() + def show_chat_menu(self): + pr = Profile.get_instance() + if not pr.is_active_a_friend(): + pass # TODO: show list of users in chat + # ----------------------------------------------------------------------------------------------------------------- # Messages, calls and file transfers # ----------------------------------------------------------------------------------------------------------------- @@ -533,29 +543,32 @@ class MainWindow(QtGui.QMainWindow): auto = QtGui.QApplication.translate("MainWindow", 'Disallow auto accept', None, QtGui.QApplication.UnicodeUTF8) if allowed else QtGui.QApplication.translate("MainWindow", 'Allow auto accept', None, QtGui.QApplication.UnicodeUTF8) if item is not None: self.listMenu = QtGui.QMenu() - set_alias_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Set alias', None, QtGui.QApplication.UnicodeUTF8)) - clear_history_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Clear history', None, QtGui.QApplication.UnicodeUTF8)) - copy_menu = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Copy', None, QtGui.QApplication.UnicodeUTF8)) - copy_name_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Name', None, QtGui.QApplication.UnicodeUTF8)) - copy_status_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Status message', None, QtGui.QApplication.UnicodeUTF8)) - copy_key_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Public key', None, QtGui.QApplication.UnicodeUTF8)) + if type(friend) is Friend: # TODO: add `invite to gc` submenu + set_alias_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Set alias', None, QtGui.QApplication.UnicodeUTF8)) + clear_history_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Clear history', None, QtGui.QApplication.UnicodeUTF8)) + copy_menu = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Copy', None, QtGui.QApplication.UnicodeUTF8)) + copy_name_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Name', None, QtGui.QApplication.UnicodeUTF8)) + copy_status_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Status message', None, QtGui.QApplication.UnicodeUTF8)) + copy_key_item = copy_menu.addAction(QtGui.QApplication.translate("MainWindow", 'Public key', None, QtGui.QApplication.UnicodeUTF8)) - auto_accept_item = self.listMenu.addAction(auto) - remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8)) - notes_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Notes', None, QtGui.QApplication.UnicodeUTF8)) + auto_accept_item = self.listMenu.addAction(auto) + remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8)) + notes_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Notes', None, QtGui.QApplication.UnicodeUTF8)) - submenu = plugin_support.PluginLoader.get_instance().get_menu(self.listMenu, num) - if len(submenu): - plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8)) - plug.addActions(submenu) - self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) - self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) - self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) - self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num)) - self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed)) - self.connect(notes_item, QtCore.SIGNAL("triggered()"), lambda: self.show_note(friend)) - self.connect(copy_name_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_name(friend)) - self.connect(copy_status_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_status(friend)) + submenu = plugin_support.PluginLoader.get_instance().get_menu(self.listMenu, num) + if len(submenu): + plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8)) + plug.addActions(submenu) + self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) + self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) + self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) + self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num)) + self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed)) + self.connect(notes_item, QtCore.SIGNAL("triggered()"), lambda: self.show_note(friend)) + self.connect(copy_name_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_name(friend)) + self.connect(copy_status_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_status(friend)) + else: + pass # TODO: add menu for gc parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) self.listMenu.move(parent_position + pos) self.listMenu.show() diff --git a/toxygen/profile.py b/toxygen/profile.py index fae590f..0403c0b 100644 --- a/toxygen/profile.py +++ b/toxygen/profile.py @@ -60,7 +60,7 @@ class Profile(basecontact.BaseContact, Singleton): 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 = Friend(i, message_getter, name, status_message, item, tox_id) friend.set_alias(alias) self._friends_and_gc.append(friend) self.filtration(self._show_online) @@ -138,7 +138,10 @@ class Profile(basecontact.BaseContact, Singleton): self.filtration(self._show_online, self._filter_string) def get_friend_by_number(self, num): - return list(filter(lambda x: x.number == num, self._friends_and_gc))[0] + return list(filter(lambda x: x.number == num and type(x) is Friend, self._friends_and_gc))[0] + + def get_gc_by_number(self, num): + return list(filter(lambda x: x.number == num and type(x) is not Friend, self._friends_and_gc))[0] def get_friend_or_gc(self, num): return self._friends_and_gc[num] @@ -350,12 +353,13 @@ class Profile(basecontact.BaseContact, Singleton): except: pass - def split_and_send(self, number, message_type, message): + def split_and_send(self, number, message_type, message, is_group=False): """ Message splitting - :param number: friend's number + :param number: friend or gc number :param message_type: type of message :param message: message text + :param is_group: send to group """ while len(message) > TOX_MAX_MESSAGE_LENGTH: size = TOX_MAX_MESSAGE_LENGTH * 4 / 5 @@ -369,9 +373,15 @@ class Profile(basecontact.BaseContact, Singleton): else: index = TOX_MAX_MESSAGE_LENGTH - size - 1 index += size + 1 - self._tox.friend_send_message(number, message_type, message[:index]) + if not is_group: + self._tox.friend_send_message(number, message_type, message[:index]) + else: + self._tox.group_send_message(number, message_type, message[:index]) message = message[index:] - self._tox.friend_send_message(number, message_type, message) + if not is_group: + self._tox.friend_send_message(number, message_type, message) + else: + self._tox.group_send_message(number, message_type, message) def new_message(self, friend_num, message_type, message): """ @@ -394,18 +404,20 @@ class Profile(basecontact.BaseContact, Singleton): if not friend.visibility: self.update_filtration() - def send_message(self, text, friend_num=None): + def send_message(self, text, number=None, is_gc=False): """ Send message :param text: message text - :param friend_num: num of friend + :param number: num of friend or gc + :param is_gc: is group chat """ - if friend_num is None: - friend_num = self.get_active_number() + if number is None: + number = self.get_active_number() + is_gc = not self.is_active_a_friend() if text.startswith('/plugin '): plugin_support.PluginLoader.get_instance().command(text[8:]) self._screen.messageEdit.clear() - elif text and friend_num + 1: + elif text and number + 1: text = ''.join(c if c <= '\u10FFFF' else '\u25AF' for c in text) if text.startswith('/me '): @@ -414,22 +426,30 @@ class Profile(basecontact.BaseContact, Singleton): else: message_type = TOX_MESSAGE_TYPE['NORMAL'] - friend = self.get_friend_by_number(friend_num) - # TODO: send to gc - # friend = self._friends_and_gc[self._active_friend_or_gc] - - friend.inc_receipts() - if friend.status is not None: - self.split_and_send(friend.number, message_type, text.encode('utf-8')) + if not is_gc: + friend_or_gc = self.get_friend_by_number(number) + else: + friend_or_gc = self.get_gc_by_number(number) t = time.time() - if friend.number == self.get_active_number(): - 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)) + + if not is_gc: + friend_or_gc.inc_receipts() + if friend_or_gc.status is not None: + self.split_and_send(friend_or_gc.number, message_type, text.encode('utf-8')) + if friend_or_gc.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() + else: + self.split_and_send(friend_or_gc.number, message_type, text.encode('utf-8'), True) + if friend_or_gc.number == self.get_active_number() and not self.is_active_a_friend(): + self.create_message_item(text, t, MESSAGE_OWNER['ME'], message_type) + self._screen.messageEdit.clear() + self._messages.scrollToBottom() + friend_or_gc.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type)) def delete_message(self, time): - friend = self._friends[self._active_friend] + friend = self._friends_and_gc[self._active_friend_or_gc] friend.delete_message(time) self._history.delete_message(friend.tox_id, time) self.update() @@ -1198,12 +1218,43 @@ class Profile(basecontact.BaseContact, Singleton): # Group chats support # ----------------------------------------------------------------------------------------------------------------- + def add_gc(self, num): + tox_id = self._tox.group_get_chat_id(num) + name = self._tox.group_get_name(num) + topic = self._tox.group_get_topic(num) + item = self.create_friend_item() + try: + 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) + except Exception as ex: # something is wrong + log('Accept friend request failed! ' + str(ex)) + message_getter = None + gc = GroupChat(self._tox, message_getter, num, name, topic, item, tox_id) + self._friends_and_gc.append(gc) + def create_gc(self, name, is_public, password): privacy_state = TOX_GROUP_PRIVACY_STATE['TOX_GROUP_PRIVACY_STATE_PUBLIC'] if is_public else TOX_GROUP_PRIVACY_STATE['TOX_GROUP_PRIVACY_STATE_PRIVATE'] num = self._tox.group_new(privacy_state, bytes(name, 'utf-8')) if password: - self._tox.group_founder_set_password(num, password) - # self._friends_and_gc.append(Groupchat(num, self._tox, )) + self._tox.group_founder_set_password(num, bytes(password, 'utf-8')) + self.add_gc(num) + + def process_group_invite(self, friend_num, data): + # TODO: add info to list and support password + try: + text = QtGui.QApplication.translate('MainWindow', 'User {} invites you to group', + None, QtGui.QApplication.UnicodeUTF8) + info = text.format(self.get_friend_by_number(friend_num).name) + fr_req = QtGui.QApplication.translate('MainWindow', 'Group chat invite', None, QtGui.QApplication.UnicodeUTF8) + reply = QtGui.QMessageBox.question(None, fr_req, info, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.Yes: # accepted + num = self._tox.group_invite_accept(data) + data = self._tox.get_savedata() + ProfileHelper.get_instance().save_profile(data) + self.add_gc(num) + except Exception as ex: # something is wrong + log('Accept group chat invite failed! ' + str(ex)) def tox_factory(data=None, settings=None): diff --git a/toxygen/tox.py b/toxygen/tox.py index 90b3ddf..41fce73 100644 --- a/toxygen/tox.py +++ b/toxygen/tox.py @@ -1893,13 +1893,14 @@ class Tox: """ Write the Chat ID designated by the given group number to a byte array. `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes. - :return true on success. + :return chat id. """ error = c_int() + buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE) result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer, groupnumber, - create_string_buffer(TOX_GROUP_CHAT_ID_SIZE), byref(error)) - return result + buff, byref(error)) + return bin_to_string(buff[:TOX_GROUP_CHAT_ID_SIZE], TOX_GROUP_CHAT_ID_SIZE) def group_get_number_groups(self): """ @@ -2093,6 +2094,15 @@ class Tox: """ Set the callback for the `group_message` event. Pass NULL to unset. This event is triggered when the client receives a group message. + + Callback: python function with params: + tox Tox* instance + groupnumber The group number of the group the message is intended for. + peer_id The ID of the peer who sent the message. + type The type of message (normal, action, ...). + message The message data. + length The length of the message. + user_data - user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_char_p, c_size_t, c_void_p) @@ -2140,7 +2150,7 @@ class Tox: result = Tox.libtoxcore.tox_group_invite_friend(self._tox_pointer, groupnumber, friend_number, byref(error)) return result - def group_invite_accept(self, invite_data, password): + def group_invite_accept(self, invite_data, password=None): """ Accept an invite to a group chat that the client previously received from a friend. The invite is only valid while the inviter is present in the group. @@ -2151,8 +2161,10 @@ class Tox: """ error = c_int() - result = Tox.libtoxcore.tox_group_invite_accept(self._tox_pointer, invite_data, len(invite_data), password, - len(password), byref(error)) + result = Tox.libtoxcore.tox_group_invite_accept(self._tox_pointer, invite_data, len(invite_data), + password, + len(password) if password is not None else 0, + byref(error)) return result def callback_group_invite(self, callback, user_data): @@ -2161,6 +2173,13 @@ class Tox: This event is triggered when the client receives a group invite from a friend. The client must store invite_data which is used to join the group via tox_group_invite_accept. + + Callback: python function with params: + tox - Tox* + friend_number The friend number of the contact who sent the invite. + invite_data The invite data. + length The length of invite_data. + user_data - user data """ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p)