diff --git a/toxygen/basecontact.py b/toxygen/basecontact.py new file mode 100644 index 0000000..b0accb3 --- /dev/null +++ b/toxygen/basecontact.py @@ -0,0 +1,114 @@ +import os +from settings import * +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui +from toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE + + +class BaseContact: + """ + Class encapsulating TOX contact + Properties: name (alias of contact or name), status_message, status (connection status) + widget - widget for update + """ + + def __init__(self, name, status_message, widget, tox_id): + """ + :param name: name, example: 'Toxygen user' + :param status_message: status message, example: 'Toxing on Toxygen' + :param widget: ContactItem instance + :param tox_id: tox id of contact + """ + self._name, self._status_message = name, status_message + self._status, self._widget = None, widget + self._widget.name.setText(name) + self._widget.status_message.setText(status_message) + self._tox_id = tox_id + self.load_avatar() + + # ----------------------------------------------------------------------------------------------------------------- + # Name - current name or alias of user + # ----------------------------------------------------------------------------------------------------------------- + + def get_name(self): + return self._name + + def set_name(self, value): + self._name = str(value, 'utf-8') + self._widget.name.setText(self._name) + self._widget.name.repaint() + + name = property(get_name, set_name) + + # ----------------------------------------------------------------------------------------------------------------- + # Status message or group topic + # ----------------------------------------------------------------------------------------------------------------- + + def get_status_message(self): + return self._status_message + + def set_status_message(self, value): + self._status_message = str(value, 'utf-8') + self._widget.status_message.setText(self._status_message) + self._widget.status_message.repaint() + + status_message = property(get_status_message, set_status_message) + + # ----------------------------------------------------------------------------------------------------------------- + # Status + # ----------------------------------------------------------------------------------------------------------------- + + def get_status(self): + return self._status + + def set_status(self, value): + self._status = value + self._widget.connection_status.update(value) + + status = property(get_status, set_status) + + # ----------------------------------------------------------------------------------------------------------------- + # TOX ID. WARNING: for friend it will return public key, for profile - full address + # ----------------------------------------------------------------------------------------------------------------- + + def get_tox_id(self): + return self._tox_id + + tox_id = property(get_tox_id) + + # ----------------------------------------------------------------------------------------------------------------- + # Avatars + # ----------------------------------------------------------------------------------------------------------------- + + def load_avatar(self, default_path='avatar.png'): + """ + 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 = default_path + 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() diff --git a/toxygen/callbacks.py b/toxygen/callbacks.py index fc13b99..450df1e 100644 --- a/toxygen/callbacks.py +++ b/toxygen/callbacks.py @@ -287,6 +287,49 @@ def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, aud rate) +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - group chats +# ----------------------------------------------------------------------------------------------------------------- + +def group_message(window, tray, tox): + """ + New message in group chat + """ + def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data): + profile = Profile.get_instance() + settings = Settings.get_instance() + message = str(message[:length], 'utf-8') + invoke_in_main_thread(profile.new_message, group_number, message_type, message, True, peer_id) + if not window.isActiveWindow(): + bl = settings['notify_all_gc'] or profile.name in message + name = tox.group_peer_get_name(group_number, peer_id) + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['MESSAGE']) + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png')) + return wrapped + + +def group_invite(tox, friend_number, invite_data, length, user_data): + invoke_in_main_thread(Profile.get_instance().process_group_invite, + friend_number, + bytes(invite_data[:length])) + + +def group_self_join(tox, group_number, user_data): + pr = Profile.get_instance() + gc = pr.get_gc_by_number(group_number) + invoke_in_main_thread(gc.set_status, TOX_USER_STATUS['NONE']) + if not pr.is_active_a_friend() and pr.get_active_number() == group_number: + invoke_in_main_thread(pr.set_active) + + +def group_peer_join(tox, group_number, peer_id, user_data): + gc = Profile.get_instance().get_gc_by_number(group_number) + gc.add_peer(peer_id) + + # ----------------------------------------------------------------------------------------------------------------- # Callbacks - initialization # ----------------------------------------------------------------------------------------------------------------- @@ -323,3 +366,8 @@ 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_message(group_message(window, tray, tox), 0) + tox.callback_group_invite(group_invite, 0) + tox.callback_group_self_join(group_self_join, 0) + tox.callback_group_peer_join(group_peer_join, 0) + diff --git a/toxygen/contact.py b/toxygen/contact.py index f4b3f9a..03fad92 100644 --- a/toxygen/contact.py +++ b/toxygen/contact.py @@ -1,114 +1,212 @@ -import os -from settings import * try: from PySide import QtCore, QtGui except ImportError: from PyQt4 import QtCore, QtGui -from toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE +import basecontact +from messages import * +from history import * +import file_transfers as ft +import util -class Contact: +class Contact(basecontact.BaseContact): """ Class encapsulating TOX contact Properties: name (alias of contact or name), status_message, status (connection status) widget - widget for update """ - def __init__(self, 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' :param widget: ContactItem instance :param tox_id: tox id of contact """ - self._name, self._status_message = name, status_message - self._status, self._widget = None, widget - self._widget.name.setText(name) - self._widget.status_message.setText(status_message) - self._tox_id = tox_id - self.load_avatar() + super().__init__(name, status_message, widget, tox_id) + self._message_getter = message_getter + self._new_messages = False + self._visible = True + self._alias = False + self._number = number + self._corr = [] + self._unsaved_messages = 0 + self._history_loaded = self._new_actions = False + self._curr_text = '' + + def __del__(self): + self.set_visibility(False) + del self._widget + if hasattr(self, '_message_getter'): + del self._message_getter + + def load_corr(self, first_time=True): + """ + :param first_time: friend became active, load first part of messages + """ + if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')): + return + data = list(self._message_getter.get(PAGE_SIZE)) + if data is not None and len(data): + data.reverse() + else: + return + data = list(map(lambda tupl: TextMessage(*tupl), data)) + self._corr = data + self._corr + self._history_loaded = True + + def get_corr_for_saving(self): + """ + Get data to save in db + :return: list of unsaved messages or [] + """ + messages = list(filter(lambda x: x.get_type() <= 1, self._corr)) + return list(map(lambda x: x.get_data(), messages[-self._unsaved_messages:])) if self._unsaved_messages else [] + + def get_corr(self): + return self._corr[:] + + def append_message(self, message): + """ + :param message: text or file transfer message + """ + self._corr.append(message) + if message.get_type() <= 1: + self._unsaved_messages += 1 + + def get_last_message_text(self): + messages = list(filter(lambda x: x.get_type() <= 1 and x.get_owner() != MESSAGE_OWNER['FRIEND'], self._corr)) + if messages: + return messages[-1].get_data()[0] + else: + return '' + + def get_unsent_messages(self): + """ + :return list of unsent messages + """ + messages = filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) + return list(messages) + + def get_unsent_messages_for_saving(self): + """ + :return list of unsent messages for saving + """ + messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) + return list(map(lambda x: x.get_data(), messages)) + + def delete_message(self, time): + elem = list(filter(lambda x: type(x) is TextMessage 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:]: + self._unsaved_messages -= 1 + self._corr.remove(elem) + + def delete_old_messages(self): + old = filter(lambda x: x.get_type() in (2, 3) and (x.get_status() >= 2 or x.get_status() is None), + self._corr[:-SAVE_MESSAGES]) + old = list(old) + l = max(len(self._corr) - SAVE_MESSAGES, 0) - len(old) + self._unsaved_messages -= l + self._corr = old + self._corr[-SAVE_MESSAGES:] + + def mark_as_sent(self): + try: + message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] + message.mark_as_sent() + except Exception as ex: + util.log('Mark as sent ex: ' + str(ex)) + + def clear_corr(self, save_unsent=False): + """ + Clear messages list + """ + if hasattr(self, '_message_getter'): + del self._message_getter + # don't delete data about active file transfer + if not save_unsent: + self._corr = list(filter(lambda x: x.get_type() in (2, 3) and + x.get_status() in ft.ACTIVE_FILE_TRANSFERS, self._corr)) + self._unsaved_messages = 0 + else: + self._corr = list(filter(lambda x: (x.get_type() in (2, 3) and x.get_status() in ft.ACTIVE_FILE_TRANSFERS) + or (x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT']), + self._corr)) + self._unsaved_messages = len(self.get_unsent_messages()) + + def get_curr_text(self): + return self._curr_text + + def set_curr_text(self, value): + self._curr_text = value + + curr_text = property(get_curr_text, set_curr_text) # ----------------------------------------------------------------------------------------------------------------- - # name - current name or alias of user + # Visibility in friends' list # ----------------------------------------------------------------------------------------------------------------- - def get_name(self): - return self._name + def get_visibility(self): + return self._visible + + def set_visibility(self, value): + self._visible = value + + visibility = property(get_visibility, set_visibility) + + # ----------------------------------------------------------------------------------------------------------------- + # Unread messages and actions + # ----------------------------------------------------------------------------------------------------------------- + + def get_actions(self): + return self._new_actions + + def set_actions(self, value): + self._new_actions = value + self._widget.connection_status.update(self.status, value) + + actions = property(get_actions, set_actions) # unread messages, incoming files, av calls + + def get_messages(self): + return self._new_messages + + def inc_messages(self): + self._new_messages += 1 + self._new_actions = True + self._widget.connection_status.update(self.status, True) + self._widget.messages.update(self._new_messages) + + def reset_messages(self): + self._new_actions = False + self._new_messages = 0 + self._widget.messages.update(self._new_messages) + self._widget.connection_status.update(self.status, False) + + messages = property(get_messages) + + # ----------------------------------------------------------------------------------------------------------------- + # Number (can be used in toxcore) + # ----------------------------------------------------------------------------------------------------------------- + + def get_number(self): + return self._number + + def set_number(self, value): + self._number = value + + number = property(get_number, set_number) + + # ----------------------------------------------------------------------------------------------------------------- + # Alias support + # ----------------------------------------------------------------------------------------------------------------- def set_name(self, value): - self._name = str(value, 'utf-8') - self._widget.name.setText(self._name) - self._widget.name.repaint() - - name = property(get_name, set_name) - - # ----------------------------------------------------------------------------------------------------------------- - # Status message - # ----------------------------------------------------------------------------------------------------------------- - - def get_status_message(self): - return self._status_message - - def set_status_message(self, value): - self._status_message = str(value, 'utf-8') - self._widget.status_message.setText(self._status_message) - self._widget.status_message.repaint() - - status_message = property(get_status_message, set_status_message) - - # ----------------------------------------------------------------------------------------------------------------- - # Status - # ----------------------------------------------------------------------------------------------------------------- - - def get_status(self): - return self._status - - def set_status(self, value): - self._status = value - self._widget.connection_status.update(value) - - status = property(get_status, set_status) - - # ----------------------------------------------------------------------------------------------------------------- - # TOX ID. WARNING: for friend it will return public key, for profile - full address - # ----------------------------------------------------------------------------------------------------------------- - - def get_tox_id(self): - return self._tox_id - - tox_id = property(get_tox_id) - - # ----------------------------------------------------------------------------------------------------------------- - # Avatars - # ----------------------------------------------------------------------------------------------------------------- - - def load_avatar(self): """ - Tries to load avatar of contact or uses default avatar + Set new name or ignore if alias exists + :param value: new name """ - 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() + if not self._alias: + super(Contact, self).set_name(value) - 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() + def set_alias(self, alias): + self._alias = bool(alias) diff --git a/toxygen/friend.py b/toxygen/friend.py index 3dd5b0f..e9a5657 100644 --- a/toxygen/friend.py +++ b/toxygen/friend.py @@ -1,8 +1,5 @@ import contact from messages import * -from history import * -import util -import file_transfers as ft class Friend(contact.Contact): @@ -10,28 +7,15 @@ 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__(*args) - self._number = number - self._new_messages = False - self._visible = True - self._alias = False - self._message_getter = message_getter - self._corr = [] - self._unsaved_messages = 0 - self._history_loaded = self._new_actions = False self._receipts = 0 - self._curr_text = '' def __del__(self): - self.set_visibility(False) - del self._widget - if hasattr(self, '_message_getter'): - del self._message_getter + super().__del__() # ----------------------------------------------------------------------------------------------------------------- # History support @@ -50,100 +34,6 @@ class Friend(contact.Contact): self._receipts -= 1 self.mark_as_sent() - def load_corr(self, first_time=True): - """ - :param first_time: friend became active, load first part of messages - """ - if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')): - return - data = list(self._message_getter.get(PAGE_SIZE)) - if data is not None and len(data): - data.reverse() - else: - return - data = list(map(lambda tupl: TextMessage(*tupl), data)) - self._corr = data + self._corr - self._history_loaded = True - - def get_corr_for_saving(self): - """ - Get data to save in db - :return: list of unsaved messages or [] - """ - messages = list(filter(lambda x: x.get_type() <= 1, self._corr)) - return list(map(lambda x: x.get_data(), messages[-self._unsaved_messages:])) if self._unsaved_messages else [] - - def get_corr(self): - return self._corr[:] - - def append_message(self, message): - """ - :param message: text or file transfer message - """ - self._corr.append(message) - if message.get_type() <= 1: - self._unsaved_messages += 1 - - def get_last_message_text(self): - messages = list(filter(lambda x: x.get_type() <= 1 and x.get_owner() != MESSAGE_OWNER['FRIEND'], self._corr)) - if messages: - return messages[-1].get_data()[0] - else: - return '' - - def get_unsent_messages(self): - """ - :return list of unsent messages - """ - messages = filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) - return list(messages) - - def get_unsent_messages_for_saving(self): - """ - :return list of unsent messages for saving - """ - messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) - return list(map(lambda x: x.get_data(), messages)) - - def delete_message(self, time): - elem = list(filter(lambda x: type(x) is TextMessage 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:]: - self._unsaved_messages -= 1 - self._corr.remove(elem) - - def mark_as_sent(self): - try: - message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] - message.mark_as_sent() - except Exception as ex: - util.log('Mark as sent ex: ' + str(ex)) - - def clear_corr(self, save_unsent=False): - """ - Clear messages list - """ - if hasattr(self, '_message_getter'): - del self._message_getter - # don't delete data about active file transfer - if not save_unsent: - self._corr = list(filter(lambda x: x.get_type() in (2, 3) and - x.get_status() in ft.ACTIVE_FILE_TRANSFERS, self._corr)) - self._unsaved_messages = 0 - else: - self._corr = list(filter(lambda x: (x.get_type() in (2, 3) and x.get_status() in ft.ACTIVE_FILE_TRANSFERS) - or (x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT']), - self._corr)) - self._unsaved_messages = len(self.get_unsent_messages()) - - def get_curr_text(self): - return self._curr_text - - def set_curr_text(self, value): - self._curr_text = value - - curr_text = property(get_curr_text, set_curr_text) - # ----------------------------------------------------------------------------------------------------------------- # File transfers support # ----------------------------------------------------------------------------------------------------------------- @@ -172,72 +62,3 @@ class Friend(contact.Contact): def delete_one_unsent_file(self, time): self._corr = list(filter(lambda x: not (type(x) is UnsentFile and x.get_data()[2] == time), self._corr)) - - # ----------------------------------------------------------------------------------------------------------------- - # Alias support - # ----------------------------------------------------------------------------------------------------------------- - - def set_name(self, value): - """ - Set new name or ignore if alias exists - :param value: new name - """ - if not self._alias: - super(Friend, self).set_name(value) - - def set_alias(self, alias): - self._alias = bool(alias) - - # ----------------------------------------------------------------------------------------------------------------- - # Visibility in friends' list - # ----------------------------------------------------------------------------------------------------------------- - - def get_visibility(self): - return self._visible - - def set_visibility(self, value): - self._visible = value - - visibility = property(get_visibility, set_visibility) - - # ----------------------------------------------------------------------------------------------------------------- - # Unread messages from friend - # ----------------------------------------------------------------------------------------------------------------- - - def get_actions(self): - return self._new_actions - - def set_actions(self, value): - self._new_actions = value - self._widget.connection_status.update(self.status, value) - - actions = property(get_actions, set_actions) # unread messages, incoming files, av calls - - def get_messages(self): - return self._new_messages - - def inc_messages(self): - self._new_messages += 1 - self._new_actions = True - self._widget.connection_status.update(self.status, True) - self._widget.messages.update(self._new_messages) - - def reset_messages(self): - self._new_actions = False - self._new_messages = 0 - self._widget.messages.update(self._new_messages) - self._widget.connection_status.update(self.status, False) - - messages = property(get_messages) - - # ----------------------------------------------------------------------------------------------------------------- - # Friend's number (can be used in toxcore) - # ----------------------------------------------------------------------------------------------------------------- - - def get_number(self): - return self._number - - def set_number(self, value): - self._number = value - - number = property(get_number, set_number) diff --git a/toxygen/groupchat.py b/toxygen/groupchat.py new file mode 100644 index 0000000..b692761 --- /dev/null +++ b/toxygen/groupchat.py @@ -0,0 +1,36 @@ +import contact + + +class GroupChat(contact.Contact): + + def __init__(self, tox, *args): + super().__init__(*args) + self._tox = tox + + def load_avatar(self, default_path='group.png'): + super().load_avatar(default_path) + + def set_status(self, value): + print('In gc set_status') + super().set_status(value) + self.name = bytes(self._tox.group_get_name(self._number), 'utf-8') + self._tox_id = self._tox.group_get_chat_id(self._number) + self.status_message = bytes(self._tox.group_get_topic(self._number), 'utf-8') + + def add_peer(self, peer_id): + print(peer_id) + print(self._tox.group_peer_get_name(self._number, peer_id)) + + # TODO: get peers list and add other methods + + def get_peers_list(self): + return [] + + +class Peer: + + def __init__(self, peer_id, name, status, role): + self._data = (peer_id, name, status, role) + + def get_data(self): + return self._data diff --git a/toxygen/history.py b/toxygen/history.py index ad18ee5..581d8a0 100644 --- a/toxygen/history.py +++ b/toxygen/history.py @@ -8,6 +8,8 @@ from toxencryptsave import ToxEncryptSave PAGE_SIZE = 42 +SAVE_MESSAGES = 150 + MESSAGE_OWNER = { 'ME': 0, 'FRIEND': 1, diff --git a/toxygen/images/group.png b/toxygen/images/group.png new file mode 100755 index 0000000..22adab0 Binary files /dev/null and b/toxygen/images/group.png differ diff --git a/toxygen/mainscreen.py b/toxygen/mainscreen.py index 3260614..b828907 100644 --- a/toxygen/mainscreen.py +++ b/toxygen/mainscreen.py @@ -29,6 +29,8 @@ class MainWindow(QtGui.QMainWindow): self.menuProfile = QtGui.QMenu(self.menubar) self.menuProfile.setObjectName("menuProfile") + self.menuGroupChats = QtGui.QMenu(self.menubar) + self.menuGroupChats.setObjectName("menuGroupChats") self.menuSettings = QtGui.QMenu(self.menubar) self.menuSettings.setObjectName("menuSettings") self.menuPlugins = QtGui.QMenu(self.menubar) @@ -56,6 +58,12 @@ class MainWindow(QtGui.QMainWindow): self.pluginData = QtGui.QAction(MainWindow) self.importPlugin = QtGui.QAction(MainWindow) self.lockApp = QtGui.QAction(MainWindow) + self.createGC = QtGui.QAction(MainWindow) + self.joinGC = QtGui.QAction(MainWindow) + self.gcRequests = QtGui.QAction(MainWindow) + self.menuGroupChats.addAction(self.createGC) + self.menuGroupChats.addAction(self.joinGC) + self.menuGroupChats.addAction(self.gcRequests) self.menuProfile.addAction(self.actionAdd_friend) self.menuProfile.addAction(self.actionSettings) self.menuProfile.addAction(self.lockApp) @@ -68,6 +76,7 @@ class MainWindow(QtGui.QMainWindow): self.menuPlugins.addAction(self.importPlugin) self.menuAbout.addAction(self.actionAbout_program) self.menubar.addAction(self.menuProfile.menuAction()) + self.menubar.addAction(self.menuGroupChats.menuAction()) self.menubar.addAction(self.menuSettings.menuAction()) self.menubar.addAction(self.menuPlugins.menuAction()) self.menubar.addAction(self.menuAbout.menuAction()) @@ -75,14 +84,18 @@ 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) self.audioSettings.triggered.connect(self.audio_settings) self.pluginData.triggered.connect(self.plugins_menu) + self.lockApp.triggered.connect(self.lock_app) self.importPlugin.triggered.connect(self.import_plugin) + self.createGC.triggered.connect(self.create_groupchat) + self.joinGC.triggered.connect(self.join_groupchat) + QtCore.QMetaObject.connectSlotsByName(MainWindow) def languageChange(self, *args, **kwargs): @@ -94,7 +107,11 @@ class MainWindow(QtGui.QMainWindow): return super(MainWindow, self).event(event) def retranslateUi(self): + self.joinGC.setText(QtGui.QApplication.translate("MainWindow", "Join group chat", None, QtGui.QApplication.UnicodeUTF8)) self.lockApp.setText(QtGui.QApplication.translate("MainWindow", "Lock", None, QtGui.QApplication.UnicodeUTF8)) + self.menuGroupChats.setTitle(QtGui.QApplication.translate("MainWindow", "Group chats", None, QtGui.QApplication.UnicodeUTF8)) + self.createGC.setText(QtGui.QApplication.translate("MainWindow", "Create group chat", None, QtGui.QApplication.UnicodeUTF8)) + self.gcRequests.setText(QtGui.QApplication.translate("MainWindow", "Groupchat requests", None, QtGui.QApplication.UnicodeUTF8)) self.menuPlugins.setTitle(QtGui.QApplication.translate("MainWindow", "Plugins", None, QtGui.QApplication.UnicodeUTF8)) self.pluginData.setText(QtGui.QApplication.translate("MainWindow", "List of plugins", None, QtGui.QApplication.UnicodeUTF8)) self.menuProfile.setTitle(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8)) @@ -193,9 +210,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") @@ -220,6 +237,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") @@ -377,7 +399,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() @@ -439,6 +461,19 @@ class MainWindow(QtGui.QMainWindow): 120)) self.menu.show() + def create_groupchat(self): + self.gc = AddGroupchat() + self.gc.show() + + def join_groupchat(self): + self.gc = JoinGroupchat() + 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 # ----------------------------------------------------------------------------------------------------------------- @@ -449,6 +484,8 @@ class MainWindow(QtGui.QMainWindow): def send_file(self): self.menu.hide() + if not self.profile.is_active_a_friend(): + return if self.profile.active_friend + 1: choose = QtGui.QApplication.translate("MainWindow", 'Choose file', None, QtGui.QApplication.UnicodeUTF8) name = QtGui.QFileDialog.getOpenFileName(self, choose, options=QtGui.QFileDialog.DontUseNativeDialog) @@ -457,6 +494,8 @@ class MainWindow(QtGui.QMainWindow): def send_screenshot(self, hide=False): self.menu.hide() + if not self.profile.is_active_a_friend(): + return if self.profile.active_friend + 1: self.sw = ScreenShotWindow(self) self.sw.show() @@ -475,6 +514,8 @@ class MainWindow(QtGui.QMainWindow): def send_sticker(self): self.menu.hide() + if not self.profile.is_active_a_friend(): + return if self.profile.active_friend + 1: self.sticker = StickerWindow(self) self.sticker.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(), @@ -511,35 +552,60 @@ class MainWindow(QtGui.QMainWindow): def friend_right_click(self, pos): item = self.friends_list.itemAt(pos) num = self.friends_list.indexFromItem(item).row() - friend = Profile.get_instance().get_friend(num) + friend = Profile.get_instance().get_friend_or_gc(num) settings = Settings.get_instance() allowed = friend.tox_id in settings['auto_accept_from_friends'] 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: + arr = Profile.get_instance().get_all_gc() + if arr: + gc_menu = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Invite to group chat', None, QtGui.QApplication.UnicodeUTF8)) + for gc in arr: + item = gc_menu.addAction(gc.name) + self.connect(item, QtCore.SIGNAL("triggered()"), + lambda: Profile.get_instance().invite_friend(gc.number, friend.number)) - 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)) + 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)) + + 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(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) + self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed)) + else: + 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", 'Topic', None, QtGui.QApplication.UnicodeUTF8)) + copy_key_item = copy_menu.addAction( + QtGui.QApplication.translate("MainWindow", 'Public key', None, QtGui.QApplication.UnicodeUTF8)) + leave_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Leave group', None, QtGui.QApplication.UnicodeUTF8)) + 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)) + notes_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Notes', None, QtGui.QApplication.UnicodeUTF8)) + self.connect(leave_item, QtCore.SIGNAL("triggered()"), lambda: Profile.get_instance().leave_group(num)) - 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(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) + self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num)) 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)) + self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) + parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) self.listMenu.move(parent_position + pos) self.listMenu.show() @@ -563,7 +629,7 @@ class MainWindow(QtGui.QMainWindow): self.profile.set_alias(num) def remove_friend(self, num): - self.profile.delete_friend(num) + self.profile.delete_friend_or_gc(num) def copy_friend_key(self, num): tox_id = self.profile.friend_public_key(num) diff --git a/toxygen/menu.py b/toxygen/menu.py index a1c39ed..8dc9621 100644 --- a/toxygen/menu.py +++ b/toxygen/menu.py @@ -11,6 +11,97 @@ import toxencryptsave import plugin_support +class AddGroupchat(CenteredWidget): + + def __init__(self): + super().__init__() + self.initUI() + self.retranslateUi() + self.center() + + def initUI(self): + self.setObjectName('AddGC') + self.resize(570, 240) + self.setMaximumSize(QtCore.QSize(570, 240)) + self.setMinimumSize(QtCore.QSize(570, 240)) + self.label = QtGui.QLabel(self) + self.label.setGeometry(QtCore.QRect(50, 20, 470, 20)) + self.createGCButton = QtGui.QPushButton(self) + self.createGCButton.setGeometry(QtCore.QRect(50, 190, 470, 30)) + self.name = LineEdit(self) + self.name.setGeometry(QtCore.QRect(50, 40, 470, 27)) + self.privacy_type = QtGui.QLabel(self) + self.privacy_type.setGeometry(QtCore.QRect(50, 70, 470, 20)) + self.privacy_combobox = QtGui.QComboBox(self) + self.privacy_combobox.setGeometry(QtCore.QRect(50, 90, 470, 30)) + self.pass_label = QtGui.QLabel(self) + self.pass_label.setGeometry(QtCore.QRect(50, 130, 470, 20)) + self.password = LineEdit(self) + self.password.setGeometry(QtCore.QRect(50, 150, 470, 27)) + self.password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) + + self.createGCButton.clicked.connect(self.button_click) + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle(QtGui.QApplication.translate('AddGC', "Create group chat", None, QtGui.QApplication.UnicodeUTF8)) + self.createGCButton.setText(QtGui.QApplication.translate("AddGC", "Create", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate('AddGC', "Name:", None, QtGui.QApplication.UnicodeUTF8)) + self.privacy_type.setText(QtGui.QApplication.translate('AddGC', "Privacy type:", None, QtGui.QApplication.UnicodeUTF8)) + self.privacy_combobox.addItem(QtGui.QApplication.translate('AddGC', "Public", None, QtGui.QApplication.UnicodeUTF8)) + self.privacy_combobox.addItem(QtGui.QApplication.translate('AddGC', "Private", None, QtGui.QApplication.UnicodeUTF8)) + self.name.setPlaceholderText(QtGui.QApplication.translate('AddGC', "Not empty group name", None, QtGui.QApplication.UnicodeUTF8)) + self.password.setPlaceholderText(QtGui.QApplication.translate('AddGC', "Optional password", None, QtGui.QApplication.UnicodeUTF8)) + self.pass_label.setText(QtGui.QApplication.translate('AddGC', "Password:", None, QtGui.QApplication.UnicodeUTF8)) + + def button_click(self): + if self.name.text(): + Profile.get_instance().create_gc(self.name.text(), + self.privacy_combobox.currentIndex() == 0, + self.password.text()) + self.close() + + +class JoinGroupchat(CenteredWidget): + + def __init__(self): + super().__init__() + self.initUI() + self.retranslateUi() + self.center() + + def initUI(self): + self.setObjectName('AddGC') + self.resize(570, 150) + self.setMaximumSize(QtCore.QSize(570, 150)) + self.setMinimumSize(QtCore.QSize(570, 150)) + self.joinGCButton = QtGui.QPushButton(self) + self.joinGCButton.setGeometry(QtCore.QRect(50, 110, 470, 30)) + self.id = LineEdit(self) + self.id.setGeometry(QtCore.QRect(50, 10, 470, 30)) + self.password = LineEdit(self) + self.password.setGeometry(QtCore.QRect(50, 50, 470, 30)) + self.password.setEchoMode(QtGui.QLineEdit.EchoMode.Password) + + self.joinGCButton.clicked.connect(self.button_click) + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle( + QtGui.QApplication.translate('JoinGC', "Join group chat", None, QtGui.QApplication.UnicodeUTF8)) + self.joinGCButton.setText( + QtGui.QApplication.translate("JoinGC", "Join", None, QtGui.QApplication.UnicodeUTF8)) + self.id.setPlaceholderText( + QtGui.QApplication.translate('JoinGC', "Group ID", None, QtGui.QApplication.UnicodeUTF8)) + self.password.setPlaceholderText( + QtGui.QApplication.translate('JoinGC', "Optional password", None, QtGui.QApplication.UnicodeUTF8)) + + def button_click(self): + if self.id.text(): + Profile.get_instance().join_gc(self.id.text().strip(), self.password.text()) + self.close() + + class AddContact(CenteredWidget): """Add contact form""" @@ -510,28 +601,33 @@ 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, 200) + self.setMinimumSize(QtCore.QSize(350, 200)) + self.setMaximumSize(QtCore.QSize(350, 200)) self.enableNotifications = QtGui.QCheckBox(self) self.enableNotifications.setGeometry(QtCore.QRect(10, 20, 340, 18)) self.callsSound = QtGui.QCheckBox(self) self.callsSound.setGeometry(QtCore.QRect(10, 120, 340, 18)) self.soundNotifications = QtGui.QCheckBox(self) self.soundNotifications.setGeometry(QtCore.QRect(10, 70, 340, 18)) + self.gcNotifications = QtGui.QCheckBox(self) + self.gcNotifications.setGeometry(QtCore.QRect(10, 170, 340, 18)) font = QtGui.QFont() font.setPointSize(12) self.callsSound.setFont(font) self.soundNotifications.setFont(font) self.enableNotifications.setFont(font) + self.gcNotifications.setFont(font) s = Settings.get_instance() self.enableNotifications.setChecked(s['notifications']) self.soundNotifications.setChecked(s['sound_notifications']) self.callsSound.setChecked(s['calls_sound']) + self.gcNotifications.setChecked(s['notify_all_gc']) self.retranslateUi() QtCore.QMetaObject.connectSlotsByName(self) def retranslateUi(self): + self.gcNotifications.setText(QtGui.QApplication.translate("notificationsForm", "Enable group chat notifications", None, QtGui.QApplication.UnicodeUTF8)) self.setWindowTitle(QtGui.QApplication.translate("notificationsForm", "Notification settings", None, QtGui.QApplication.UnicodeUTF8)) self.enableNotifications.setText(QtGui.QApplication.translate("notificationsForm", "Enable notifications", None, QtGui.QApplication.UnicodeUTF8)) self.callsSound.setText(QtGui.QApplication.translate("notificationsForm", "Enable call\'s sound", None, QtGui.QApplication.UnicodeUTF8)) @@ -542,6 +638,7 @@ class NotificationsSettings(CenteredWidget): settings['notifications'] = self.enableNotifications.isChecked() settings['sound_notifications'] = self.soundNotifications.isChecked() settings['calls_sound'] = self.callsSound.isChecked() + settings['notify_all_gc'] = self.gcNotifications.isChecked() settings.save() diff --git a/toxygen/messages.py b/toxygen/messages.py index 87a1cc2..6c1c9e6 100644 --- a/toxygen/messages.py +++ b/toxygen/messages.py @@ -39,6 +39,18 @@ class TextMessage(Message): return self._message, self._owner, self._time, self._type +class GroupChatTextMessage(TextMessage): + + def __init__(self, friend_name, *args): + super().__init__(*args) + self._name = friend_name + + def get_data(self): + data = list(super().get_data()) + data.append(self._name) + return tuple(data) + + class TransferMessage(Message): """ Message with info about file transfer @@ -71,6 +83,7 @@ class TransferMessage(Message): class UnsentFile(Message): + def __init__(self, path, data, time): super(UnsentFile, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], 0, time) self._data, self._path = data, path diff --git a/toxygen/profile.py b/toxygen/profile.py index 9d5f1ab..fc54a31 100644 --- a/toxygen/profile.py +++ b/toxygen/profile.py @@ -15,9 +15,11 @@ import time import calls import avwidgets import plugin_support +import basecontact +from groupchat import * -class Profile(contact.Contact, Singleton): +class Profile(basecontact.BaseContact, Singleton): """ Profile of current toxygen user. Contains friends list, tox instance """ @@ -26,11 +28,11 @@ class Profile(contact.Contact, Singleton): :param tox: tox instance :param screen: ref to main screen """ - contact.Contact.__init__(self, - tox.self_get_name(), - tox.self_get_status_message(), - screen.user_info, - tox.self_get_address()) + 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 @@ -39,13 +41,14 @@ class Profile(contact.Contact, Singleton): self._call = calls.AV(tox.AV) # object with data about calls self._incoming_calls = set() self._load_history = True + self._gc_invites = {} # dict of gc invites. key - friend number, value - list of gc data settings = Settings.get_instance() self._show_online = settings['show_online_friends'] screen.online_contacts.setCurrentIndex(int(self._show_online)) aliases = settings['friends_aliases'] data = tox.self_get_friend_list() self._history = History(tox.self_get_public_key()) # connection to db - self._friends, self._active_friend = [], -1 + self._friends_and_gc, self._active_friend_or_gc = [], -1 for i in data: # creates list of friends tox_id = tox.friend_get_public_key(i) try: @@ -58,9 +61,27 @@ class Profile(contact.Contact, 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.append(friend) + self._friends_and_gc.append(friend) + + l = self._tox.group_get_number_groups() + for i in range(l): # creates list of group chats + tox_id = tox.group_get_chat_id(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.group_get_name(i) or tox_id + status_message = tox.group_get_topic(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) + gc = GroupChat(self._tox, i, message_getter, name, status_message, item, tox_id) + gc.set_alias(alias) + self._friends_and_gc.append(gc) + self.filtration(self._show_online) # ----------------------------------------------------------------------------------------------------------------- @@ -75,6 +96,9 @@ class Profile(contact.Contact, Singleton): self.set_status((self._status + 1) % 3) def set_status(self, status): + if self.status is None: + for gc in filter(lambda x: type(x) is GroupChat, self._friends_and_gc): + self._tox.group_reconnect(gc.number) super(Profile, self).set_status(status) if status is not None: self._tox.self_set_status(status) @@ -87,11 +111,11 @@ class Profile(contact.Contact, Singleton): self._tox.self_set_name(self._name.encode('utf-8')) message = QtGui.QApplication.translate("MainWindow", 'User {} is now known as {}', None, QtGui.QApplication.UnicodeUTF8) - message = message.format(tmp, value) - for friend in self._friends: + message = message.format(tmp, str(value, 'utf-8')) + for friend in self._friends_and_gc: friend.append_message(InfoMessage(message, time.time())) - if self._active_friend + 1: - self.create_message_item(message, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) + if self._active_friend_or_gc + 1: + self.create_message_item(message, curr_time(), '', MESSAGE_TYPE['INFO_MESSAGE']) self._messages.scrollToBottom() def set_status_message(self, value): @@ -117,7 +141,7 @@ class Profile(contact.Contact, Singleton): """ filter_str = filter_str.lower() settings = Settings.get_instance() - for index, friend in enumerate(self._friends): + for index, friend in enumerate(self._friends_and_gc): friend.visibility = (friend.status is not None or not show_online) and (filter_str in friend.name.lower()) friend.visibility = friend.visibility or friend.messages or friend.actions if friend.visibility: @@ -136,29 +160,32 @@ class Profile(contact.Contact, 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))[0] + return list(filter(lambda x: x.number == num and type(x) is Friend, self._friends_and_gc))[0] - def get_friend(self, num): - return self._friends[num] + 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] # ----------------------------------------------------------------------------------------------------------------- # Work with active friend # ----------------------------------------------------------------------------------------------------------------- def get_active(self): - return self._active_friend + return self._active_friend_or_gc 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 + if value is None and self._active_friend_or_gc == -1: # nothing to update return if value == -1: # all friends were deleted self._screen.account_name.setText('') self._screen.account_status.setText('') - self._active_friend = -1 + self._active_friend_or_gc = -1 self._screen.account_avatar.setHidden(True) self._messages.clear() self._screen.messageEdit.clear() @@ -167,26 +194,30 @@ class Profile(contact.Contact, Singleton): self.send_typing(False) self._screen.typing.setVisible(False) if value is not None: - if self._active_friend + 1: + if self._active_friend_or_gc + 1: try: - self._friends[self._active_friend].curr_text = self._screen.messageEdit.toPlainText() + self._friends_and_gc[self._active_friend_or_gc].curr_text = self._screen.messageEdit.toPlainText() except: pass - self._active_friend = value - friend = self._friends[value] - self._friends[value].reset_messages() - self._screen.messageEdit.setPlainText(friend.curr_text) + self._active_friend_or_gc = value + friend_or_gc = self._friends_and_gc[value] + friend_or_gc.reset_messages() + friend_or_gc.delete_old_messages() + self._screen.messageEdit.setPlainText(friend_or_gc.curr_text) self._messages.clear() - friend.load_corr() - messages = friend.get_corr()[-PAGE_SIZE:] + friend_or_gc.load_corr() + messages = friend_or_gc.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]) + data[3], + True, + data[4] if len(data) == 5 else None) elif message.get_type() == MESSAGE_TYPE['FILE_TRANSFER']: if message.get_status() is None: self.create_unsent_file_item(message) @@ -216,13 +247,16 @@ class Profile(contact.Contact, Singleton): else: self._screen.call_finished() else: - friend = self._friends[self._active_friend] + friend_or_gc = self._friends_and_gc[self._active_friend_or_gc] - self._screen.account_name.setText(friend.name) - self._screen.account_status.setText(friend.status_message) - avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(friend.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + self._screen.account_name.setText(friend_or_gc.name) + self._screen.account_status.setText(friend_or_gc.status_message) + avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(friend_or_gc.tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) if not os.path.isfile(avatar_path): # load default image - avatar_path = curr_directory() + '/images/avatar.png' + if type(friend_or_gc) is Friend: + avatar_path = curr_directory() + '/images/avatar.png' + else: + avatar_path = curr_directory() + '/images/group.png' os.chdir(os.path.dirname(avatar_path)) pixmap = QtGui.QPixmap(QtCore.QSize(64, 64)) pixmap.load(avatar_path) @@ -237,17 +271,20 @@ class Profile(contact.Contact, Singleton): active_friend = property(get_active, set_active) + def is_active_a_friend(self): + return type(self._friends_and_gc[self._active_friend_or_gc]) is Friend + def get_last_message(self): - return self._friends[self._active_friend].get_last_message_text() + return self._friends_and_gc[self._active_friend_or_gc].get_last_message_text() def get_active_number(self): - return self._friends[self._active_friend].number if self._active_friend + 1 else -1 + return self._friends_and_gc[self._active_friend_or_gc].number if self._active_friend_or_gc + 1 else -1 def get_active_name(self): - return self._friends[self._active_friend].name if self._active_friend + 1 else '' + return self._friends_and_gc[self._active_friend_or_gc].name if self._active_friend_or_gc + 1 else '' def is_active_online(self): - return self._active_friend + 1 and self._friends[self._active_friend].status is not None + return self._active_friend_or_gc + 1 and self._friends_and_gc[self._active_friend_or_gc].status is not None def new_name(self, number, name): friend = self.get_friend_by_number(number) @@ -265,8 +302,8 @@ class Profile(contact.Contact, Singleton): self.set_active(None) def update(self): - if self._active_friend + 1: - self.set_active(self._active_friend) + if self._active_friend_or_gc + 1: + self.set_active(self._active_friend_or_gc) # ----------------------------------------------------------------------------------------------------------------- # Friend connection status callbacks @@ -306,8 +343,8 @@ class Profile(contact.Contact, Singleton): """ Send typing notification to a friend """ - if Settings.get_instance()['typing_notifications'] and self._active_friend + 1: - friend = self._friends[self._active_friend] + if Settings.get_instance()['typing_notifications'] and self._active_friend_or_gc + 1: + friend = self._friends_and_gc[self._active_friend_or_gc] if friend.status is not None: self._tox.self_set_typing(friend.number, typing) @@ -341,12 +378,13 @@ class Profile(contact.Contact, 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 @@ -360,62 +398,95 @@ class Profile(contact.Contact, 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): + def new_message(self, num, message_type, message, is_group=False, peer_id=-1): """ Current user gets new message - :param friend_num: friend_num of friend who sent message + :param num: num of friend or gc who sent message :param message_type: message type - plain text or action message (/me) :param message: text of message + :param is_group: is group chat message or not + :param peer_id: if gc - peer id """ - if friend_num == self.get_active_number(): # add message to list - t = time.time() - self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) + t = time.time() + active = False + if num == self.get_active_number() and is_group != self.is_active_a_friend(): # add message to list + if not is_group: + self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) + else: + self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type, True, + self._tox.group_peer_get_name(num, peer_id)) self._messages.scrollToBottom() - self._friends[self._active_friend].append_message( - TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type)) + active = True + if is_group: + friend_or_gc = self.get_gc_by_number(num) + friend_or_gc.append_message(GroupChatTextMessage(self._tox.group_peer_get_name(num, peer_id), + message, MESSAGE_OWNER['FRIEND'], + time.time(), 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() + friend_or_gc = self.get_friend_by_number(num) + friend_or_gc.append_message(TextMessage(message, MESSAGE_OWNER['FRIEND'], time.time(), message_type)) + if not active: + friend_or_gc.inc_messages() + if not friend_or_gc.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 '): 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')) - if friend.number == self.get_active_number(): - t = time.time() - 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 = self.get_friend_by_number(number) + else: + friend_or_gc = self.get_gc_by_number(number) + t = time.time() + + 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() + friend_or_gc.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type)) + 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['ME'], 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() @@ -429,20 +500,21 @@ class Profile(contact.Contact, Singleton): Save history to db """ s = Settings.get_instance() + # TODO: different saving for friends and gc if hasattr(self, '_history'): if s['save_history']: - for friend in self._friends: - if not self._history.friend_exists_in_db(friend.tox_id): - self._history.add_friend_to_db(friend.tox_id) + for friend_or_gc in self._friends_and_gc: + if not self._history.friend_exists_in_db(friend_or_gc.tox_id): + self._history.add_friend_to_db(friend_or_gc.tox_id) if not s['save_unsent_only']: - messages = friend.get_corr_for_saving() + messages = friend_or_gc.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() + messages = friend_or_gc.get_unsent_messages_for_saving() + self._history.delete_messages(friend_or_gc.tox_id) + self._history.save_messages_to_db(friend_or_gc.tox_id, messages) + unsent_messages = friend_or_gc.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.update_messages(friend_or_gc.tox_id, unsent_time) self._history.save() del self._history @@ -451,14 +523,15 @@ class Profile(contact.Contact, Singleton): Clear chat history """ if num is not None: - friend = self._friends[num] + friend = self._friends_and_gc[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._friends)): + for number in range(len(self._friends_and_gc)): self.clear_history(number, save_unsent) + if num is None or num == self.get_active_number(): self.update() @@ -469,9 +542,10 @@ class Profile(contact.Contact, Singleton): if not self._load_history: return self._load_history = False - friend = self._friends[self._active_friend] - friend.load_corr(False) - data = friend.get_corr() + friend_or_gc = self._friends_and_gc[self._active_friend_or_gc] + + friend_or_gc.load_corr(False) + data = friend_or_gc.get_corr() if not data: return data.reverse() @@ -525,11 +599,11 @@ class Profile(contact.Contact, Singleton): self._screen.friends_list.setItemWidget(elem, item) return item - def create_message_item(self, text, time, owner, message_type, append=True): + def create_message_item(self, text, time, owner, message_type, append=True, friend_name=None): if message_type == MESSAGE_TYPE['INFO_MESSAGE']: name = '' elif owner == MESSAGE_OWNER['FRIEND']: - name = self.get_active_name() + name = friend_name or self.get_active_name() else: name = self._name item = MessageItem(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], message_type, self._messages) @@ -588,7 +662,7 @@ class Profile(contact.Contact, Singleton): """ Set new alias for friend """ - friend = self._friends[num] + friend = self._friends_and_gc[num] name = friend.name dialog = QtGui.QApplication.translate('MainWindow', "Enter new alias for friend {} or leave empty to use friend's name:", @@ -626,14 +700,16 @@ class Profile(contact.Contact, Singleton): self.update() def friend_public_key(self, num): - return self._friends[num].tox_id + return self._friends_and_gc[num].tox_id - def delete_friend(self, num): + def delete_friend_or_gc(self, num, is_gc=False, message=None): """ - Removes friend from contact list - :param num: number of friend in list + Removes friend or gc from contact list + :param num: number of friend or gc in list + :param is_gc: is a group chat + :param message: message in gc """ - friend = self._friends[num] + friend = self._friends_and_gc[num] settings = Settings.get_instance() try: index = list(map(lambda x: x[0], settings['friends_aliases'])).index(friend.tox_id) @@ -646,11 +722,14 @@ class Profile(contact.Contact, Singleton): self.clear_history(num) if self._history.friend_exists_in_db(friend.tox_id): self._history.delete_friend_from_db(friend.tox_id) - self._tox.friend_delete(friend.number) - del self._friends[num] + if not is_gc: + self._tox.friend_delete(friend.number) + else: + self._tox.group_leave(friend.number, message.encode('utf-8') if message is not None else None) + del self._friends_and_gc[num] self._screen.friends_list.takeItem(num) - if num == self._active_friend: # active friend was deleted - if not len(self._friends): # last friend was deleted + if num == self._active_friend_or_gc: # active friend or gc was deleted + if not len(self._friends_and_gc): # last contact was deleted self.set_active(-1) else: self.set_active(0) @@ -671,7 +750,7 @@ class Profile(contact.Contact, Singleton): log('Accept friend request failed! ' + str(ex)) message_getter = None friend = Friend(message_getter, num, tox_id, '', item, tox_id) - self._friends.append(friend) + self._friends_and_gc.append(friend) def block_user(self, tox_id): """ @@ -686,7 +765,7 @@ class Profile(contact.Contact, Singleton): settings.save() try: num = self._tox.friend_by_public_key(tox_id) - self.delete_friend(num) + self.delete_friend_or_gc(num) data = self._tox.get_savedata() ProfileHelper.get_instance().save_profile(data) except: # not in friend list @@ -738,7 +817,7 @@ class Profile(contact.Contact, Singleton): self._history.add_friend_to_db(tox_id) message_getter = self._history.messages_getter(tox_id) friend = Friend(message_getter, result, tox_id, '', item, tox_id) - self._friends.append(friend) + self._friends_and_gc.append(friend) data = self._tox.get_savedata() ProfileHelper.get_instance().save_profile(data) return True @@ -781,7 +860,7 @@ class Profile(contact.Contact, Singleton): del self._tox self._tox = restart() self.status = None - for friend in self._friends: + for friend in self._friends_and_gc: friend.status = None self.update_filtration() @@ -789,8 +868,8 @@ class Profile(contact.Contact, Singleton): if hasattr(self, '_call'): self._call.stop() del self._call - for i in range(len(self._friends)): - del self._friends[0] + for i in range(len(self._friends_and_gc)): + del self._friends_and_gc[0] # ----------------------------------------------------------------------------------------------------------------- # File transfers support @@ -874,7 +953,7 @@ class Profile(contact.Contact, Singleton): 0, -1) def cancel_not_started_transfer(self, time): - self._friends[self._active_friend].delete_one_unsent_file(time) + self._friends_and_gc[self._active_friend_or_gc].delete_one_unsent_file(time) self.update() def pause_transfer(self, friend_number, file_number, by_friend=False): @@ -997,7 +1076,7 @@ class Profile(contact.Contact, Singleton): st.get_file_number()) item = self.create_file_transfer_item(tm) st.set_state_changed_handler(item.update) - self._friends[self._active_friend].append_message(tm) + self._friends_and_gc[self._active_friend_or_gc].append_message(tm) self._messages.scrollToBottom() def incoming_chunk(self, friend_number, file_number, position, data): @@ -1090,12 +1169,12 @@ class Profile(contact.Contact, Singleton): def reset_avatar(self): super(Profile, self).reset_avatar() - for friend in filter(lambda x: x.status is not None, self._friends): + for friend in filter(lambda x: x.status is not None, self._friends_and_gc): self.send_avatar(friend.number) def set_avatar(self, data): super(Profile, self).set_avatar(data) - for friend in filter(lambda x: x.status is not None, self._friends): + for friend in filter(lambda x: x.status is not None, self._friends_and_gc): self.send_avatar(friend.number) # ----------------------------------------------------------------------------------------------------------------- @@ -1109,6 +1188,8 @@ class Profile(contact.Contact, Singleton): def call_click(self, audio=True, video=False): """User clicked audio button in main window""" + if not self.is_active_a_friend(): + return num = self.get_active_number() if num not in self._call and self.is_active_online(): # start call self._call(num, audio, video) @@ -1119,7 +1200,8 @@ class Profile(contact.Contact, Singleton): else: text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None, QtGui.QApplication.UnicodeUTF8) - self._friends[self._active_friend].append_message(InfoMessage(text, time.time())) + + self._friends_and_gc[self._active_friend_or_gc].append_message(InfoMessage(text, time.time())) self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) self._messages.scrollToBottom() elif num in self._call: # finish or cancel call if you call with active friend @@ -1177,6 +1259,74 @@ class Profile(contact.Contact, Singleton): self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) self._messages.scrollToBottom() + # ----------------------------------------------------------------------------------------------------------------- + # Group chats support + # ----------------------------------------------------------------------------------------------------------------- + + def get_all_gc(self): + return list(filter(lambda x: type(x) is GroupChat, self._friends_and_gc)) + + def add_gc(self, num): + try: + tox_id = self._tox.group_get_chat_id(num) + name = self._tox.group_get_name(num) + topic = self._tox.group_get_topic(num) + except: + tox_id = name = topic = '' + 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, num, message_getter, name, topic, item, tox_id) + self._friends_and_gc.append(gc) + + def join_gc(self, chat_id, password): + num = self._tox.group_join(chat_id, password if password else None) + if num != 2 ** 32 - 1: + self.add_gc(num) + else: + pass # TODO: join failed, show error + + 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, bytes(password, 'utf-8')) + self.add_gc(num) + self.get_gc_by_number(num).set_status(TOX_USER_STATUS['NONE']) + + def process_group_invite(self, friend_num, data): + # TODO: 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, friend_num) + data = self._tox.get_savedata() + ProfileHelper.get_instance().save_profile(data) + print('In gc invite', num) + self.add_gc(num) + elif reply != QtGui.QMessageBox.No: + if friend_num in self._gc_invites: + self._gc_invites[friend_num].append(data) + else: + self._gc_invites[friend_num] = data + except Exception as ex: # something is wrong + log('Accept group chat invite failed! ' + str(ex)) + + def invite_friend(self, group_number, friend_number): + self._tox.group_invite_friend(group_number, friend_number) + + def leave_group(self, num, message=None): + self.delete_friend_or_gc(num, True, message) + def tox_factory(data=None, settings=None): """ diff --git a/toxygen/settings.py b/toxygen/settings.py index f4c7746..627a4ed 100644 --- a/toxygen/settings.py +++ b/toxygen/settings.py @@ -126,7 +126,8 @@ class Settings(dict, Singleton): 'unread_color': 'red', 'save_unsent_only': False, 'compact_mode': False, - 'show_welcome_screen': True + 'show_welcome_screen': True, + 'notify_all_gc': False } @staticmethod diff --git a/toxygen/tox.py b/toxygen/tox.py index 862badd..a783e5a 100644 --- a/toxygen/tox.py +++ b/toxygen/tox.py @@ -92,6 +92,22 @@ class Tox: self.file_recv_chunk_cb = None self.friend_lossy_packet_cb = None self.friend_lossless_packet_cb = None + self.group_moderation_cb = None + self.group_join_fail_cb = None + self.group_self_join_cb = None + self.group_invite_cb = None + self.group_custom_packet_cb = None + self.group_private_message_cb = None + self.group_private_message_cb = None + self.group_message_cb = None + self.group_password_cb = None + self.group_peer_limit_cb = None + self.group_privacy_state_cb = None + self.group_topic_cb = None + self.group_peer_status_cb = None + self.group_peer_name_cb = None + self.group_peer_exit_cb = None + self.group_peer_join_cb = None self.AV = ToxAV(self._tox_pointer) @@ -1510,3 +1526,935 @@ 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 chat instance management + # ----------------------------------------------------------------------------------------------------------------- + + def group_new(self, privacy_state, group_name): + """ + Creates a new group chat. + + This function creates a new group chat object and adds it to the chats array. + + The client should initiate its peer list with self info after calling this function, as + the peer_join callback will not be triggered. + + :param privacy_state: The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC, + the group will attempt to announce itself to the DHT and anyone with the Chat ID may join. + Otherwise a friend invite will be required to join the group. + :param group_name: The name of the group. The name must be non-NULL. + + :return group number on success, UINT32_MAX on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_new(self._tox_pointer, privacy_state, group_name, + len(group_name), byref(error)) + return result + + def group_join(self, chat_id, password): + """ + Joins a group chat with specified Chat ID. + + This function creates a new group chat object, adds it to the chats array, and sends + a DHT announcement to find peers in the group associated with chat_id. Once a peer has been + found a join attempt will be initiated. + + :param chat_id: The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes. + :param password: The password required to join the group. Set to NULL if no password is required. + + :return groupnumber on success, UINT32_MAX on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_join(self._tox_pointer, string_to_bin(chat_id), + password, + len(password) if password is not None else 0, + byref(error)) + return result + + def group_reconnect(self, groupnumber): + """ + Reconnects to a group. + + This function disconnects from all peers in the group, then attempts to reconnect with the group. + The caller's state is not changed (i.e. name, status, role, chat public key etc.) + + :param groupnumber: The group number of the group we wish to reconnect to. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_reconnect(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_leave(self, groupnumber, message): + """ + Leaves a group. + + This function sends a parting packet containing a custom (non-obligatory) message to all + peers in a group, and deletes the group from the chat array. All group state information is permanently + lost, including keys and role credentials. + + :param groupnumber: The group number of the group we wish to leave. + :param message: The parting message to be sent to all the peers. Set to NULL if we do not wish to + send a parting message. + + :return True if the group chat instance was successfully deleted. + """ + + error = c_int() + f = Tox.libtoxcore.tox_group_leave + f.restype = c_bool + result = f(self._tox_pointer, groupnumber, message, + len(message) if message is not None else 0, byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group user-visible client information (nickname/status/role/public key) + # ----------------------------------------------------------------------------------------------------------------- + + def group_self_set_name(self, groupnumber, name): + """ + Set the client's nickname for the group instance designated by the given group number. + + Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is equal to zero or name is a NULL + pointer, the function call will fail. + + :param name: A byte array containing the new nickname. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_set_name(self._tox_pointer, groupnumber, name, len(name), byref(error)) + return result + + def group_self_get_name_size(self, groupnumber): + """ + Return the length of the client's current nickname for the group instance designated + by groupnumber as passed to tox_group_self_set_name. + + If no nickname was set before calling this function, the name is empty, + and this function returns 0. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_name_size(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_self_get_name(self, groupnumber): + """ + Write the nickname set by tox_group_self_set_name to a byte array. + + If no nickname was set before calling this function, the name is empty, + and this function has no effect. + + Call tox_group_self_get_name_size to find out how much memory to allocate for the result. + :return nickname + """ + + error = c_int() + size = self.group_self_get_name_size(groupnumber) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_self_get_name(self._tox_pointer, groupnumber, name, byref(error)) + return str(name[:size], 'utf-8') + + def group_self_set_status(self, groupnumber, status): + + """ + Set the client's status for the group instance. Status must be a TOX_USER_STATUS. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_set_status(self._tox_pointer, groupnumber, status, byref(error)) + return result + + def group_self_get_status(self, groupnumber): + """ + returns the client's status for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_status(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_self_get_role(self, groupnumber): + """ + returns the client's role for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_role(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_self_get_peer_id(self, groupnumber): + """ + returns the client's peer id for the group instance on success. + return value is unspecified on failure. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_self_get_peer_id(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_self_get_public_key(self, groupnumber): + """ + Write the client's group public key designated by the given group number to a byte array. + + This key will be permanently tied to the client's identity for this particular group until + the client explicitly leaves the group or gets kicked/banned. This key is the only way for + other peers to reliably identify the client across client restarts. + + `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + + :return public key + """ + + error = c_int() + key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + result = Tox.libtoxcore.tox_group_self_get_public_key(self._tox_pointer, groupnumber, + key, byref(error)) + return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + + # ----------------------------------------------------------------------------------------------------------------- + # Peer-specific group state queries. + # ----------------------------------------------------------------------------------------------------------------- + + def group_peer_get_name_size(self, groupnumber, peer_id): + """ + Return the length of the peer's name. If the group number or ID is invalid, the + return value is unspecified. + + The return value is equal to the `length` argument received by the last + `group_peer_name` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_name_size(self._tox_pointer, groupnumber, peer_id, byref(error)) + return result + + def group_peer_get_name(self, groupnumber, peer_id): + """ + Write the name of the peer designated by the given ID to a byte + array. + + Call tox_group_peer_get_name_size to determine the allocation size for the `name` parameter. + + The data written to `name` is equal to the data received by the last + `group_peer_name` callback. + + :param groupnumber: The group number of the group we wish to query. + :param peer_id: The ID of the peer whose name we want to retrieve. + + :return name. + """ + error = c_int() + size = self.group_peer_get_name_size(groupnumber, peer_id) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_peer_get_name(self._tox_pointer, groupnumber, peer_id, name, byref(error)) + return str(name[:], 'utf-8') + + def group_peer_get_status(self, groupnumber, peer_id): + """ + Return the peer's user status (away/busy/...). If the ID or group number is + invalid, the return value is unspecified. + + The status returned is equal to the last status received through the + `group_peer_status` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_status(self._tox_pointer, groupnumber, peer_id, byref(error)) + return result + + def group_peer_get_role(self, groupnumber, peer_id): + """ + Return the peer's role (user/moderator/founder...). If the ID or group number is + invalid, the return value is unspecified. + + The role returned is equal to the last role received through the + `group_moderation` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_peer_get_role(self._tox_pointer, groupnumber, peer_id, byref(error)) + return result + + def group_peer_get_public_key(self, groupnumber, peer_id): + """ + Write the group public key with the designated peer_id for the designated group number to public_key. + + This key will be parmanently tied to a particular peer until they explicitly leave the group or + get kicked/banned, and is the only way to reliably identify the same peer across client restarts. + + `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + + :return public key + """ + + error = c_int() + key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + result = Tox.libtoxcore.tox_group_peer_get_public_key(self._tox_pointer, groupnumber, peer_id, + key, byref(error)) + return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) + + def callback_group_peer_name(self, callback, user_data): + """ + Set the callback for the `group_peer_name` event. Pass NULL to unset. + This event is triggered when a peer changes their nickname. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_peer_name_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_name(self._tox_pointer, self.group_peer_name_cb, user_data) + + def callback_group_peer_status(self, callback, user_data): + """ + Set the callback for the `group_peer_status` event. Pass NULL to unset. + This event is triggered when a peer changes their status. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p) + self.group_peer_status_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_status(self._tox_pointer, self.group_peer_status_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat state queries and events. + # ----------------------------------------------------------------------------------------------------------------- + + def group_set_topic(self, groupnumber, topic): + """ + Set the group topic and broadcast it to the rest of the group. + + topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH. If length is equal to zero or + topic is set to NULL, the topic will be unset. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_set_topic(self._tox_pointer, groupnumber, topic, len(topic), byref(error)) + return result + + def group_get_topic_size(self, groupnumber): + """ + Return the length of the group topic. If the group number is invalid, the + return value is unspecified. + + The return value is equal to the `length` argument received by the last + `group_topic` callback. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_get_topic(self, groupnumber): + """ + Write the topic designated by the given group number to a byte array. + Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter. + The data written to `topic` is equal to the data received by the last + `group_topic` callback. + + :return topic + """ + + error = c_int() + size = self.group_get_topic_size(groupnumber) + topic = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_topic(self._tox_pointer, groupnumber, topic, byref(error)) + return str(topic[:size], 'utf-8') + + def group_get_name_size(self, groupnumber): + """ + Return the length of the group name. If the group number is invalid, the + return value is unspecified. + """ + error = c_int() + result = Tox.libtoxcore.tox_group_get_name_size(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_get_name(self, groupnumber): + """ + Write the name of the group designated by the given group number to a byte array. + Call tox_group_get_name_size to determine the allocation size for the `name` parameter. + :return true on success. + """ + + error = c_int() + size = self.group_get_name_size(groupnumber) + name = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_name(self._tox_pointer, groupnumber, + name, byref(error)) + return str(name[:size], 'utf-8') + + def group_get_chat_id(self, groupnumber): + """ + 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 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, + buff, byref(error)) + return bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE) + + def group_get_number_groups(self): + """ + Return the number of groups in the Tox chats array. + """ + + result = Tox.libtoxcore.tox_group_get_number_groups(self._tox_pointer) + return result + + def group_get_privacy_state(self, groupnumber): + """ + Return the privacy state of the group designated by the given group number. If group number + is invalid, the return value is unspecified. + + The value returned is equal to the data received by the last + `group_privacy_state` callback. + + see the `Group chat founder controls` section for the respective set function. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_privacy_state(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_get_peer_limit(self, groupnumber): + """ + Return the maximum number of peers allowed for the group designated by the given group number. + If the group number is invalid, the return value is unspecified. + + The value returned is equal to the data received by the last + `group_peer_limit` callback. + + see the `Group chat founder controls` section for the respective set function. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_peer_limit(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_get_password_size(self, groupnumber): + """ + Return the length of the group password. If the group number is invalid, the + return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_get_password_size(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_get_password(self, groupnumber): + """ + Write the password for the group designated by the given group number to a byte array. + + Call tox_group_get_password_size to determine the allocation size for the `password` parameter. + + The data received is equal to the data received by the last + `group_password` callback. + + see the `Group chat founder controls` section for the respective set function. + + :return password + """ + + error = c_int() + size = self.group_get_password_size(groupnumber) + password = create_string_buffer(size) + result = Tox.libtoxcore.tox_group_get_password(self._tox_pointer, groupnumber, + password, byref(error)) + return str(password[:size], 'utf-8') + + def callback_group_topic(self, callback, user_data): + """ + Set the callback for the `group_topic` event. Pass NULL to unset. + This event is triggered when a peer changes the group topic. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_topic_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_topic(self._tox_pointer, self.group_topic_cb, user_data) + + def callback_group_privacy_state(self, callback, user_data): + """ + Set the callback for the `group_privacy_state` event. Pass NULL to unset. + This event is triggered when the group founder changes the privacy state. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) + self.group_privacy_state_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_privacy_state(self._tox_pointer, self.group_privacy_state_cb, user_data) + + def callback_group_peer_limit(self, callback, user_data): + """ + Set the callback for the `group_peer_limit` event. Pass NULL to unset. + This event is triggered when the group founder changes the maximum peer limit. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.group_peer_limit_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_limit(self._tox_pointer, self.group_peer_limit_cb, user_data) + + def callback_group_password(self, callback, user_data): + """ + Set the callback for the `group_password` event. Pass NULL to unset. + This event is triggered when the group founder changes the group password. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_password_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_password(self._tox_pointer, self.group_password_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group message sending + # ----------------------------------------------------------------------------------------------------------------- + + def group_send_custom_packet(self, groupnumber, lossless, data): + """ + Send a custom packet to the group. + + If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + to TCP (reliability, arrive in order) but with packets instead of a stream. + + If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + meaning they might never reach the other side or might arrive more than once (if someone + is messing with the connection) or might arrive in the wrong order. + + Unless latency is an issue or message reliability is not important, it is recommended that you use + lossless custom packets. + + :param groupnumber: The group number of the group the message is intended for. + :param lossless: True if the packet should be lossless. + :param data A byte array containing the packet data. + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_custom_packet(self._tox_pointer, groupnumber, lossless, data, + len(data), byref(error)) + return result + + def group_send_private_message(self, groupnumber, peer_id, message): + """ + Send a text chat message to the specified peer in the specified group. + + This function creates a group private message packet and pushes it into the send + queue. + + The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages + must be split by the client and sent as separate messages. Other clients can + then reassemble the fragments. Messages may not be empty. + + :param groupnumber: The group number of the group the message is intended for. + :param peer_id: The ID of the peer the message is intended for. + :param message: A non-NULL pointer to the first element of a byte array containing the message text. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_private_message(self._tox_pointer, groupnumber, peer_id, message, + len(message), byref(error)) + return result + + def group_send_message(self, groupnumber, type, message): + """ + Send a text chat message to the group. + + This function creates a group message packet and pushes it into the send + queue. + + The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages + must be split by the client and sent as separate messages. Other clients can + then reassemble the fragments. Messages may not be empty. + + :param groupnumber: The group number of the group the message is intended for. + :param type: Message type (normal, action, ...). + :param message: A non-NULL pointer to the first element of a byte array containing the message text. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_send_message(self._tox_pointer, groupnumber, type, message, len(message), + byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group message receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_group_message(self, callback, user_data): + """ + 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) + 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_private_message(self, callback, user_data): + """ + Set the callback for the `group_private_message` event. Pass NULL to unset. + This event is triggered when the client receives a private message. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_private_message_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_private_message(self._tox_pointer, self.group_private_message_cb, user_data) + + def callback_group_custom_packet(self, callback, user_data): + """ + Set the callback for the `group_custom_packet` event. Pass NULL to unset. + + This event is triggered when the client receives a custom packet. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, POINTER(c_uint8), c_void_p) + self.group_custom_packet_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_custom_packet(self._tox_pointer, self.group_custom_packet_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat inviting and join/part events + # ----------------------------------------------------------------------------------------------------------------- + + def group_invite_friend(self, groupnumber, friend_number): + """ + Invite a friend to a group. + + This function creates an invite request packet and pushes it to the send queue. + + :param groupnumber: The group number of the group the message is intended for. + :param friend_number: The friend number of the friend the invite is intended for. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_invite_friend(self._tox_pointer, groupnumber, friend_number, byref(error)) + return result + + def group_invite_accept(self, invite_data, friend_number, 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. + + :param invite_data: The invite data received from the `group_invite` event. + :param password: The password required to join the group. Set to NULL if no password is required. + :return the groupnumber on success, UINT32_MAX on failure. + """ + + error = c_int() + f = Tox.libtoxcore.tox_group_invite_accept + f.restype = c_uint32 + result = f(self._tox_pointer, friend_number, invite_data, len(invite_data), password, + len(password) if password is not None else 0, byref(error)) + print('Invite accept. Result:', result, 'Error:', error.value) + return result + + def callback_group_invite(self, callback, user_data): + """ + Set the callback for the `group_invite` event. Pass NULL to unset. + + 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, POINTER(c_uint8), c_size_t, 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_peer_join(self, callback, user_data): + """ + Set the callback for the `group_peer_join` event. Pass NULL to unset. + + This event is triggered when a peer other than self joins the group. + Callback: python function with params: + tox - Tox* + group_number - group number + peer_id - peer id + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.group_peer_join_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_join(self._tox_pointer, self.group_peer_join_cb, user_data) + + def callback_group_peer_exit(self, callback, user_data): + """ + Set the callback for the `group_peer_exit` event. Pass NULL to unset. + + This event is triggered when a peer other than self exits the group. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p) + self.group_peer_exit_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_peer_exit(self._tox_pointer, self.group_peer_exit_cb, user_data) + + def callback_group_self_join(self, callback, user_data): + """ + Set the callback for the `group_self_join` event. Pass NULL to unset. + + This event is triggered when the client has successfully joined a group. Use this to initialize + any group information the client may need. + Callback: python fucntion with params: + tox - *Tox + group_number - group number + user_data - user data + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_void_p) + self.group_self_join_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_self_join(self._tox_pointer, self.group_self_join_cb, user_data) + + def callback_group_join_fail(self, callback, user_data): + """ + Set the callback for the `group_join_fail` event. Pass NULL to unset. + + This event is triggered when the client fails to join a group. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p) + self.group_join_fail_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_join_fail(self._tox_pointer, self.group_join_fail_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat founder controls (these only work for the group founder) + # ----------------------------------------------------------------------------------------------------------------- + + def group_founder_set_password(self, groupnumber, password): + """ + Set or unset the group password. + + This function sets the groups password, creates a new group shared state including the change, + and distributes it to the rest of the group. + + :param groupnumber: The group number of the group for which we wish to set the password. + :param password: The password we want to set. Set password to NULL to unset the password. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_password(self._tox_pointer, groupnumber, password, + len(password), byref(error)) + return result + + def group_founder_set_privacy_state(self, groupnumber, privacy_state): + """ + Set the group privacy state. + + This function sets the group's privacy state, creates a new group shared state + including the change, and distributes it to the rest of the group. + + If an attempt is made to set the privacy state to the same state that the group is already + in, the function call will be successful and no action will be taken. + + :param groupnumber: The group number of the group for which we wish to change the privacy state. + :param privacy_state: The privacy state we wish to set the group to. + + :return true on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_privacy_state(self._tox_pointer, groupnumber, privacy_state, + byref(error)) + return result + + def group_founder_set_peer_limit(self, groupnumber, max_peers): + """ + Set the group peer limit. + + This function sets a limit for the number of peers who may be in the group, creates a new + group shared state including the change, and distributes it to the rest of the group. + + :param groupnumber: The group number of the group for which we wish to set the peer limit. + :param max_peers: The maximum number of peers to allow in the group. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_founder_set_peer_limit(self._tox_pointer, groupnumber, + max_peers, byref(error)) + return result + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat moderation + # ----------------------------------------------------------------------------------------------------------------- + + def group_toggle_ignore(self, groupnumber, peer_id, ignore): + """ + Ignore or unignore a peer. + + :param groupnumber: The group number of the group the in which you wish to ignore a peer. + :param peer_id: The ID of the peer who shall be ignored or unignored. + :param ignore: True to ignore the peer, false to unignore the peer. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_toggle_ignore(self._tox_pointer, groupnumber, peer_id, ignore, byref(error)) + return result + + def group_mod_set_role(self, groupnumber, peer_id, role): + """ + Set a peer's role. + + This function will first remove the peer's previous role and then assign them a new role. + It will also send a packet to the rest of the group, requesting that they perform + the role reassignment. Note: peers cannot be set to the founder role. + + :param groupnumber: The group number of the group the in which you wish set the peer's role. + :param peer_id: The ID of the peer whose role you wish to set. + :param role: The role you wish to set the peer to. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_set_role(self._tox_pointer, groupnumber, peer_id, role, byref(error)) + return result + + def group_mod_remove_peer(self, groupnumber, peer_id, set_ban): + """ + Kick/ban a peer. + + This function will remove a peer from the caller's peer list and optionally add their IP address + to the ban list. It will also send a packet to all group members requesting them + to do the same. + + :param groupnumber: The group number of the group the ban is intended for. + :param peer_id: The ID of the peer who will be kicked and/or added to the ban list. + :param set_ban: Set to true if a ban shall be set on the peer's IP address. + + :return True on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_remove_peer(self._tox_pointer, groupnumber, peer_id, + set_ban, byref(error)) + return result + + def group_mod_remove_ban(self, groupnumber, ban_id): + """ + Removes a ban. + + This function removes a ban entry from the ban list, and sends a packet to the rest of + the group requesting that they do the same. + + :param groupnumber: The group number of the group in which the ban is to be removed. + :param ban_id: The ID of the ban entry that shall be removed. + + :return True on success + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_mod_remove_ban(self._tox_pointer, groupnumber, ban_id, byref(error)) + return result + + def callback_group_moderation(self, callback, user_data): + """ + Set the callback for the `group_moderation` event. Pass NULL to unset. + + This event is triggered when a moderator or founder executes a moderation event. + """ + + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_int, c_void_p) + self.group_moderation_cb = c_callback(callback) + Tox.libtoxcore.tox_callback_group_moderation(self._tox_pointer, self.group_moderation_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Group chat ban list queries + # ----------------------------------------------------------------------------------------------------------------- + + def group_ban_get_list_size(self, groupnumber): + """ + Return the number of entries in the ban list for the group designated by + the given group number. If the group number is invalid, the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_list_size(self._tox_pointer, groupnumber, byref(error)) + return result + + def group_ban_get_list(self, groupnumber): + """ + Copy a list of valid ban list ID's into an array. + + Call tox_group_ban_get_list_size to determine the number of elements to allocate. + return true on success. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_list(self._tox_pointer, groupnumber, POINTER(c_uint32)( + create_string_buffer(sizeof(c_uint32) * self.group_ban_get_list_size(groupnumber)), byref(error))) + return result + + def group_ban_get_name_size(self, groupnumber, ban_id): + """ + Return the length of the name for the ban list entry designated by ban_id, in the + group designated by the given group number. If either groupnumber or ban_id is invalid, + the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_name_size(self._tox_pointer, groupnumber, ban_id, byref(error)) + return result + + def group_ban_get_name(self, groupnumber, ban_id): + """ + Write the name of the ban entry designated by ban_id in the group designated by the + given group number to a byte array. + + Call tox_group_ban_get_name_size to find out how much memory to allocate for the result. + + :return name + """ + + error = c_int() + size = self.group_ban_get_name_size(groupnumber, ban_id) + name = create_string_buffer() + + result = Tox.libtoxcore.tox_group_ban_get_name(self._tox_pointer, groupnumber, ban_id, + name, byref(error)) + return str(name[:size], 'utf-8') + + def group_ban_get_time_set(self, groupnumber, ban_id): + """ + Return a time stamp indicating the time the ban was set, for the ban list entry + designated by ban_id, in the group designated by the given group number. + If either groupnumber or ban_id is invalid, the return value is unspecified. + """ + + error = c_int() + result = Tox.libtoxcore.tox_group_ban_get_time_set(self._tox_pointer, groupnumber, ban_id, byref(error)) + return result diff --git a/toxygen/toxcore_enums_and_consts.py b/toxygen/toxcore_enums_and_consts.py index 4d52837..6942d3a 100644 --- a/toxygen/toxcore_enums_and_consts.py +++ b/toxygen/toxcore_enums_and_consts.py @@ -188,6 +188,729 @@ TOX_ERR_GET_PORT = { 'NOT_BOUND': 1, } +TOX_GROUP_PRIVACY_STATE = { + + # + # The group is considered to be public. Anyone may join the group using the Chat ID. + # + # If the group is in this state, even if the Chat ID is never explicitly shared + # with someone outside of the group, information including the Chat ID, IP addresses, + # and peer ID's (but not Tox ID's) is visible to anyone with access to a node + # storing a DHT entry for the given group. + # + 'TOX_GROUP_PRIVACY_STATE_PUBLIC': 0, + + # + # The group is considered to be private. The only way to join the group is by having + # someone in your contact list send you an invite. + # + # If the group is in this state, no group information (mentioned above) is present in the DHT; + # the DHT is not used for any purpose at all. If a public group is set to private, + # all DHT information related to the group will expire shortly. + # + 'TOX_GROUP_PRIVACY_STATE_PRIVATE': 1 +} + +TOX_GROUP_ROLE = { + + # + # May kick and ban all other peers as well as set their role to anything (except founder). + # Founders may also set the group password, toggle the privacy state, and set the peer limit. + # + 'TOX_GROUP_ROLE_FOUNDER': 0, + + # + # May kick, ban and set the user and observer roles for peers below this role. + # May also set the group topic. + # + 'TOX_GROUP_ROLE_MODERATOR': 1, + + # + # May communicate with other peers normally. + # + 'TOX_GROUP_ROLE_USER': 2, + + # + # May observe the group and ignore peers; may not communicate with other peers or with the group. + # + 'TOX_GROUP_ROLE_OBSERVER': 3 +} + +TOX_ERR_GROUP_NEW = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_NEW_OK': 0, + + # + # The group name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH. + # + 'TOX_ERR_GROUP_NEW_TOO_LONG': 1, + + # + # group_name is NULL or length is zero. + # + 'TOX_ERR_GROUP_NEW_EMPTY': 2, + + # + # TOX_GROUP_PRIVACY_STATE is an invalid type. + # + 'TOX_ERR_GROUP_NEW_PRIVACY': 3, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_NEW_INIT': 4, + + # + # The group state failed to initialize. This usually indicates that something went wrong + # related to cryptographic signing. + # + 'TOX_ERR_GROUP_NEW_STATE': 5, + + # + # The group failed to announce to the DHT. This indicates a network related error. + # + 'TOX_ERR_GROUP_NEW_ANNOUNCE': 6, +} + +TOX_ERR_GROUP_JOIN = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_JOIN_OK': 0, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_JOIN_INIT': 1, + + # + # The chat_id pointer is set to NULL or a group with chat_id already exists. This usually + # happens if the client attempts to create multiple sessions for the same group. + # + 'TOX_ERR_GROUP_JOIN_BAD_CHAT_ID': 2, + + # + # Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_JOIN_TOO_LONG': 3, +} + +TOX_ERR_GROUP_RECONNECT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_RECONNECT_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND': 1, +} + +TOX_ERR_GROUP_LEAVE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_LEAVE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_GROUP_MAX_PART_LENGTH. + # + 'TOX_ERR_GROUP_LEAVE_TOO_LONG': 2, + + # + # The parting packet failed to send. + # + 'TOX_ERR_GROUP_LEAVE_FAIL_SEND': 3, + + # + # The group chat instance failed to be deleted. This may occur due to memory related errors. + # + 'TOX_ERR_GROUP_LEAVE_DELETE_FAIL': 4, +} + +TOX_ERR_GROUP_SELF_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND': 1, +} + + +TOX_ERR_GROUP_SELF_NAME_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND': 1, + + # + # Name length exceeded 'TOX_MAX_NAME_LENGTH. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG': 2, + + # + # The length given to the set function is zero or name is a NULL pointer. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_INVALID': 3, + + # + # The name is already taken by another peer in the group. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_TAKEN': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_SELF_STATUS_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND': 1, + + # + # An invalid type was passed to the set function. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_INVALID': 2, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND': 3 +} + +TOX_ERR_GROUP_PEER_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_PEER_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND': 2 +} + +TOX_ERR_GROUP_STATE_QUERIES = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_STATE_QUERIES_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND': 1 +} + + +TOX_ERR_GROUP_TOPIC_SET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_TOPIC_SET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND': 1, + + # + # Topic length exceeded 'TOX_GROUP_MAX_TOPIC_LENGTH. + # + 'TOX_ERR_GROUP_TOPIC_SET_TOO_LONG': 2, + + # + # The caller does not have the required permissions to set the topic. + # + 'TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS': 3, + + # + # The packet could not be created. This error is usually related to cryptographic signing. + # + 'TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_SEND_MESSAGE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG': 2, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_EMPTY': 3, + + # + # The message type is invalid. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE': 4, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS': 5, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND': 6 +} + +TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND': 2, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG': 3, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY': 4, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS': 5, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND': 6 +} + +TOX_ERR_GROUP_SEND_CUSTOM_PACKET = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND': 1, + + # + # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG': 2, + + # + # The message pointer is null or length is zero. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY': 3, + + # + # The caller does not have the required permissions to send group messages. + # + 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS': 4 +} + +TOX_ERR_GROUP_INVITE_FRIEND = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND': 1, + + # + # The friend number passed did not designate a valid friend. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND': 2, + + # + # Creation of the invite packet failed. This indicates a network related error. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL': 3, + + # + # Packet failed to send. + # + 'TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_INVITE_ACCEPT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_OK': 0, + + # + # The invite data is not in the expected format. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE': 1, + + # + # The group instance failed to initialize. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED': 2, + + # + # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG': 3 +} + +TOX_GROUP_JOIN_FAIL = { + + # + # You are using the same nickname as someone who is already in the group. + # + 'TOX_GROUP_JOIN_FAIL_NAME_TAKEN': 0, + + # + # The group peer limit has been reached. + # + 'TOX_GROUP_JOIN_FAIL_PEER_LIMIT': 1, + + # + # You have supplied an invalid password. + # + 'TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD': 2, + + # + # The join attempt failed due to an unspecified error. This often occurs when the group is + # not found in the DHT. + # + 'TOX_GROUP_JOIN_FAIL_UNKNOWN': 3 +} + +TOX_ERR_GROUP_FOUNDER_SET_PASSWORD = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions to set the password. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS': 2, + + # + # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND': 1, + + # + # 'TOX_GROUP_PRIVACY_STATE is an invalid type. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_INVALID': 2, + + # + # The caller does not have the required permissions to set the privacy state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS': 3, + + # + # The privacy state could not be set. This may occur due to an error related to + # cryptographic signing of the new shared state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions to set the peer limit. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS': 2, + + # + # The peer limit could not be set. This may occur due to an error related to + # cryptographic signing of the new shared state. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND': 4 +} + +TOX_ERR_GROUP_TOGGLE_IGNORE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_TOGGLE_IGNORE_PEER_NOT_FOUND': 2 +} + +TOX_ERR_GROUP_MOD_SET_ROLE = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. Note: you cannot set your own role. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND': 2, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS': 3, + + # + # The role assignment is invalid. This will occur if you try to set a peer's role to + # the role they already have. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT': 4, + + # + # The role was not successfully set. This may occur if something goes wrong with role setting': , + # or if the packet fails to send. + # + 'TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION': 5 +} + +TOX_ERR_GROUP_MOD_REMOVE_PEER = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_GROUP_NOT_FOUND': 1, + + # + # The ID passed did not designate a valid peer. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PEER_NOT_FOUND': 2, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PERMISSIONS': 3, + + # + # The peer could not be removed from the group. + # + # If a ban was set': , this error indicates that the ban entry could not be created. + # This is usually due to the peer's IP address already occurring in the ban list. It may also + # be due to the entry containing invalid peer information': , or a failure to cryptographically + # authenticate the entry. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_ACTION': 4, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_SEND': 5 +} + +TOX_ERR_GROUP_MOD_REMOVE_BAN = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_GROUP_NOT_FOUND': 1, + + # + # The caller does not have the required permissions for this action. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_PERMISSIONS': 2, + + # + # The ban entry could not be removed. This may occur if ban_id does not designate + # a valid ban entry. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_ACTION': 3, + + # + # The packet failed to send. + # + 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_SEND': 4 +} + +TOX_GROUP_MOD_EVENT = { + + # + # A peer has been kicked from the group. + # + 'TOX_GROUP_MOD_EVENT_KICK': 0, + + # + # A peer has been banned from the group. + # + 'TOX_GROUP_MOD_EVENT_BAN': 1, + + # + # A peer as been given the observer role. + # + 'TOX_GROUP_MOD_EVENT_OBSERVER': 2, + + # + # A peer has been given the user role. + # + 'TOX_GROUP_MOD_EVENT_USER': 3, + + # + # A peer has been given the moderator role. + # + 'TOX_GROUP_MOD_EVENT_MODERATOR': 4, +} + +TOX_ERR_GROUP_BAN_QUERY = { + + # + # The function returned successfully. + # + 'TOX_ERR_GROUP_BAN_QUERY_OK': 0, + + # + # The group number passed did not designate a valid group. + # + 'TOX_ERR_GROUP_BAN_QUERY_GROUP_NOT_FOUND': 1, + + # + # The ban_id does not designate a valid ban list entry. + # + 'TOX_ERR_GROUP_BAN_QUERY_BAD_ID': 2, +} + TOX_PUBLIC_KEY_SIZE = 32 TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6 @@ -196,6 +919,18 @@ TOX_MAX_FRIEND_REQUEST_LENGTH = 1016 TOX_MAX_MESSAGE_LENGTH = 1372 +TOX_GROUP_MAX_TOPIC_LENGTH = 512 + +TOX_GROUP_MAX_PART_LENGTH = 128 + +TOX_GROUP_MAX_GROUP_NAME_LENGTH = 48 + +TOX_GROUP_MAX_PASSWORD_SIZE = 32 + +TOX_GROUP_CHAT_ID_SIZE = 32 + +TOX_GROUP_PEER_PUBLIC_KEY_SIZE = 32 + TOX_MAX_NAME_LENGTH = 128 TOX_MAX_STATUS_MESSAGE_LENGTH = 1007