diff --git a/gajim/command_system/implementation/standard.py b/gajim/command_system/implementation/standard.py index 42a4966ec..ba6a6cb85 100644 --- a/gajim/command_system/implementation/standard.py +++ b/gajim/command_system/implementation/standard.py @@ -291,7 +291,7 @@ class StandardGroupChatCommands(CommandContainer): @command(raw=True, empty=True) @doc(_("Invite a user to a room for a reason")) def invite(self, jid, reason): - self.connection.send_invite(self.room_jid, jid, reason) + self.connection.get_module('MUC').invite(self.room_jid, jid, reason) return _("Invited %(jid)s to %(room_jid)s") % {'jid': jid, 'room_jid': self.room_jid} diff --git a/gajim/common/connection.py b/gajim/common/connection.py index 7496d0106..6ac8ba522 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -2379,68 +2379,6 @@ class Connection(CommonConnection, ConnectionHandlers): else: _on_unregister_account_connect(self.connection) - def send_invite(self, room, to, reason='', continue_tag=False): - """ - Send invitation - """ - if not app.account_is_connected(self.name): - return - contact = app.contacts.get_contact_from_full_jid(self.name, to) - if contact and contact.supports(nbxmpp.NS_CONFERENCE): - # send direct invite - message=nbxmpp.Message(to=to) - attrs = {'jid': room} - if reason: - attrs['reason'] = reason - if continue_tag: - attrs['continue'] = 'true' - password = app.gc_passwords.get(room, '') - if password: - attrs['password'] = password - c = message.addChild(name='x', attrs=attrs, - namespace=nbxmpp.NS_CONFERENCE) - self.connection.send(message) - return - message=nbxmpp.Message(to=room) - c = message.addChild(name='x', namespace=nbxmpp.NS_MUC_USER) - c = c.addChild(name='invite', attrs={'to': to}) - if continue_tag: - c.addChild(name='continue') - if reason != '': - c.setTagData('reason', reason) - self.connection.send(message) - - def decline_invitation(self, room, to, reason=''): - """ - decline a groupchat invitation - """ - if not app.account_is_connected(self.name): - return - message=nbxmpp.Message(to=room) - c = message.addChild(name='x', namespace=nbxmpp.NS_MUC_USER) - c = c.addChild(name='decline', attrs={'to': to}) - if reason != '': - c.setTagData('reason', reason) - self.connection.send(message) - - def request_voice(self, room): - """ - Request voice in a moderated room - """ - if not app.account_is_connected(self.name): - return - message = nbxmpp.Message(to=room) - - x = nbxmpp.DataForm(typ='submit') - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value=nbxmpp.NS_MUC + '#request')) - x.addChild(node=nbxmpp.DataField(name='muc#role', value='participant', - typ='text-single')) - - message.addChild(node=x) - - self.connection.send(message) - def _reconnect_alarm(self): if not app.config.get_per('accounts', self.name, 'active'): # Account may have been disabled diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py index 1566345de..5a0fd44be 100644 --- a/gajim/common/connection_handlers.py +++ b/gajim/common/connection_handlers.py @@ -297,7 +297,8 @@ class ConnectionHandlersBase: nbxmpp.NS_PUBSUB_EVENT, nbxmpp.NS_ROSTERX, nbxmpp.NS_MAM_1, - nbxmpp.NS_MAM_2]) + nbxmpp.NS_MAM_2, + nbxmpp.NS_CONFERENCE]) app.ged.register_event_handler('iq-error-received', ged.CORE, self._nec_iq_error_received) @@ -950,6 +951,12 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco, # but nbxmpp executes less common handlers last if self._message_namespaces & set(stanza.getProperties()): return + + muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) + if muc_user is not None: + if muc_user.getChildren(): + # Not a PM, handled by MUC module + return log.debug('MessageCB') app.nec.push_incoming_event(NetworkEvent('raw-message-received', diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index 0c2ea00a0..ad44d5a7f 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -697,32 +697,6 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): return self.jid = app.get_jid_without_resource(self.fjid) - # Mediated invitation? - muc_user = self.stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) - if muc_user: - if muc_user.getTag('decline'): - app.nec.push_incoming_event( - GcDeclineReceivedEvent( - None, conn=self.conn, - room_jid=self.fjid, stanza=muc_user)) - return - if muc_user.getTag('invite'): - app.nec.push_incoming_event( - GcInvitationReceivedEvent( - None, conn=self.conn, jid_from=self.fjid, - mediated=True, stanza=muc_user)) - return - else: - # Direct invitation? - direct = self.stanza.getTag( - 'x', namespace=nbxmpp.NS_CONFERENCE) - if direct: - app.nec.push_incoming_event( - GcInvitationReceivedEvent( - None, conn=self.conn, jid_from=self.fjid, - mediated=False, stanza=direct)) - return - self.thread_id = self.stanza.getThread() self.mtype = self.stanza.getType() if not self.mtype or self.mtype not in ('chat', 'groupchat', 'error'): @@ -840,72 +814,6 @@ class ZeroconfMessageReceivedEvent(MessageReceivedEvent): stanza=self.stanza) return super(ZeroconfMessageReceivedEvent, self).generate() -class GcInvitationReceivedEvent(nec.NetworkIncomingEvent): - name = 'gc-invitation-received' - base_network_events = [] - - def generate(self): - account = self.conn.name - if not self.mediated: - # direct invitation - try: - self.room_jid = helpers.parse_jid(self.stanza.getAttr('jid')) - except helpers.InvalidFormat: - log.warning('Invalid JID: %s, ignoring it', - self.stanza.getAttr('jid')) - return - self.reason = self.stanza.getAttr('reason') - self.password = self.stanza.getAttr('password') - self.is_continued = False - self.is_continued = self.stanza.getAttr('continue') == 'true' - else: - self.invite = self.stanza.getTag('invite') - self.room_jid = self.jid_from - try: - self.jid_from = helpers.parse_jid(self.invite.getAttr('from')) - except helpers.InvalidFormat: - log.warning('Invalid JID: %s, ignoring it', - self.invite.getAttr('from')) - return - - self.reason = self.invite.getTagData('reason') - self.password = self.stanza.getTagData('password') - self.is_continued = self.stanza.getTag('continue') is not None - - if self.room_jid in app.gc_connected[account] and \ - app.gc_connected[account][self.room_jid]: - # We are already in groupchat. Ignore invitation - return - jid = app.get_jid_without_resource(self.jid_from) - - ignore = app.config.get_per( - 'accounts', account, 'ignore_unknown_contacts') - if ignore and not app.contacts.get_contacts(account, jid): - return - - return True - -class GcDeclineReceivedEvent(nec.NetworkIncomingEvent): - name = 'gc-decline-received' - base_network_events = [] - - def generate(self): - account = self.conn.name - decline = self.stanza.getTag('decline') - try: - self.jid_from = helpers.parse_jid(decline.getAttr('from')) - except helpers.InvalidFormat: - log.warning('Invalid JID: %s, ignoring it', - decline.getAttr('from')) - return - jid = app.get_jid_without_resource(self.jid_from) - ignore = app.config.get_per( - 'accounts', account, 'ignore_unknown_contacts') - if ignore and not app.contacts.get_contacts(account, jid): - return - self.reason = decline.getTagData('reason') - - return True class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): name = 'decrypted-message-received' diff --git a/gajim/common/modules/muc.py b/gajim/common/modules/muc.py new file mode 100644 index 000000000..436e1d83a --- /dev/null +++ b/gajim/common/modules/muc.py @@ -0,0 +1,205 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +# XEP-0045: Multi-User Chat +# XEP-0249: Direct MUC Invitations + +import logging + +import nbxmpp + +from gajim.common import app +from gajim.common import helpers +from gajim.common.nec import NetworkIncomingEvent + +log = logging.getLogger('gajim.c.m.muc') + + +class MUC: + def __init__(self, con): + self._con = con + self._account = con.name + + self.handlers = [ + ('message', self._mediated_invite, '', nbxmpp.NS_MUC_USER), + ('message', self._direct_invite, '', nbxmpp.NS_CONFERENCE), + ] + + def _mediated_invite(self, con, stanza): + muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) + if muc_user is None: + 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)) + raise nbxmpp.NodeProcessed + + invite = muc_user.getTag('invite') + if invite is not None: + + room_jid = stanza.getFrom().getStripped() + from_ = self._get_from(room_jid, invite) + + 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]: + # 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)) + 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 + 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) + + def _build_direct_invite(self, 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 = message.addChild(name='x', attrs=attrs, + namespace=nbxmpp.NS_CONFERENCE) + return message + + def _build_mediated_invite(self, 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): + 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): + return + message = nbxmpp.Message(to=room) + x = nbxmpp.DataForm(typ='submit') + x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', + value=nbxmpp.NS_MUC + '#request')) + x.addChild(node=nbxmpp.DataField(name='muc#role', + value='participant', + typ='text-single')) + message.addChild(node=x) + self._con.connection.send(message) + + +class GcInvitationReceived(NetworkIncomingEvent): + name = 'gc-invitation-received' + + +class GcDeclineReceived(NetworkIncomingEvent): + name = 'gc-decline-received' + + +def get_instance(*args, **kwargs): + return MUC(*args, **kwargs), 'MUC' diff --git a/gajim/dialogs.py b/gajim/dialogs.py index 4403bdda9..ff116f7eb 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -4437,8 +4437,8 @@ class InvitationReceivedDialog: self.account, self.room_jid, password=self.password) def on_no(text): - app.connections[account].decline_invitation(self.room_jid, - self.contact_fjid, text) + app.connections[account].get_module('MUC').decline( + self.room_jid, self.contact_fjid, text) dlg = YesNoDialog(pritext, sectext, text_label=_('Reason (if you decline):'), on_response_yes=on_yes, diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index ad4fdef36..61df955c7 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -716,7 +716,8 @@ class GroupchatControl(ChatControlBase): """ Request voice in the current room """ - app.connections[self.account].request_voice(self.room_jid) + con = app.connections[self.account] + con.get_module('MUC').request_voice(self.room_jid) def _on_minimize(self, action, param): """ @@ -1797,7 +1798,7 @@ class GroupchatControl(ChatControlBase): # We just need to invite contacts for jid in app.automatic_rooms[self.account][ self.room_jid]['invities']: - obj.conn.send_invite(self.room_jid, jid) + obj.conn.get_module('MUC').invite(self.room_jid, jid) self.print_conversation(_('%(jid)s has been ' 'invited in this room') % {'jid': jid}, graphics=False) @@ -2405,7 +2406,8 @@ class GroupchatControl(ChatControlBase): return contact_jid = data - app.connections[self.account].send_invite(self.room_jid, contact_jid) + con = app.connections[self.account] + con.get_module('MUC').invite(self.room_jid, contact_jid) self.print_conversation(_('%(jid)s has been invited in this room') % {'jid': contact_jid}, graphics=False) diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index 6045b8dd3..7896307ed 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -608,8 +608,8 @@ class Interface: continue_tag = True if 'invities' in app.automatic_rooms[account][obj.jid]: for jid in app.automatic_rooms[account][obj.jid]['invities']: - obj.conn.send_invite(obj.jid, jid, - continue_tag=continue_tag) + obj.conn.get_module('MUC').invite( + obj.jid, jid, continue_=continue_tag) gc_control = self.msg_win_mgr.get_gc_control(obj.jid, account) if gc_control: @@ -629,35 +629,35 @@ class Interface: affiliation_list_received(obj.users_dict) def handle_event_gc_decline(self, obj): - account = obj.conn.name - gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, account) + gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, obj.account) if gc_control: if obj.reason: gc_control.print_conversation( _('%(jid)s declined the invitation: %(reason)s') % { - 'jid': obj.jid_from, 'reason': obj.reason}, graphics=False) + 'jid': obj.from_, 'reason': obj.reason}, + graphics=False) else: gc_control.print_conversation( _('%(jid)s declined the invitation') % { - 'jid': obj.jid_from}, graphics=False) + 'jid': obj.from_}, graphics=False) def handle_event_gc_invitation(self, obj): - #('GC_INVITATION', (room_jid, jid_from, reason, password, is_continued)) - account = obj.conn.name - if helpers.allow_popup_window(account) or not self.systray_enabled: - dialogs.InvitationReceivedDialog(account, obj.room_jid, - obj.jid_from, obj.password, obj.reason, + 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) return - event = events.GcInvitationtEvent(obj.room_jid, obj.reason, - obj.password, obj.is_continued, obj.jid_from) - self.add_event(account, obj.jid_from, event) + 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) - if helpers.allow_showing_notification(account): + if helpers.allow_showing_notification(obj.account): event_type = _('Groupchat Invitation') app.notification.popup( - event_type, obj.jid_from, account, 'gc-invitation', + event_type, str(obj.from_), obj.account, 'gc-invitation', 'gajim-gc_invitation', event_type, obj.room_jid) def forget_gpg_passphrase(self, keyid): diff --git a/gajim/roster_window.py b/gajim/roster_window.py index 4a3cef54d..58c0aa6f7 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -3091,7 +3091,8 @@ class RosterWindow: contact_jid = contact.jid if resource: # we MUST have one contact only in list_ contact_jid += '/' + resource - app.connections[room_account].send_invite(room_jid, contact_jid) + con = app.connections[room_account] + con.get_module('MUC').invite(room_jid, contact_jid) gc_control = app.interface.msg_win_mgr.get_gc_control(room_jid, room_account) if gc_control: