diff --git a/gajim/chat_control.py b/gajim/chat_control.py index f3c7975e2..6fdfcad0e 100644 --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -1482,12 +1482,12 @@ class ChatControl(ChatControlBase): self._add_info_bar_message(markup, [b], file_props, Gtk.MessageType.ERROR) def _on_accept_gc_invitation(self, widget, event): - if event.is_continued: - app.interface.join_gc_room(self.account, event.room_jid, + if event.continued: + app.interface.join_gc_room(self.account, str(event.muc), app.nicks[self.account], event.password, is_continued=True) else: - app.interface.join_gc_minimal(self.account, event.room_jid) + app.interface.join_gc_minimal(self.account, str(event.muc)) app.events.remove_events(self.account, self.contact.jid, event=event) @@ -1495,14 +1495,14 @@ class ChatControl(ChatControlBase): app.events.remove_events(self.account, self.contact.jid, event=event) def _get_gc_invitation(self, event): - markup = '%s: %s' % (_('Groupchat Invitation'), event.room_jid) + markup = '%s: %s' % (_('Groupchat Invitation'), event.muc) if event.reason: markup += ' (%s)' % event.reason b1 = Gtk.Button.new_with_mnemonic(_('_Join')) b1.connect('clicked', self._on_accept_gc_invitation, event) b2 = Gtk.Button(stock=Gtk.STOCK_CANCEL) b2.connect('clicked', self._on_cancel_gc_invitation, event) - self._add_info_bar_message(markup, [b1, b2], (event.room_jid, + self._add_info_bar_message(markup, [b1, b2], (event.muc, event.reason), Gtk.MessageType.QUESTION) def on_event_added(self, event): @@ -1546,7 +1546,7 @@ class ChatControl(ChatControlBase): removed = False for ib_msg in self.info_bar_queue: if ev.type_ == 'gc-invitation': - if ev.room_jid == ib_msg[2][0]: + if ev.muc == ib_msg[2][0]: self.info_bar_queue.remove(ib_msg) removed = True else: # file-* diff --git a/gajim/common/app.py b/gajim/common/app.py index d849b7e9b..d693d4d55 100644 --- a/gajim/common/app.py +++ b/gajim/common/app.py @@ -480,6 +480,7 @@ def zeroconf_is_connected(): config.get_per('accounts', ZEROCONF_ACC_NAME, 'is_zeroconf') def in_groupchat(account, room_jid): + room_jid = str(room_jid) if room_jid not in gc_connected[account]: return False return gc_connected[account][room_jid] diff --git a/gajim/common/contacts.py b/gajim/common/contacts.py index 63b2d3363..da084ae32 100644 --- a/gajim/common/contacts.py +++ b/gajim/common/contacts.py @@ -307,12 +307,12 @@ class LegacyContactsAPI: return self_contact def create_not_in_roster_contact(self, jid, account, resource='', name='', - keyID=''): + keyID='', groupchat=False): # Use Account object if available account = self._accounts.get(account, account) return self.create_contact(jid=jid, account=account, resource=resource, name=name, groups=[_('Not in Roster')], show='not in roster', - status='', sub='none', keyID=keyID) + status='', sub='none', keyID=keyID, groupchat=groupchat) def copy_contact(self, contact): return self.create_contact(contact.jid, contact.account, diff --git a/gajim/common/events.py b/gajim/common/events.py index 372c092ab..a33da82bc 100644 --- a/gajim/common/events.py +++ b/gajim/common/events.py @@ -130,15 +130,10 @@ class UnsubscribedEvent(Event): class GcInvitationtEvent(Event): type_ = 'gc-invitation' - def __init__(self, room_jid, reason, password, is_continued, jid_from, - time_=None, show_in_roster=False, show_in_systray=True): - Event.__init__(self, time_, show_in_roster=show_in_roster, - show_in_systray=show_in_systray) - self.room_jid = room_jid - self.reason = reason - self.password = password - self.is_continued = is_continued - self.jid_from = jid_from + def __init__(self, event): + Event.__init__(self, None, show_in_roster=False, show_in_systray=True) + for key, value in vars(event).items(): + setattr(self, key, value) class FileRequestEvent(Event): type_ = 'file-request' diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 269332921..7ededa758 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -1462,6 +1462,15 @@ def load_json(path, key=None, default=None): return json_dict return json_dict.get(key, default) +def ignore_contact(account, jid): + jid = str(jid) + known_contact = app.contacts.get_contacts(account, jid) + ignore = app.config.get_per('accounts', account, 'ignore_unknown_contacts') + if ignore and not known_contact: + log.info('Ignore unknown contact %s', jid) + return True + return False + class AdditionalDataDict(collections.UserDict): def __init__(self, initialdata=None): collections.UserDict.__init__(self, initialdata) diff --git a/gajim/common/modules/bits_of_binary.py b/gajim/common/modules/bits_of_binary.py index 2bae0eb39..37745ac1e 100644 --- a/gajim/common/modules/bits_of_binary.py +++ b/gajim/common/modules/bits_of_binary.py @@ -170,5 +170,29 @@ def parse_bob_data(stanza): return filepath +def store_bob_data(bob_data): + if bob_data is None: + return + + algo_hash = '%s+%s' % (bob_data.algo, bob_data.hash_) + + filepath = Path(configpaths.get('BOB')) / algo_hash + if algo_hash in app.bob_cache or filepath.exists(): + log.info('BoB data already cached') + return + + if bob_data.max_age == 0: + app.bob_cache[algo_hash] = bob_data.data + else: + try: + with open(str(filepath), 'w+b') as file: + file.write(bob_data.data) + except Exception: + log.exception('Unable to save data') + return + log.info('BoB data stored: %s', algo_hash) + return filepath + + def get_instance(*args, **kwargs): return BitsOfBinary(*args, **kwargs), 'BitsOfBinary' diff --git a/gajim/common/modules/message.py b/gajim/common/modules/message.py index 3f3e601b5..c301cd213 100644 --- a/gajim/common/modules/message.py +++ b/gajim/common/modules/message.py @@ -212,31 +212,14 @@ class Message: subject = event.stanza.getSubject() groupchat = event.mtype == 'groupchat' - # XEP-0045: only a message that contains a but no - # element shall be considered a subject change for MUC purposes. - muc_subject = subject and groupchat and not event.msgtxt - # Determine timestamps - if groupchat: - delay_entity_jid = event.jid - else: - delay_entity_jid = self._con.get_own_jid().getDomain() - - if muc_subject: - # MUC Subjects can have a delay timestamp - # to indicate when the user has set the subject, - # the 'from' attr on these delays is the MUC server - # but we treat it as user timestamp + delay_jid = self._con.get_own_jid().getDomain() + timestamp = parse_delay(event.stanza, from_=delay_jid) + if timestamp is None: timestamp = time.time() - user_timestamp = parse_delay(event.stanza, from_=delay_entity_jid) - else: - timestamp = parse_delay(event.stanza, from_=delay_entity_jid) - if timestamp is None: - timestamp = time.time() - - user_timestamp = parse_delay(event.stanza, - not_from=[delay_entity_jid]) + user_timestamp = parse_delay(event.stanza, + not_from=[delay_jid]) if user_timestamp is not None: event.additional_data.set_value( @@ -271,11 +254,6 @@ class Message: event.session, event.fjid, timestamp) return - if muc_subject: - app.nec.push_incoming_event(NetworkEvent('gc-subject-received', - **vars(event))) - return - if groupchat: if not event.msgtxt: return diff --git a/gajim/common/modules/muc.py b/gajim/common/modules/muc.py index 08182fc12..58f14c849 100644 --- a/gajim/common/modules/muc.py +++ b/gajim/common/modules/muc.py @@ -17,18 +17,17 @@ import time import logging -import weakref import nbxmpp +from nbxmpp.const import InviteType +from nbxmpp.structs import StanzaHandler from gajim.common import i18n from gajim.common import app from gajim.common import helpers from gajim.common.caps_cache import muc_caps_cache from gajim.common.nec import NetworkEvent -from gajim.common.nec import NetworkIncomingEvent -from gajim.common.modules import dataforms -from gajim.common.modules.bits_of_binary import parse_bob_data +from gajim.common.modules.bits_of_binary import store_bob_data log = logging.getLogger('gajim.c.m.muc') @@ -39,13 +38,55 @@ class MUC: self._account = con.name self.handlers = [ - ('message', self._on_config_change, '', nbxmpp.NS_MUC_USER), - ('message', self._mediated_invite, 'normal', nbxmpp.NS_MUC_USER), - ('message', self._direct_invite, '', nbxmpp.NS_CONFERENCE), - ('message', self._on_captcha_challenge, '', nbxmpp.NS_CAPTCHA), - ('message', self._on_voice_request, '', nbxmpp.NS_DATA, 45), + StanzaHandler(name='message', + callback=self._on_subject_change, + typ='groupchat', + priority=49), + StanzaHandler(name='message', + callback=self._on_config_change, + ns=nbxmpp.NS_MUC_USER, + priority=49), + StanzaHandler(name='message', + callback=self._on_invite_or_decline, + typ='normal', + ns=nbxmpp.NS_MUC_USER, + priority=49), + StanzaHandler(name='message', + callback=self._on_invite_or_decline, + ns=nbxmpp.NS_CONFERENCE, + priority=49), + StanzaHandler(name='message', + callback=self._on_captcha_challenge, + ns=nbxmpp.NS_CAPTCHA, + priority=49), + StanzaHandler(name='message', + callback=self._on_voice_request, + ns=nbxmpp.NS_DATA, + priority=49) ] + self._nbmxpp_methods = [ + 'get_affiliation', + 'set_role', + 'set_affiliation', + 'set_config', + 'set_subject', + 'cancel_config', + 'send_captcha', + 'decline', + 'request_voice' + 'destroy', + ] + + def __getattr__(self, key): + if key in self._nbmxpp_methods: + if not app.account_is_connected(self._account): + log.warning('Account %s not connected, cant use %s', + self._account, key) + return + module = self._con.connection.get_module('MUC') + return getattr(module, key) + def pass_disco(self, from_, identities, features, _data, _node): for identity in identities: if identity.get('category') != 'conference': @@ -108,438 +149,136 @@ class MUC: if tags: muc_x.setTag('history', tags) - def set_subject(self, room_jid, subject): - if not app.account_is_connected(self._account): - return - message = nbxmpp.Message(room_jid, typ='groupchat', subject=subject) - log.info('Set subject for %s', room_jid) - self._con.connection.send(message) - - def _on_voice_request(self, _con, stanza): - data_form = stanza.getTag('x', namespace=nbxmpp.NS_DATA) - if data_form is None: + def _on_subject_change(self, _con, _stanza, properties): + if not properties.is_muc_subject: return - if stanza.getBody(): - return - - room_jid = str(stanza.getFrom()) - contact = app.contacts.get_groupchat_contact(self._account, room_jid) + jid = properties.jid.getBare() + contact = app.contacts.get_groupchat_contact(self._account, jid) if contact is None: return - data_form = dataforms.extend_form(data_form) - try: - if data_form['FORM_TYPE'].value != nbxmpp.NS_MUC_REQUEST: - return - except KeyError: + contact.status = properties.subject + + app.nec.push_incoming_event( + NetworkEvent('gc-subject-received', + account=self._account, + jid=jid, + subject=properties.subject, + nickname=properties.muc_nickname, + user_timestamp=properties.user_timestamp)) + raise nbxmpp.NodeProcessed + + def _on_voice_request(self, _con, _stanza, properties): + if not properties.is_voice_request: + return + + jid = str(properties.jid) + contact = app.contacts.get_groupchat_contact(self._account, jid) + if contact is None: return app.nec.push_incoming_event( NetworkEvent('voice-approval', account=self._account, - room_jid=room_jid, - form=data_form)) + jid=jid, + form=properties.voice_request.form)) raise nbxmpp.NodeProcessed - def _on_captcha_challenge(self, _con, stanza): - captcha = stanza.getTag('captcha', namespace=nbxmpp.NS_CAPTCHA) - if captcha is None: + def _on_captcha_challenge(self, _con, _stanza, properties): + if not properties.is_captcha_challenge: return - parse_bob_data(stanza) - room_jid = str(stanza.getFrom()) - contact = app.contacts.get_groupchat_contact(self._account, room_jid) + contact = app.contacts.get_groupchat_contact(self._account, + str(properties.jid)) if contact is None: return - log.info('Captcha challenge received from %s', room_jid) - data_form = captcha.getTag('x', namespace=nbxmpp.NS_DATA) - data_form = dataforms.extend_form(node=data_form) + log.info('Captcha challenge received from %s', properties.jid) + store_bob_data(properties.captcha.bob_data) + app.nec.push_incoming_event( NetworkEvent('captcha-challenge', account=self._account, - room_jid=room_jid, - form=data_form)) + jid=properties.jid, + form=properties.captcha.form)) raise nbxmpp.NodeProcessed - def send_captcha(self, room_jid, form_node): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='set', to=room_jid) - captcha = iq.addChild(name='captcha', namespace=nbxmpp.NS_CAPTCHA) - captcha.addChild(node=form_node) - self._con.connection.send(iq) - - def request_config(self, room_jid): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='get', - queryNS=nbxmpp.NS_MUC_OWNER, - to=room_jid) - iq.setAttr('xml:lang', i18n.LANG) - log.info('Request config for %s', room_jid) - self._con.connection.SendAndCallForResponse( - iq, self._config_received) - - def _config_received(self, stanza): - if not nbxmpp.isResultNode(stanza): - log.info('Error: %s', stanza.getError()) + def _on_config_change(self, _con, _stanza, properties): + if not properties.is_muc_config_change: return - room_jid = stanza.getFrom().getStripped() - payload = stanza.getQueryPayload() - - for form in payload: - if form.getNamespace() == nbxmpp.NS_DATA: - dataform = dataforms.extend_form(node=form) - log.info('Config form received for %s', room_jid) - app.nec.push_incoming_event(MucOwnerReceivedEvent( - None, - conn=self._con, - form_node=form, - dataform=dataform, - jid=room_jid)) - break - - def cancel_config(self, room_jid): - if not app.account_is_connected(self._account): - return - cancel = nbxmpp.Node(tag='x', attrs={'xmlns': nbxmpp.NS_DATA, - 'type': 'cancel'}) - iq = nbxmpp.Iq(typ='set', - queryNS=nbxmpp.NS_MUC_OWNER, - payload=cancel, - to=room_jid) - log.info('Cancel config for %s', room_jid) - self._con.connection.SendAndCallForResponse( - iq, self._default_response, {}) - - def _on_config_change(self, _con, stanza): - muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) - if muc_user is None: - return - - if stanza.getBody(): - return - - room_list = app.contacts.get_gc_list(self._account) - room_jid = str(stanza.getFrom()) - if room_jid not in room_list: - # Message not from a group chat - return - - # https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes - change_codes = ['100', '102', '103', '104', - '170', '171', '172', '173', '174'] - - codes = set() - for status in muc_user.getTags('status'): - code = status.getAttr('code') - if code in change_codes: - codes.add(code) - - if not codes: - return - - log.info('Received config change: %s', codes) + log.info('Received config change: %s %s', + properties.jid, properties.muc_status_codes) app.nec.push_incoming_event( NetworkEvent('gc-config-changed-received', account=self._account, - room_jid=room_jid, - status_codes=codes)) + jid=properties.jid, + status_codes=properties.muc_status_codes)) raise nbxmpp.NodeProcessed - def destroy(self, room_jid, reason='', jid=''): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='set', - queryNS=nbxmpp.NS_MUC_OWNER, - to=room_jid) - destroy = iq.setQuery().setTag('destroy') - if reason: - destroy.setTagData('reason', reason) - if jid: - destroy.setAttr('jid', jid) - log.info('Destroy room: %s, reason: %s, alternate: %s', - room_jid, reason, jid) - self._con.connection.SendAndCallForResponse( - iq, self._default_response, {}) + def _on_invite_or_decline(self, _con, _stanza, properties): + if properties.muc_decline is not None: + data = properties.muc_decline + if helpers.ignore_contact(self._account, data.from_): + raise nbxmpp.NodeProcessed - def set_config(self, room_jid, form): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='set', to=room_jid, queryNS=nbxmpp.NS_MUC_OWNER) - query = iq.setQuery() - form.setAttr('type', 'submit') - query.addChild(node=form) - log.info('Set config for %s', room_jid) - self._con.connection.SendAndCallForResponse( - iq, self._default_response, {}) + log.info('Invite declined from: %s, reason: %s', + data.from_, data.reason) - def set_affiliation(self, room_jid, users_dict): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='set', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN) - item = iq.setQuery() - for jid in users_dict: - affiliation = users_dict[jid].get('affiliation') - reason = users_dict[jid].get('reason') - nick = users_dict[jid].get('nick') - item_tag = item.addChild('item', {'jid': jid, - 'affiliation': affiliation}) - if reason is not None: - item_tag.setTagData('reason', reason) - - if nick is not None: - item_tag.setAttr('nick', nick) - log.info('Set affiliation for %s: %s', room_jid, users_dict) - self._con.connection.SendAndCallForResponse( - iq, self._default_response, {}) - - def get_affiliation(self, room_jid, affiliation, success_cb, error_cb): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='get', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN) - item = iq.setQuery().setTag('item') - item.setAttr('affiliation', affiliation) - log.info('Get affiliation %s for %s', affiliation, room_jid) - - weak_success_cb = weakref.WeakMethod(success_cb) - weak_error_cb = weakref.WeakMethod(error_cb) - - self._con.connection.SendAndCallForResponse( - iq, self._affiliation_received, {'affiliation': affiliation, - 'success_cb': weak_success_cb, - 'error_cb': weak_error_cb}) - - def _affiliation_received(self, _con, stanza, affiliation, - success_cb, error_cb): - if not nbxmpp.isResultNode(stanza): - if error_cb() is not None: - error_cb()(affiliation, stanza.getError()) - return - - room_jid = stanza.getFrom().getStripped() - query = stanza.getTag('query', namespace=nbxmpp.NS_MUC_ADMIN) - items = query.getTags('item') - users_dict = {} - for item in items: - try: - jid = helpers.parse_jid(item.getAttr('jid')) - except helpers.InvalidFormat: - log.warning('Invalid JID: %s, ignoring it', - item.getAttr('jid')) - continue - - users_dict[jid] = {} - if item.has_attr('nick'): - users_dict[jid]['nick'] = item.getAttr('nick') - if item.has_attr('role'): - users_dict[jid]['role'] = item.getAttr('role') - reason = item.getTagData('reason') - if reason: - users_dict[jid]['reason'] = reason - - log.info('%s affiliations received from %s: %s', - affiliation, room_jid, users_dict) - - if success_cb() is not None: - success_cb()(self._account, room_jid, affiliation, users_dict) - - def set_role(self, room_jid, nick, role, reason=''): - if not app.account_is_connected(self._account): - return - iq = nbxmpp.Iq(typ='set', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN) - item = iq.setQuery().setTag('item') - item.setAttr('nick', nick) - item.setAttr('role', role) - if reason: - item.addChild(name='reason', payload=reason) - log.info('Set role for %s: %s %s %s', room_jid, nick, role, reason) - self._con.connection.SendAndCallForResponse( - iq, self._default_response, {}) - - def _mediated_invite(self, _con, stanza): - muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) - if muc_user is None: - return - - if stanza.getType() == 'error': - return - - if stanza.getBody(): - return - - decline = muc_user.getTag('decline') - if decline is not None: - - room_jid = stanza.getFrom().getStripped() - from_ = self._get_from(room_jid, decline) - - reason = decline.getTagData('reason') - log.info('Invite declined: %s, %s', reason, from_) app.nec.push_incoming_event( - GcDeclineReceived(None, - account=self._account, - from_=from_, - room_jid=room_jid, - reason=reason)) + NetworkEvent('gc-decline-received', + account=self._account, + **data._asdict())) raise nbxmpp.NodeProcessed - invite = muc_user.getTag('invite') - if invite is not None: + if properties.muc_invite is not None: + data = properties.muc_invite + if helpers.ignore_contact(self._account, data.from_): + raise nbxmpp.NodeProcessed - room_jid = stanza.getFrom().getStripped() - from_ = self._get_from(room_jid, invite) + log.info('Invite from: %s, to: %s', data.from_, data.muc) - reason = invite.getTagData('reason') - password = muc_user.getTagData('password') - is_continued = invite.getTag('continue') is not None - log.info('Mediated invite: continued: %s, reason: %s, from: %s', - is_continued, reason, from_) - if room_jid in app.gc_connected[self._account] and \ - app.gc_connected[self._account][room_jid]: + if app.in_groupchat(self._account, data.muc): # We are already in groupchat. Ignore invitation log.info('We are already in this room') raise nbxmpp.NodeProcessed app.nec.push_incoming_event( - GcInvitationReceived(None, - account=self._account, - from_=from_, - room_jid=room_jid, - reason=reason, - password=password, - is_continued=is_continued)) + NetworkEvent('gc-invitation-received', + account=self._account, + **data._asdict())) raise nbxmpp.NodeProcessed - def _get_from(self, room_jid, stanza): - try: - from_ = nbxmpp.JID(helpers.parse_jid(stanza.getAttr('from'))) - except helpers.InvalidFormat: - log.warning('Invalid JID on invite: %s, ignoring it', - stanza.getAttr('from')) - raise nbxmpp.NodeProcessed - - known_contact = app.contacts.get_contacts(self._account, room_jid) - ignore = app.config.get_per( - 'accounts', self._account, 'ignore_unknown_contacts') - if ignore and not known_contact: - log.info('Ignore invite from unknown contact %s', from_) - raise nbxmpp.NodeProcessed - - return from_ - - def _direct_invite(self, _con, stanza): - direct = stanza.getTag('x', namespace=nbxmpp.NS_CONFERENCE) - if direct is None: - return - - from_ = stanza.getFrom() - - try: - room_jid = helpers.parse_jid(direct.getAttr('jid')) - except helpers.InvalidFormat: - log.warning('Invalid JID on invite: %s, ignoring it', - direct.getAttr('jid')) - raise nbxmpp.NodeProcessed - - reason = direct.getAttr('reason') - password = direct.getAttr('password') - is_continued = direct.getAttr('continue') == 'true' - - log.info('Direct invite: continued: %s, reason: %s, from: %s', - is_continued, reason, from_) - - app.nec.push_incoming_event( - GcInvitationReceived(None, - account=self._account, - from_=from_, - room_jid=room_jid, - reason=reason, - password=password, - is_continued=is_continued)) - raise nbxmpp.NodeProcessed - def invite(self, room, to, reason=None, continue_=False): if not app.account_is_connected(self._account): return + + type_ = InviteType.MEDIATED contact = app.contacts.get_contact_from_full_jid(self._account, to) if contact and contact.supports(nbxmpp.NS_CONFERENCE): - invite = self._build_direct_invite(room, to, reason, continue_) - else: - invite = self._build_mediated_invite(room, to, reason, continue_) - self._con.connection.send(invite) + type_ = InviteType.DIRECT - @staticmethod - def _build_direct_invite(room, to, reason, continue_): - message = nbxmpp.Message(to=to) - attrs = {'jid': room} - if reason: - attrs['reason'] = reason - if continue_: - attrs['continue'] = 'true' password = app.gc_passwords.get(room, None) - if password: - attrs['password'] = password - message.addChild(name='x', attrs=attrs, - namespace=nbxmpp.NS_CONFERENCE) - return message + self._con.connection.get_module('MUC').invite( + room, to, reason, password, continue_, type_) - @staticmethod - def _build_mediated_invite(room, to, reason, continue_): - message = nbxmpp.Message(to=room) - muc_user = message.addChild('x', namespace=nbxmpp.NS_MUC_USER) - invite = muc_user.addChild('invite', attrs={'to': to}) - if continue_: - invite.addChild(name='continue') - if reason: - invite.setTagData('reason', reason) - password = app.gc_passwords.get(room, None) - if password: - muc_user.setTagData('password', password) - return message - - def decline(self, room, to, reason=None): + def request_config(self, room_jid): if not app.account_is_connected(self._account): return - message = nbxmpp.Message(to=room) - muc_user = message.addChild('x', namespace=nbxmpp.NS_MUC_USER) - decline = muc_user.addChild('decline', attrs={'to': to}) - if reason: - decline.setTagData('reason', reason) - self._con.connection.send(message) - def request_voice(self, room): - if not app.account_is_connected(self._account): + self._con.connection.get_module('MUC').request_config( + room_jid, i18n.LANG, callback=self._config_received) + + def _config_received(self, result): + if result.is_error: return - message = nbxmpp.Message(to=room) - xdata = nbxmpp.DataForm(typ='submit') - xdata.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value=nbxmpp.NS_MUC + '#request')) - xdata.addChild(node=nbxmpp.DataField(name='muc#role', - value='participant', - typ='text-single')) - message.addChild(node=xdata) - self._con.connection.send(message) - @staticmethod - def _default_response(_con, stanza, **kwargs): - if not nbxmpp.isResultNode(stanza): - log.info('Error: %s', stanza.getError()) - - -class GcInvitationReceived(NetworkIncomingEvent): - name = 'gc-invitation-received' - - -class GcDeclineReceived(NetworkIncomingEvent): - name = 'gc-decline-received' - - -class MucOwnerReceivedEvent(NetworkIncomingEvent): - name = 'muc-owner-received' + app.nec.push_incoming_event(NetworkEvent( + 'muc-owner-received', + conn=self._con, + dataform=result.form, + jid=result.jid)) def get_instance(*args, **kwargs): diff --git a/gajim/dialogs.py b/gajim/dialogs.py index f0fe8bdb5..e76640677 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -1291,51 +1291,60 @@ class RosterItemExchangeWindow: class InvitationReceivedDialog: - def __init__(self, account, room_jid, contact_fjid, password=None, - comment=None, is_continued=False): - - self.room_jid = room_jid + def __init__(self, account, event): self.account = account - self.password = password - self.is_continued = is_continued - self.contact_fjid = contact_fjid + self.room_jid = str(event.muc) + self.from_ = str(event.from_) + self.password = event.password + self.is_continued = event.continued - jid = app.get_jid_without_resource(contact_fjid) + if event.from_.bareMatch(event.muc): + contact_text = event.from_.getResource() + else: + contact = app.contacts.get_first_contact_from_jid( + account, event.from_.getBare()) + if contact is None: + contact_text = str(event.from_) + else: + contact_text = contact.get_shown_name() pritext = _('''You are invited to a groupchat''') #Don't translate $Contact - if is_continued: + if self.is_continued: sectext = _('$Contact has invited you to join a discussion') else: sectext = _('$Contact has invited you to group chat %(room_jid)s')\ - % {'room_jid': room_jid} - contact = app.contacts.get_first_contact_from_jid(account, jid) - contact_text = contact and contact.name or jid - sectext = i18n.direction_mark + sectext.replace('$Contact', - contact_text) + % {'room_jid': self.room_jid} - if comment: # only if not None and not '' - comment = GLib.markup_escape_text(comment) + sectext = sectext.replace('$Contact', contact_text) + + if event.reason: + comment = GLib.markup_escape_text(event.reason) comment = _('Comment: %s') % comment sectext += '\n\n%s' % comment sectext += '\n\n' + _('Do you want to accept the invitation?') - def on_yes(checked, text): + def on_yes(_checked, _text): if self.is_continued: - app.interface.join_gc_room(self.account, self.room_jid, - app.nicks[self.account], self.password, - is_continued=True) + app.interface.join_gc_room(self.account, + self.room_jid, + app.nicks[self.account], + self.password, + is_continued=True) else: - app.interface.join_gc_minimal( - self.account, self.room_jid, password=self.password) + app.interface.join_gc_minimal(self.account, + self.room_jid, + password=self.password) def on_no(text): app.connections[account].get_module('MUC').decline( - self.room_jid, self.contact_fjid, text) + self.room_jid, self.from_, text) - dlg = YesNoDialog(pritext, sectext, - text_label=_('Reason (if you decline):'), on_response_yes=on_yes, - on_response_no=on_no) + dlg = YesNoDialog(pritext, + sectext, + text_label=_('Reason (if you decline):'), + on_response_yes=on_yes, + on_response_no=on_no) dlg.set_title(_('Groupchat Invitation')) class ProgressDialog: diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index bb15bcdd7..5f01c1bc9 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -33,6 +33,7 @@ import logging from enum import IntEnum, unique import nbxmpp +from nbxmpp.const import StatusCode from gi.repository import Gtk from gi.repository import Gdk @@ -1080,7 +1081,7 @@ class GroupchatControl(ChatControlBase): def _on_voice_approval(self, event): if event.account != self.account: return - if event.room_jid != self.room_jid: + if event.jid != self.room_jid: return SingleMessageWindow(self.account, self.room_jid, @@ -1091,7 +1092,7 @@ class GroupchatControl(ChatControlBase): def _on_captcha_challenge(self, event): if event.account != self.account: return - if event.room_jid != self.room_jid: + if event.jid != self.room_jid: return if self.form_widget: @@ -1419,11 +1420,11 @@ class GroupchatControl(ChatControlBase): return self.set_subject(event.subject) text = _('%(nick)s has set the subject to %(subject)s') % { - 'nick': event.resource, 'subject': event.subject} + 'nick': event.nickname, 'subject': event.subject} - if event.delayed: + if event.user_timestamp: date = time.strftime('%d-%m-%Y %H:%M:%S', - time.localtime(event.timestamp)) + time.localtime(event.user_timestamp)) text = '%s - %s' % (text, date) just_joined = self.join_time > time.time() - 10 @@ -1440,35 +1441,31 @@ class GroupchatControl(ChatControlBase): if event.account != self.account: return - if event.room_jid != self.room_jid: + if event.jid != self.room_jid: return changes = [] - if '100' in event.status_codes: - # Can be a presence (see chg_contact_status in groupchat_control.py) - changes.append(_('Any occupant is allowed to see your full JID')) - self.is_anonymous = False - if '102' in event.status_codes: + if StatusCode.SHOWING_UNAVAILABLE in event.status_codes: changes.append(_('Room now shows unavailable members')) - if '103' in event.status_codes: + if StatusCode.NOT_SHOWING_UNAVAILABLE in event.status_codes: changes.append(_('Room now does not show unavailable members')) - if '104' in event.status_codes: + if StatusCode.CONFIG_NON_PRIVACY_RELATED in event.status_codes: changes.append(_('A setting not related to privacy has been ' 'changed')) app.connections[self.account].get_module('Discovery').disco_muc( self.room_jid, self.update_actions, update=True) - if '170' in event.status_codes: + if StatusCode.CONFIG_ROOM_LOGGING in event.status_codes: # Can be a presence (see chg_contact_status in groupchat_control.py) changes.append(_('Room logging is now enabled')) - if '171' in event.status_codes: + if StatusCode.CONFIG_NO_ROOM_LOGGING in event.status_codes: changes.append(_('Room logging is now disabled')) - if '172' in event.status_codes: + if StatusCode.CONFIG_NON_ANONYMOUS in event.status_codes: changes.append(_('Room is now non-anonymous')) self.is_anonymous = False - if '173' in event.status_codes: + if StatusCode.CONFIG_SEMI_ANONYMOUS in event.status_codes: changes.append(_('Room is now semi-anonymous')) self.is_anonymous = True - if '174' in event.status_codes: + if StatusCode.CONFIG_FULL_ANONYMOUS in event.status_codes: changes.append(_('Room is now fully anonymous')) self.is_anonymous = True diff --git a/gajim/gtk/groupchat_config.py b/gajim/gtk/groupchat_config.py index e73780efa..91ce0fb8f 100644 --- a/gajim/gtk/groupchat_config.py +++ b/gajim/gtk/groupchat_config.py @@ -62,8 +62,7 @@ class GroupchatConfig(Gtk.ApplicationWindow): con.get_module('MUC').get_affiliation( self.jid, affiliation, - self._on_affiliations_received, - self._on_affiliations_error) + callback=self._on_affiliations_received) if form is not None: self._ui.stack.set_visible_child_name('config') @@ -340,29 +339,28 @@ class GroupchatConfig(Gtk.ApplicationWindow): con = app.connections[self.account] con.get_module('MUC').set_affiliation(self.jid, diff_dict) - def _on_affiliations_error(self, affiliation, error): - log.info('Error while requesting %s affiliations: %s', - affiliation, error) + def _on_affiliations_received(self, result): + if result.is_error: + log.info('Error while requesting %s affiliations: %s', + result.affiliation, result.error) + return - def _on_affiliations_received(self, _account, _room_jid, - affiliation, users): - - if affiliation == 'outcast': + if result.affiliation == 'outcast': self._ui.stack.get_child_by_name('outcast').show() - for jid, attrs in users.items(): - affiliation_edit, jid_edit = self._allowed_to_edit(affiliation) - if affiliation == 'outcast': + for jid, attrs in result.users.items(): + affiliation_edit, jid_edit = self._allowed_to_edit(result.affiliation) + if result.affiliation == 'outcast': reason = attrs.get('reason') self._ui.outcast_store.append( [jid, reason, None, - affiliation, + result.affiliation, None, affiliation_edit, jid_edit]) - self._affiliations[jid] = {'affiliation': affiliation, + self._affiliations[jid] = {'affiliation': result.affiliation, 'reason': reason} else: nick = attrs.get('nick') @@ -371,11 +369,11 @@ class GroupchatConfig(Gtk.ApplicationWindow): [jid, nick, role, - affiliation, - _(affiliation.capitalize()), + result.affiliation, + _(result.affiliation.capitalize()), affiliation_edit, jid_edit]) - self._affiliations[jid] = {'affiliation': affiliation, + self._affiliations[jid] = {'affiliation': result.affiliation, 'nick': nick} if role is not None: self._ui.role_column.set_visible(True) diff --git a/gajim/gtk/notification.py b/gajim/gtk/notification.py index 59e67d53c..9ea10213a 100644 --- a/gajim/gtk/notification.py +++ b/gajim/gtk/notification.py @@ -109,7 +109,7 @@ class Notification: def _on_event_removed(self, event_list): for event in event_list: if event.type_ == 'gc-invitation': - self._withdraw('gc-invitation', event.account, event.room_jid) + self._withdraw('gc-invitation', event.account, event.muc) if event.type_ in ('normal', 'printed_chat', 'chat', 'printed_pm', 'pm', 'printed_marked_gc_msg', 'printed_gc_msg'): diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index aa441f09e..f43def857 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -609,39 +609,43 @@ class Interface: else: GroupchatConfig(account, obj.jid, 'owner', obj.dataform) - def handle_event_gc_decline(self, obj): - gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, obj.account) + def handle_event_gc_decline(self, event): + gc_control = self.msg_win_mgr.get_gc_control(str(event.muc), + event.account) if gc_control: - if obj.reason: + if event.reason: gc_control.print_conversation( _('%(jid)s declined the invitation: %(reason)s') % { - 'jid': obj.from_, 'reason': obj.reason}, + 'jid': event.from_, 'reason': event.reason}, graphics=False) else: gc_control.print_conversation( _('%(jid)s declined the invitation') % { - 'jid': obj.from_}, graphics=False) + 'jid': event.from_}, graphics=False) - def handle_event_gc_invitation(self, obj): - if helpers.allow_popup_window(obj.account) or not self.systray_enabled: - dialogs.InvitationReceivedDialog( - obj.account, obj.room_jid, - str(obj.from_), obj.password, obj.reason, - is_continued=obj.is_continued) + def handle_event_gc_invitation(self, event): + if helpers.allow_popup_window(event.account) or not self.systray_enabled: + dialogs.InvitationReceivedDialog(event.account, event) return - event = events.GcInvitationtEvent( - obj.room_jid, obj.reason, - obj.password, obj.is_continued, str(obj.from_)) - self.add_event(obj.account, str(obj.from_), event) + from_ = str(event.from_) + muc = str(event.muc) - if helpers.allow_showing_notification(obj.account): + event_ = events.GcInvitationtEvent(event) + self.add_event(event.account, from_, event_) + + if helpers.allow_showing_notification(event.account): event_type = _('Groupchat Invitation') - text = _('You are invited to {room} by {user}').format( - room=obj.room_jid, user=str(obj.from_)) - app.notification.popup( - event_type, str(obj.from_), obj.account, 'gc-invitation', - 'gajim-gc_invitation', event_type, text, room_jid=obj.room_jid) + text = _('You are invited to {room} by {user}').format(room=muc, + user=from_) + app.notification.popup(event_type, + from_, + event.account, + 'gc-invitation', + 'gajim-gc_invitation', + event_type, + text, + room_jid=muc) def forget_gpg_passphrase(self, keyid): if keyid in self.gpg_passphrase: @@ -1523,7 +1527,9 @@ class Interface: if app.contacts.get_contact_with_highest_priority(account, jid): self.roster.draw_contact(jid, account) else: - self.roster.add_to_not_in_the_roster(account, jid) + groupchat = event.type_ == 'gc-invitation' + self.roster.add_to_not_in_the_roster( + account, jid, groupchat=groupchat) # Select the big brother contact in roster, it's visible because it has # events. @@ -1621,8 +1627,7 @@ class Interface: event = app.events.get_first_event(account, jid, type_) if event is None: return - dialogs.InvitationReceivedDialog(account, event.room_jid, jid, - event.password, event.reason, event.is_continued) + dialogs.InvitationReceivedDialog(account, event) app.events.remove_events(account, jid, event) self.roster.draw_contact(jid, account) elif type_ == 'subscription_request': diff --git a/gajim/roster_window.py b/gajim/roster_window.py index 77120cd5d..eb8d6c284 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -1032,14 +1032,16 @@ class RosterWindow: self.draw_group(group, account) # FIXME: integrate into add_contact() - def add_to_not_in_the_roster(self, account, jid, nick='', resource=''): + def add_to_not_in_the_roster(self, account, jid, nick='', resource='', + groupchat=False): keyID = '' attached_keys = app.config.get_per('accounts', account, 'attached_gpg_keys').split() if jid in attached_keys: keyID = attached_keys[attached_keys.index(jid) + 1] - contact = app.contacts.create_not_in_roster_contact(jid=jid, - account=account, resource=resource, name=nick, keyID=keyID) + contact = app.contacts.create_not_in_roster_contact( + jid=jid, account=account, resource=resource, name=nick, + keyID=keyID, groupchat=groupchat) app.contacts.add_contact(account, contact) self.add_contact(contact.jid, account) return contact @@ -2002,9 +2004,7 @@ class RosterWindow: return True if event.type_ == 'gc-invitation': - dialogs.InvitationReceivedDialog(account, event.room_jid, - event.jid_from, event.password, event.reason, - is_continued=event.is_continued) + dialogs.InvitationReceivedDialog(account, event) app.events.remove_events(account, jid, event) return True @@ -2692,12 +2692,8 @@ class RosterWindow: app.log('avatar').debug('Draw roster avatar: %s', obj.jid) self.draw_avatar(obj.jid, obj.account) - def _nec_gc_subject_received(self, obj): - contact = app.contacts.get_contact_with_highest_priority( - obj.account, obj.jid) - if contact: - contact.status = obj.subject - self.draw_contact(obj.jid, obj.account) + def _nec_gc_subject_received(self, event): + self.draw_contact(event.jid, event.account) def _nec_metacontacts_received(self, obj): self.redraw_metacontacts(obj.conn.name)