gajim3/gajim/common/modules/muc.py

858 lines
32 KiB
Python
Raw Normal View History

# 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 dont 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, dont 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'