858 lines
32 KiB
Python
858 lines
32 KiB
Python
|
# 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 <http://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
# XEP-0045: Multi-User Chat
|
|||
|
# XEP-0249: Direct MUC Invitations
|
|||
|
|
|||
|
import logging
|
|||
|
|
|||
|
import nbxmpp
|
|||
|
from nbxmpp.namespaces import Namespace
|
|||
|
from nbxmpp.const import InviteType
|
|||
|
from nbxmpp.const import PresenceType
|
|||
|
from nbxmpp.const import StatusCode
|
|||
|
from nbxmpp.structs import StanzaHandler
|
|||
|
from nbxmpp.errors import StanzaError
|
|||
|
|
|||
|
from gi.repository import GLib
|
|||
|
|
|||
|
from gajim.common import app
|
|||
|
from gajim.common import helpers
|
|||
|
from gajim.common import ged
|
|||
|
from gajim.common.const import KindConstant
|
|||
|
from gajim.common.const import MUCJoinedState
|
|||
|
from gajim.common.helpers import AdditionalDataDict
|
|||
|
from gajim.common.helpers import get_default_muc_config
|
|||
|
from gajim.common.helpers import to_user_string
|
|||
|
from gajim.common.helpers import event_filter
|
|||
|
from gajim.common.nec import NetworkEvent
|
|||
|
from gajim.common.modules.bits_of_binary import store_bob_data
|
|||
|
from gajim.common.modules.base import BaseModule
|
|||
|
|
|||
|
|
|||
|
log = logging.getLogger('gajim.c.m.muc')
|
|||
|
|
|||
|
|
|||
|
class MUC(BaseModule):
|
|||
|
|
|||
|
_nbxmpp_extends = 'MUC'
|
|||
|
_nbxmpp_methods = [
|
|||
|
'get_affiliation',
|
|||
|
'set_role',
|
|||
|
'set_affiliation',
|
|||
|
'set_config',
|
|||
|
'set_subject',
|
|||
|
'cancel_config',
|
|||
|
'send_captcha',
|
|||
|
'cancel_captcha',
|
|||
|
'decline',
|
|||
|
'invite',
|
|||
|
'request_config',
|
|||
|
'request_voice',
|
|||
|
'approve_voice_request',
|
|||
|
'destroy',
|
|||
|
'request_disco_info'
|
|||
|
]
|
|||
|
|
|||
|
def __init__(self, con):
|
|||
|
BaseModule.__init__(self, con)
|
|||
|
|
|||
|
self.handlers = [
|
|||
|
StanzaHandler(name='presence',
|
|||
|
callback=self._on_muc_user_presence,
|
|||
|
ns=Namespace.MUC_USER,
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='presence',
|
|||
|
callback=self._on_error_presence,
|
|||
|
typ='error',
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='message',
|
|||
|
callback=self._on_subject_change,
|
|||
|
typ='groupchat',
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='message',
|
|||
|
callback=self._on_config_change,
|
|||
|
ns=Namespace.MUC_USER,
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='message',
|
|||
|
callback=self._on_invite_or_decline,
|
|||
|
typ='normal',
|
|||
|
ns=Namespace.MUC_USER,
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='message',
|
|||
|
callback=self._on_invite_or_decline,
|
|||
|
ns=Namespace.CONFERENCE,
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='message',
|
|||
|
callback=self._on_captcha_challenge,
|
|||
|
ns=Namespace.CAPTCHA,
|
|||
|
priority=49),
|
|||
|
StanzaHandler(name='message',
|
|||
|
callback=self._on_voice_request,
|
|||
|
ns=Namespace.DATA,
|
|||
|
priority=49)
|
|||
|
]
|
|||
|
|
|||
|
self.register_events([
|
|||
|
('account-disconnected', ged.CORE, self._on_account_disconnected),
|
|||
|
])
|
|||
|
|
|||
|
self._manager = MUCManager(self._log)
|
|||
|
self._rejoin_muc = set()
|
|||
|
self._join_timeouts = {}
|
|||
|
self._rejoin_timeouts = {}
|
|||
|
self._muc_service_jid = None
|
|||
|
|
|||
|
@property
|
|||
|
def supported(self):
|
|||
|
return self._muc_service_jid is not None
|
|||
|
|
|||
|
@property
|
|||
|
def service_jid(self):
|
|||
|
return self._muc_service_jid
|
|||
|
|
|||
|
def get_manager(self):
|
|||
|
return self._manager
|
|||
|
|
|||
|
def pass_disco(self, info):
|
|||
|
for identity in info.identities:
|
|||
|
if identity.category != 'conference':
|
|||
|
continue
|
|||
|
if identity.type != 'text':
|
|||
|
continue
|
|||
|
if Namespace.MUC in info.features:
|
|||
|
self._log.info('Discovered MUC: %s', info.jid)
|
|||
|
self._muc_service_jid = info.jid
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
def join(self, muc_data):
|
|||
|
if not app.account_is_available(self._account):
|
|||
|
return
|
|||
|
|
|||
|
self._manager.add(muc_data)
|
|||
|
|
|||
|
disco_info = app.storage.cache.get_last_disco_info(muc_data.jid,
|
|||
|
max_age=60)
|
|||
|
if disco_info is None:
|
|||
|
self._con.get_module('Discovery').disco_muc(
|
|||
|
muc_data.jid,
|
|||
|
callback=self._on_disco_result)
|
|||
|
else:
|
|||
|
self._join(muc_data)
|
|||
|
|
|||
|
def create(self, muc_data):
|
|||
|
if not app.account_is_available(self._account):
|
|||
|
return
|
|||
|
|
|||
|
self._manager.add(muc_data)
|
|||
|
self._create(muc_data)
|
|||
|
|
|||
|
def _on_disco_result(self, task):
|
|||
|
try:
|
|||
|
result = task.finish()
|
|||
|
except StanzaError as error:
|
|||
|
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-join-failed',
|
|||
|
account=self._account,
|
|||
|
room_jid=error.jid.bare,
|
|||
|
error=error))
|
|||
|
return
|
|||
|
|
|||
|
muc_data = self._manager.get(result.info.jid)
|
|||
|
if muc_data is None:
|
|||
|
self._log.warning('MUC Data not found, join aborted')
|
|||
|
return
|
|||
|
self._join(muc_data)
|
|||
|
|
|||
|
def _join(self, muc_data):
|
|||
|
presence = self._con.get_module('Presence').get_presence(
|
|||
|
muc_data.occupant_jid,
|
|||
|
show=self._con.status,
|
|||
|
status=self._con.status_message)
|
|||
|
|
|||
|
muc_x = presence.setTag(Namespace.MUC + ' x')
|
|||
|
muc_x.setTag('history', {'maxchars': '0'})
|
|||
|
|
|||
|
if muc_data.password is not None:
|
|||
|
muc_x.setTagData('password', muc_data.password)
|
|||
|
|
|||
|
self._log.info('Join MUC: %s', muc_data.jid)
|
|||
|
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINING)
|
|||
|
self._con.connection.send(presence)
|
|||
|
|
|||
|
def _rejoin(self, room_jid):
|
|||
|
muc_data = self._manager.get(room_jid)
|
|||
|
if muc_data.state == MUCJoinedState.NOT_JOINED:
|
|||
|
self._log.info('Rejoin %s', room_jid)
|
|||
|
self._join(muc_data)
|
|||
|
return True
|
|||
|
|
|||
|
def _create(self, muc_data):
|
|||
|
presence = self._con.get_module('Presence').get_presence(
|
|||
|
muc_data.occupant_jid,
|
|||
|
show=self._con.status,
|
|||
|
status=self._con.status_message)
|
|||
|
|
|||
|
presence.setTag(Namespace.MUC + ' x')
|
|||
|
|
|||
|
self._log.info('Create MUC: %s', muc_data.jid)
|
|||
|
self._manager.set_state(muc_data.jid, MUCJoinedState.CREATING)
|
|||
|
self._con.connection.send(presence)
|
|||
|
|
|||
|
def leave(self, room_jid, reason=None):
|
|||
|
self._log.info('Leave MUC: %s', room_jid)
|
|||
|
self._remove_join_timeout(room_jid)
|
|||
|
self._remove_rejoin_timeout(room_jid)
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
muc_data = self._manager.get(room_jid)
|
|||
|
self._con.get_module('Presence').send_presence(
|
|||
|
muc_data.occupant_jid,
|
|||
|
typ='unavailable',
|
|||
|
status=reason,
|
|||
|
caps=False)
|
|||
|
# We leave a group chat, disable bookmark autojoin
|
|||
|
self._con.get_module('Bookmarks').modify(room_jid, autojoin=False)
|
|||
|
|
|||
|
def configure_room(self, room_jid):
|
|||
|
self._nbxmpp('MUC').request_config(room_jid,
|
|||
|
callback=self._on_room_config)
|
|||
|
|
|||
|
def _on_room_config(self, task):
|
|||
|
try:
|
|||
|
result = task.finish()
|
|||
|
except StanzaError as error:
|
|||
|
self._log.info(error)
|
|||
|
app.nec.push_incoming_event(NetworkEvent(
|
|||
|
'muc-configuration-failed',
|
|||
|
account=self._account,
|
|||
|
room_jid=error.jid,
|
|||
|
error=error))
|
|||
|
return
|
|||
|
|
|||
|
self._log.info('Configure room: %s', result.jid)
|
|||
|
|
|||
|
muc_data = self._manager.get(result.jid)
|
|||
|
self._apply_config(result.form, muc_data.config)
|
|||
|
self.set_config(result.jid,
|
|||
|
result.form,
|
|||
|
callback=self._on_config_result)
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def _apply_config(form, config=None):
|
|||
|
default_config = get_default_muc_config()
|
|||
|
if config is not None:
|
|||
|
default_config.update(config)
|
|||
|
for var, value in default_config.items():
|
|||
|
try:
|
|||
|
field = form[var]
|
|||
|
except KeyError:
|
|||
|
pass
|
|||
|
else:
|
|||
|
field.value = value
|
|||
|
|
|||
|
def _on_config_result(self, task):
|
|||
|
try:
|
|||
|
result = task.finish()
|
|||
|
except StanzaError as error:
|
|||
|
self._log.info(error)
|
|||
|
app.nec.push_incoming_event(NetworkEvent(
|
|||
|
'muc-configuration-failed',
|
|||
|
account=self._account,
|
|||
|
room_jid=error.jid,
|
|||
|
error=error))
|
|||
|
return
|
|||
|
|
|||
|
self._con.get_module('Discovery').disco_muc(
|
|||
|
result.jid, callback=self._on_disco_result_after_config)
|
|||
|
|
|||
|
# If this is an automatic room creation
|
|||
|
try:
|
|||
|
invites = app.automatic_rooms[self._account][result.jid]['invities']
|
|||
|
except KeyError:
|
|||
|
return
|
|||
|
|
|||
|
user_list = {}
|
|||
|
for jid in invites:
|
|||
|
user_list[jid] = {'affiliation': 'member'}
|
|||
|
self.set_affiliation(result.jid, user_list)
|
|||
|
|
|||
|
for jid in invites:
|
|||
|
self.invite(result.jid, jid)
|
|||
|
|
|||
|
def _on_disco_result_after_config(self, task):
|
|||
|
try:
|
|||
|
result = task.finish()
|
|||
|
except StanzaError as error:
|
|||
|
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
|
|||
|
return
|
|||
|
|
|||
|
jid = result.info.jid
|
|||
|
muc_data = self._manager.get(jid)
|
|||
|
self._room_join_complete(muc_data)
|
|||
|
|
|||
|
self._log.info('Configuration finished: %s', jid)
|
|||
|
app.nec.push_incoming_event(NetworkEvent(
|
|||
|
'muc-configuration-finished',
|
|||
|
account=self._account,
|
|||
|
room_jid=jid))
|
|||
|
|
|||
|
def update_presence(self):
|
|||
|
mucs = self._manager.get_mucs_with_state([MUCJoinedState.JOINED,
|
|||
|
MUCJoinedState.JOINING])
|
|||
|
|
|||
|
status, message, idle = self._con.get_presence_state()
|
|||
|
for muc_data in mucs:
|
|||
|
self._con.get_module('Presence').send_presence(
|
|||
|
muc_data.occupant_jid,
|
|||
|
show=status,
|
|||
|
status=message,
|
|||
|
idle_time=idle)
|
|||
|
|
|||
|
def change_nick(self, room_jid, new_nick):
|
|||
|
status, message, _idle = self._con.get_presence_state()
|
|||
|
self._con.get_module('Presence').send_presence(
|
|||
|
'%s/%s' % (room_jid, new_nick),
|
|||
|
show=status,
|
|||
|
status=message)
|
|||
|
|
|||
|
def _on_error_presence(self, _con, _stanza, properties):
|
|||
|
room_jid = properties.jid.bare
|
|||
|
muc_data = self._manager.get(room_jid)
|
|||
|
if muc_data is None:
|
|||
|
return
|
|||
|
|
|||
|
if muc_data.state == MUCJoinedState.JOINING:
|
|||
|
if properties.error.condition == 'conflict':
|
|||
|
self._remove_rejoin_timeout(room_jid)
|
|||
|
muc_data.nick += '_'
|
|||
|
self._log.info('Nickname conflict: %s change to %s',
|
|||
|
muc_data.jid, muc_data.nick)
|
|||
|
self._join(muc_data)
|
|||
|
elif properties.error.condition == 'not-authorized':
|
|||
|
self._remove_rejoin_timeout(room_jid)
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
self._raise_muc_event('muc-password-required', properties)
|
|||
|
else:
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
if room_jid not in self._rejoin_muc:
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-join-failed',
|
|||
|
account=self._account,
|
|||
|
room_jid=room_jid,
|
|||
|
error=properties.error))
|
|||
|
|
|||
|
elif muc_data.state == MUCJoinedState.CREATING:
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-creation-failed',
|
|||
|
account=self._account,
|
|||
|
room_jid=room_jid,
|
|||
|
error=properties.error))
|
|||
|
|
|||
|
elif muc_data.state == MUCJoinedState.CAPTCHA_REQUEST:
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-captcha-error',
|
|||
|
account=self._account,
|
|||
|
room_jid=room_jid,
|
|||
|
error_text=to_user_string(properties.error)))
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
|
|||
|
elif muc_data.state == MUCJoinedState.CAPTCHA_FAILED:
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
|
|||
|
else:
|
|||
|
self._raise_muc_event('muc-presence-error', properties)
|
|||
|
|
|||
|
def _on_muc_user_presence(self, _con, stanza, properties):
|
|||
|
if properties.type == PresenceType.ERROR:
|
|||
|
return
|
|||
|
|
|||
|
room_jid = str(properties.muc_jid)
|
|||
|
if room_jid not in self._manager:
|
|||
|
self._log.warning('Presence from unknown MUC')
|
|||
|
self._log.warning(stanza)
|
|||
|
return
|
|||
|
|
|||
|
muc_data = self._manager.get(room_jid)
|
|||
|
|
|||
|
if properties.is_muc_destroyed:
|
|||
|
for contact in app.contacts.get_gc_contact_list(
|
|||
|
self._account, room_jid):
|
|||
|
contact.presence = PresenceType.UNAVAILABLE
|
|||
|
self._log.info('MUC destroyed: %s', room_jid)
|
|||
|
self._remove_join_timeout(room_jid)
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
self._raise_muc_event('muc-destroyed', properties)
|
|||
|
return
|
|||
|
|
|||
|
contact = app.contacts.get_gc_contact(self._account,
|
|||
|
room_jid,
|
|||
|
properties.muc_nickname)
|
|||
|
|
|||
|
if properties.is_nickname_changed:
|
|||
|
if properties.is_muc_self_presence:
|
|||
|
muc_data.nick = properties.muc_user.nick
|
|||
|
self._con.get_module('Bookmarks').modify(muc_data.jid,
|
|||
|
nick=muc_data.nick)
|
|||
|
app.contacts.remove_gc_contact(self._account, contact)
|
|||
|
contact.name = properties.muc_user.nick
|
|||
|
app.contacts.add_gc_contact(self._account, contact)
|
|||
|
initiator = 'Server' if properties.is_nickname_modified else 'User'
|
|||
|
self._log.info('%s nickname changed: %s to %s',
|
|||
|
initiator,
|
|||
|
properties.jid,
|
|||
|
properties.muc_user.nick)
|
|||
|
self._raise_muc_event('muc-nickname-changed', properties)
|
|||
|
return
|
|||
|
|
|||
|
if contact is None and properties.type.is_available:
|
|||
|
self._add_new_muc_contact(properties)
|
|||
|
if properties.is_muc_self_presence:
|
|||
|
self._log.info('Self presence: %s', properties.jid)
|
|||
|
if muc_data.state == MUCJoinedState.JOINING:
|
|||
|
if (properties.is_nickname_modified or
|
|||
|
muc_data.nick != properties.muc_nickname):
|
|||
|
muc_data.nick = properties.muc_nickname
|
|||
|
self._log.info('Server modified nickname to: %s',
|
|||
|
properties.muc_nickname)
|
|||
|
|
|||
|
elif muc_data.state == MUCJoinedState.CREATING:
|
|||
|
if properties.is_new_room:
|
|||
|
self.configure_room(room_jid)
|
|||
|
|
|||
|
self._start_join_timeout(room_jid)
|
|||
|
self._raise_muc_event('muc-self-presence', properties)
|
|||
|
|
|||
|
else:
|
|||
|
self._log.info('User joined: %s', properties.jid)
|
|||
|
self._raise_muc_event('muc-user-joined', properties)
|
|||
|
return
|
|||
|
|
|||
|
if properties.is_muc_self_presence and properties.is_kicked:
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
self._raise_muc_event('muc-self-kicked', properties)
|
|||
|
status_codes = properties.muc_status_codes or []
|
|||
|
if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
|
|||
|
self._start_rejoin_timeout(room_jid)
|
|||
|
return
|
|||
|
|
|||
|
if properties.is_muc_self_presence and properties.type.is_unavailable:
|
|||
|
# Its not a kick, so this is the reflection of our own
|
|||
|
# unavailable presence, because we left the MUC
|
|||
|
return
|
|||
|
|
|||
|
if properties.type.is_unavailable:
|
|||
|
for _event in app.events.get_events(self._account,
|
|||
|
jid=str(properties.jid),
|
|||
|
types=['pm']):
|
|||
|
contact.show = properties.show
|
|||
|
contact.presence = properties.type
|
|||
|
contact.status = properties.status
|
|||
|
contact.affiliation = properties.affiliation
|
|||
|
app.interface.handle_event(self._account,
|
|||
|
str(properties.jid),
|
|||
|
'pm')
|
|||
|
# Handle only the first pm event, the rest will be
|
|||
|
# handled by the opened ChatControl
|
|||
|
break
|
|||
|
|
|||
|
if contact is None:
|
|||
|
# If contact is None, its probably that a user left from a not
|
|||
|
# insync MUC, can happen on older servers
|
|||
|
self._log.warning('Unknown contact left groupchat: %s',
|
|||
|
properties.jid)
|
|||
|
else:
|
|||
|
# We remove the contact from the MUC, but there could be
|
|||
|
# a PrivateChatControl open, so we update the contacts presence
|
|||
|
contact.presence = properties.type
|
|||
|
app.contacts.remove_gc_contact(self._account, contact)
|
|||
|
self._log.info('User %s left', properties.jid)
|
|||
|
self._raise_muc_event('muc-user-left', properties)
|
|||
|
return
|
|||
|
|
|||
|
if contact.affiliation != properties.affiliation:
|
|||
|
contact.affiliation = properties.affiliation
|
|||
|
self._log.info('Affiliation changed: %s %s',
|
|||
|
properties.jid,
|
|||
|
properties.affiliation)
|
|||
|
self._raise_muc_event('muc-user-affiliation-changed', properties)
|
|||
|
|
|||
|
if contact.role != properties.role:
|
|||
|
contact.role = properties.role
|
|||
|
self._log.info('Role changed: %s %s',
|
|||
|
properties.jid,
|
|||
|
properties.role)
|
|||
|
self._raise_muc_event('muc-user-role-changed', properties)
|
|||
|
|
|||
|
if (contact.status != properties.status or
|
|||
|
contact.show != properties.show):
|
|||
|
contact.status = properties.status
|
|||
|
contact.show = properties.show
|
|||
|
self._log.info('Show/Status changed: %s %s %s',
|
|||
|
properties.jid,
|
|||
|
properties.status,
|
|||
|
properties.show)
|
|||
|
self._raise_muc_event('muc-user-status-show-changed', properties)
|
|||
|
|
|||
|
def _start_rejoin_timeout(self, room_jid):
|
|||
|
self._remove_rejoin_timeout(room_jid)
|
|||
|
self._rejoin_muc.add(room_jid)
|
|||
|
self._log.info('Start rejoin timeout for: %s', room_jid)
|
|||
|
id_ = GLib.timeout_add_seconds(2, self._rejoin, room_jid)
|
|||
|
self._rejoin_timeouts[room_jid] = id_
|
|||
|
|
|||
|
def _remove_rejoin_timeout(self, room_jid):
|
|||
|
self._rejoin_muc.discard(room_jid)
|
|||
|
id_ = self._rejoin_timeouts.get(room_jid)
|
|||
|
if id_ is not None:
|
|||
|
self._log.info('Remove rejoin timeout for: %s', room_jid)
|
|||
|
GLib.source_remove(id_)
|
|||
|
del self._rejoin_timeouts[room_jid]
|
|||
|
|
|||
|
def _start_join_timeout(self, room_jid):
|
|||
|
self._remove_join_timeout(room_jid)
|
|||
|
self._log.info('Start join timeout for: %s', room_jid)
|
|||
|
id_ = GLib.timeout_add_seconds(
|
|||
|
10, self._fake_subject_change, room_jid)
|
|||
|
self._join_timeouts[room_jid] = id_
|
|||
|
|
|||
|
def _remove_join_timeout(self, room_jid):
|
|||
|
id_ = self._join_timeouts.get(room_jid)
|
|||
|
if id_ is not None:
|
|||
|
self._log.info('Remove join timeout for: %s', room_jid)
|
|||
|
GLib.source_remove(id_)
|
|||
|
del self._join_timeouts[room_jid]
|
|||
|
|
|||
|
def _raise_muc_event(self, event_name, properties):
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent(event_name,
|
|||
|
account=self._account,
|
|||
|
room_jid=properties.jid.bare,
|
|||
|
properties=properties))
|
|||
|
self._log_muc_event(event_name, properties)
|
|||
|
|
|||
|
def _log_muc_event(self, event_name, properties):
|
|||
|
if event_name not in ['muc-user-joined',
|
|||
|
'muc-user-left',
|
|||
|
'muc-user-status-show-changed']:
|
|||
|
return
|
|||
|
|
|||
|
if (not app.settings.get('log_contact_status_changes') or
|
|||
|
not helpers.should_log(self._account, properties.jid)):
|
|||
|
return
|
|||
|
|
|||
|
additional_data = AdditionalDataDict()
|
|||
|
if properties.muc_user is not None:
|
|||
|
if properties.muc_user.jid is not None:
|
|||
|
additional_data.set_value(
|
|||
|
'gajim', 'real_jid', str(properties.muc_user.jid))
|
|||
|
|
|||
|
# TODO: Refactor
|
|||
|
if properties.type == PresenceType.UNAVAILABLE:
|
|||
|
show = 'offline'
|
|||
|
else:
|
|||
|
show = properties.show.value
|
|||
|
show = app.storage.archive.convert_show_values_to_db_api_values(show)
|
|||
|
|
|||
|
app.storage.archive.insert_into_logs(
|
|||
|
self._account,
|
|||
|
properties.jid.bare,
|
|||
|
properties.timestamp,
|
|||
|
KindConstant.GCSTATUS,
|
|||
|
contact_name=properties.muc_nickname,
|
|||
|
message=properties.status or None,
|
|||
|
show=show,
|
|||
|
additional_data=additional_data)
|
|||
|
|
|||
|
def _add_new_muc_contact(self, properties):
|
|||
|
real_jid = None
|
|||
|
if properties.muc_user.jid is not None:
|
|||
|
real_jid = str(properties.muc_user.jid)
|
|||
|
contact = app.contacts.create_gc_contact(
|
|||
|
room_jid=properties.jid.bare,
|
|||
|
account=self._account,
|
|||
|
name=properties.muc_nickname,
|
|||
|
show=properties.show,
|
|||
|
status=properties.status,
|
|||
|
presence=properties.type,
|
|||
|
role=properties.role,
|
|||
|
affiliation=properties.affiliation,
|
|||
|
jid=real_jid,
|
|||
|
avatar_sha=properties.avatar_sha)
|
|||
|
app.contacts.add_gc_contact(self._account, contact)
|
|||
|
|
|||
|
def _on_subject_change(self, _con, _stanza, properties):
|
|||
|
if not properties.is_muc_subject:
|
|||
|
return
|
|||
|
|
|||
|
self._handle_subject_change(str(properties.muc_jid),
|
|||
|
properties.subject,
|
|||
|
properties.muc_nickname,
|
|||
|
properties.user_timestamp)
|
|||
|
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
def _fake_subject_change(self, room_jid):
|
|||
|
# This is for servers which don’t send empty subjects as part of the
|
|||
|
# event order on joining a MUC. For example jabber.ru
|
|||
|
self._log.warning('Fake subject received for %s', room_jid)
|
|||
|
del self._join_timeouts[room_jid]
|
|||
|
self._handle_subject_change(room_jid, None, None, None)
|
|||
|
|
|||
|
def _handle_subject_change(self, room_jid, subject, nickname, timestamp):
|
|||
|
contact = app.contacts.get_groupchat_contact(self._account, room_jid)
|
|||
|
if contact is None:
|
|||
|
return
|
|||
|
|
|||
|
contact.status = subject
|
|||
|
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-subject',
|
|||
|
account=self._account,
|
|||
|
room_jid=room_jid,
|
|||
|
subject=subject,
|
|||
|
nickname=nickname,
|
|||
|
user_timestamp=timestamp,
|
|||
|
is_fake=subject is None))
|
|||
|
|
|||
|
muc_data = self._manager.get(room_jid)
|
|||
|
if muc_data.state == MUCJoinedState.JOINING:
|
|||
|
self._room_join_complete(muc_data)
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-joined',
|
|||
|
account=self._account,
|
|||
|
room_jid=muc_data.jid))
|
|||
|
|
|||
|
def _room_join_complete(self, muc_data):
|
|||
|
self._remove_join_timeout(muc_data.jid)
|
|||
|
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINED)
|
|||
|
self._remove_rejoin_timeout(muc_data.jid)
|
|||
|
|
|||
|
# We successfully joined a MUC, set add bookmark with autojoin
|
|||
|
self._con.get_module('Bookmarks').add_or_modify(
|
|||
|
muc_data.jid,
|
|||
|
autojoin=True,
|
|||
|
password=muc_data.password,
|
|||
|
nick=muc_data.nick)
|
|||
|
|
|||
|
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('muc-voice-request',
|
|||
|
account=self._account,
|
|||
|
room_jid=str(properties.muc_jid),
|
|||
|
voice_request=properties.voice_request))
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
def _on_captcha_challenge(self, _con, _stanza, properties):
|
|||
|
if not properties.is_captcha_challenge:
|
|||
|
return
|
|||
|
|
|||
|
if properties.is_mam_message:
|
|||
|
# Some servers store captcha challenges in MAM, don’t process them
|
|||
|
self._log.warning('Ignore captcha challenge received from MAM')
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
muc_data = self._manager.get(properties.jid)
|
|||
|
if muc_data is None:
|
|||
|
return
|
|||
|
|
|||
|
if muc_data.state != MUCJoinedState.JOINING:
|
|||
|
self._log.warning('Received captcha request but state != %s',
|
|||
|
MUCJoinedState.JOINING)
|
|||
|
return
|
|||
|
|
|||
|
contact = app.contacts.get_groupchat_contact(self._account,
|
|||
|
str(properties.jid))
|
|||
|
if contact is None:
|
|||
|
return
|
|||
|
|
|||
|
self._log.info('Captcha challenge received from %s', properties.jid)
|
|||
|
store_bob_data(properties.captcha.bob_data)
|
|||
|
muc_data.captcha_id = properties.id
|
|||
|
|
|||
|
self._manager.set_state(properties.jid, MUCJoinedState.CAPTCHA_REQUEST)
|
|||
|
self._remove_rejoin_timeout(properties.jid)
|
|||
|
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-captcha-challenge',
|
|||
|
account=self._account,
|
|||
|
room_jid=properties.jid.bare,
|
|||
|
form=properties.captcha.form))
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
def cancel_captcha(self, room_jid):
|
|||
|
muc_data = self._manager.get(room_jid)
|
|||
|
if muc_data is None:
|
|||
|
return
|
|||
|
|
|||
|
if muc_data.captcha_id is None:
|
|||
|
self._log.warning('No captcha message id available')
|
|||
|
return
|
|||
|
self._nbxmpp('MUC').cancel_captcha(room_jid, muc_data.captcha_id)
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
|||
|
|
|||
|
def send_captcha(self, room_jid, form_node):
|
|||
|
self._manager.set_state(room_jid, MUCJoinedState.JOINING)
|
|||
|
self._nbxmpp('MUC').send_captcha(room_jid,
|
|||
|
form_node,
|
|||
|
callback=self._on_captcha_result)
|
|||
|
|
|||
|
def _on_captcha_result(self, task):
|
|||
|
try:
|
|||
|
task.finish()
|
|||
|
except StanzaError as error:
|
|||
|
muc_data = self._manager.get(error.jid)
|
|||
|
if muc_data is None:
|
|||
|
return
|
|||
|
self._manager.set_state(error.jid, MUCJoinedState.CAPTCHA_FAILED)
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-captcha-error',
|
|||
|
account=self._account,
|
|||
|
room_jid=str(error.jid),
|
|||
|
error_text=to_user_string(error)))
|
|||
|
|
|||
|
def _on_config_change(self, _con, _stanza, properties):
|
|||
|
if not properties.is_muc_config_change:
|
|||
|
return
|
|||
|
|
|||
|
room_jid = str(properties.muc_jid)
|
|||
|
self._log.info('Received config change: %s %s',
|
|||
|
room_jid, properties.muc_status_codes)
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-config-changed',
|
|||
|
account=self._account,
|
|||
|
room_jid=room_jid,
|
|||
|
status_codes=properties.muc_status_codes))
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
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
|
|||
|
|
|||
|
self._log.info('Invite declined from: %s, reason: %s',
|
|||
|
data.from_, data.reason)
|
|||
|
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-decline',
|
|||
|
account=self._account,
|
|||
|
**data._asdict()))
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
if properties.muc_invite is not None:
|
|||
|
data = properties.muc_invite
|
|||
|
if helpers.ignore_contact(self._account, data.from_):
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
self._log.info('Invite from: %s, to: %s', data.from_, data.muc)
|
|||
|
|
|||
|
if app.in_groupchat(self._account, data.muc):
|
|||
|
# We are already in groupchat. Ignore invitation
|
|||
|
self._log.info('We are already in this room')
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
self._con.get_module('Discovery').disco_muc(
|
|||
|
data.muc,
|
|||
|
request_vcard=True,
|
|||
|
callback=self._on_disco_result_after_invite,
|
|||
|
user_data=data)
|
|||
|
|
|||
|
raise nbxmpp.NodeProcessed
|
|||
|
|
|||
|
def _on_disco_result_after_invite(self, task):
|
|||
|
try:
|
|||
|
result = task.finish()
|
|||
|
except StanzaError as error:
|
|||
|
self._log.warning(error)
|
|||
|
return
|
|||
|
|
|||
|
invite_data = task.get_user_data()
|
|||
|
app.nec.push_incoming_event(
|
|||
|
NetworkEvent('muc-invitation',
|
|||
|
account=self._account,
|
|||
|
info=result.info,
|
|||
|
**invite_data._asdict()))
|
|||
|
|
|||
|
def invite(self, room, to, reason=None, continue_=False):
|
|||
|
type_ = InviteType.MEDIATED
|
|||
|
contact = app.contacts.get_contact_from_full_jid(self._account, to)
|
|||
|
if contact and contact.supports(Namespace.CONFERENCE):
|
|||
|
type_ = InviteType.DIRECT
|
|||
|
|
|||
|
password = app.gc_passwords.get(room, None)
|
|||
|
self._log.info('Invite %s to %s', to, room)
|
|||
|
return self._nbxmpp('MUC').invite(room, to, reason, password,
|
|||
|
continue_, type_)
|
|||
|
|
|||
|
@event_filter(['account'])
|
|||
|
def _on_account_disconnected(self, _event):
|
|||
|
for room_jid in list(self._rejoin_timeouts.keys()):
|
|||
|
self._remove_rejoin_timeout(room_jid)
|
|||
|
|
|||
|
for room_jid in list(self._join_timeouts.keys()):
|
|||
|
self._remove_join_timeout(room_jid)
|
|||
|
|
|||
|
|
|||
|
class MUCManager:
|
|||
|
def __init__(self, logger):
|
|||
|
self._log = logger
|
|||
|
self._mucs = {}
|
|||
|
|
|||
|
def add(self, muc):
|
|||
|
self._mucs[muc.jid] = muc
|
|||
|
|
|||
|
def remove(self, muc):
|
|||
|
self._mucs.pop(muc.jid, None)
|
|||
|
|
|||
|
def get(self, room_jid):
|
|||
|
return self._mucs.get(room_jid)
|
|||
|
|
|||
|
def set_state(self, room_jid, state):
|
|||
|
muc = self._mucs.get(room_jid)
|
|||
|
if muc is not None:
|
|||
|
if muc.state == state:
|
|||
|
return
|
|||
|
self._log.info('Set MUC state: %s %s', room_jid, state)
|
|||
|
muc.state = state
|
|||
|
|
|||
|
def get_joined_mucs(self):
|
|||
|
mucs = self._mucs.values()
|
|||
|
return [muc.jid for muc in mucs if muc.state == MUCJoinedState.JOINED]
|
|||
|
|
|||
|
def get_mucs_with_state(self, states):
|
|||
|
return [muc for muc in self._mucs.values() if muc.state in states]
|
|||
|
|
|||
|
def reset_state(self):
|
|||
|
for muc in self._mucs.values():
|
|||
|
self.set_state(muc.jid, MUCJoinedState.NOT_JOINED)
|
|||
|
|
|||
|
def __contains__(self, room_jid):
|
|||
|
return room_jid in self._mucs
|
|||
|
|
|||
|
|
|||
|
def get_instance(*args, **kwargs):
|
|||
|
return MUC(*args, **kwargs), 'MUC'
|