444 lines
17 KiB
Python
444 lines
17 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 time
|
|
import logging
|
|
|
|
import nbxmpp
|
|
from nbxmpp.const import InviteType
|
|
from nbxmpp.const import PresenceType
|
|
from nbxmpp.structs import StanzaHandler
|
|
|
|
from gajim.common import app
|
|
from gajim.common import helpers
|
|
from gajim.common.const import KindConstant
|
|
from gajim.common.helpers import AdditionalDataDict
|
|
from gajim.common.caps_cache import muc_caps_cache
|
|
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',
|
|
'decline',
|
|
'invite',
|
|
'request_config',
|
|
'request_voice',
|
|
'destroy',
|
|
]
|
|
|
|
def __init__(self, con):
|
|
BaseModule.__init__(self, con)
|
|
|
|
self.handlers = [
|
|
StanzaHandler(name='presence',
|
|
callback=self._on_muc_user_presence,
|
|
ns=nbxmpp.NS_MUC_USER,
|
|
priority=49),
|
|
StanzaHandler(name='presence',
|
|
callback=self._on_muc_presence,
|
|
ns=nbxmpp.NS_MUC,
|
|
priority=49),
|
|
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._register_callback('request_config', self._config_received)
|
|
|
|
def pass_disco(self, from_, identities, features, _data, _node):
|
|
for identity in identities:
|
|
if identity.get('category') != 'conference':
|
|
continue
|
|
if identity.get('type') != 'text':
|
|
continue
|
|
if nbxmpp.NS_MUC in features:
|
|
log.info('Discovered MUC: %s', from_)
|
|
# TODO: make this nicer
|
|
self._con.muc_jid['jabber'] = from_
|
|
raise nbxmpp.NodeProcessed
|
|
|
|
def send_muc_join_presence(self, *args, room_jid=None, password=None,
|
|
rejoin=False, **kwargs):
|
|
if not app.account_is_connected(self._account):
|
|
return
|
|
presence = self._con.get_module('Presence').get_presence(
|
|
*args, **kwargs)
|
|
|
|
muc_x = presence.setTag(nbxmpp.NS_MUC + ' x')
|
|
if room_jid is not None:
|
|
self._add_history_query(muc_x, room_jid, rejoin)
|
|
|
|
if password is not None:
|
|
muc_x.setTagData('password', password)
|
|
|
|
log.debug('Send MUC join presence:\n%s', presence)
|
|
|
|
self._con.connection.send(presence)
|
|
|
|
def _add_history_query(self, muc_x, room_jid, rejoin):
|
|
last_date = app.logger.get_room_last_message_time(
|
|
self._account, room_jid)
|
|
if not last_date:
|
|
last_date = 0
|
|
|
|
if muc_caps_cache.has_mam(room_jid):
|
|
# The room is MAM capable dont get MUC History
|
|
muc_x.setTag('history', {'maxchars': '0'})
|
|
else:
|
|
# Request MUC History (not MAM)
|
|
tags = {}
|
|
timeout = app.config.get_per('rooms', room_jid,
|
|
'muc_restore_timeout')
|
|
if timeout is None or timeout == -2:
|
|
timeout = app.config.get('muc_restore_timeout')
|
|
if last_date == 0 and timeout >= 0:
|
|
last_date = time.time() - timeout * 60
|
|
elif not rejoin and timeout >= 0:
|
|
last_date = max(last_date, time.time() - timeout * 60)
|
|
last_date = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(
|
|
last_date))
|
|
tags['since'] = last_date
|
|
|
|
nb = app.config.get_per('rooms', room_jid, 'muc_restore_lines')
|
|
if nb is None or nb == -2:
|
|
nb = app.config.get('muc_restore_lines')
|
|
if nb >= 0:
|
|
tags['maxstanzas'] = nb
|
|
if tags:
|
|
muc_x.setTag('history', tags)
|
|
|
|
def _on_muc_presence(self, _con, _stanza, properties):
|
|
if properties.type == PresenceType.ERROR:
|
|
self._raise_muc_event('muc-presence-error', properties)
|
|
return
|
|
|
|
def _on_muc_user_presence(self, _con, _stanza, properties):
|
|
if properties.type == PresenceType.ERROR:
|
|
return
|
|
|
|
if properties.is_muc_destroyed:
|
|
for contact in app.contacts.get_gc_contact_list(
|
|
self._account, properties.jid.getBare()):
|
|
contact.presence = PresenceType.UNAVAILABLE
|
|
log.info('MUC destroyed: %s', properties.jid.getBare())
|
|
self._raise_muc_event('muc-destroyed', properties)
|
|
return
|
|
|
|
contact = app.contacts.get_gc_contact(self._account,
|
|
properties.jid.getBare(),
|
|
properties.muc_nickname)
|
|
|
|
if properties.is_nickname_changed:
|
|
app.contacts.remove_gc_contact(self._account, contact)
|
|
contact.name = properties.muc_user.nick
|
|
app.contacts.add_gc_contact(self._account, contact)
|
|
log.info('Nickname changed: %s to %s',
|
|
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:
|
|
log.info('Self presence: %s', properties.jid)
|
|
self._raise_muc_event('muc-self-presence', properties)
|
|
else:
|
|
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._raise_muc_event('muc-self-kicked', properties)
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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 _raise_muc_event(self, event_name, properties):
|
|
app.nec.push_incoming_event(
|
|
NetworkEvent(event_name,
|
|
account=self._account,
|
|
room_jid=properties.jid.getBare(),
|
|
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.config.get('log_contact_status_changes') or
|
|
not app.config.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.logger.convert_show_values_to_db_api_values(show)
|
|
|
|
app.logger.insert_into_logs(
|
|
self._account,
|
|
properties.jid.getBare(),
|
|
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.getBare(),
|
|
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
|
|
|
|
jid = properties.jid.getBare()
|
|
contact = app.contacts.get_groupchat_contact(self._account, jid)
|
|
if contact is None:
|
|
return
|
|
|
|
contact.status = properties.subject
|
|
|
|
app.nec.push_incoming_event(
|
|
NetworkEvent('muc-subject',
|
|
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('muc-voice-approval',
|
|
account=self._account,
|
|
jid=jid,
|
|
form=properties.voice_request.form))
|
|
raise nbxmpp.NodeProcessed
|
|
|
|
def _on_captcha_challenge(self, _con, _stanza, properties):
|
|
if not properties.is_captcha_challenge:
|
|
return
|
|
|
|
contact = app.contacts.get_groupchat_contact(self._account,
|
|
str(properties.jid))
|
|
if contact is None:
|
|
return
|
|
|
|
log.info('Captcha challenge received from %s', properties.jid)
|
|
store_bob_data(properties.captcha.bob_data)
|
|
|
|
app.nec.push_incoming_event(
|
|
NetworkEvent('muc-captcha-challenge',
|
|
account=self._account,
|
|
jid=properties.jid,
|
|
form=properties.captcha.form))
|
|
raise nbxmpp.NodeProcessed
|
|
|
|
def _on_config_change(self, _con, _stanza, properties):
|
|
if not properties.is_muc_config_change:
|
|
return
|
|
|
|
log.info('Received config change: %s %s',
|
|
properties.jid, properties.muc_status_codes)
|
|
app.nec.push_incoming_event(
|
|
NetworkEvent('muc-config-changed',
|
|
account=self._account,
|
|
jid=properties.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
|
|
|
|
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
|
|
|
|
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
|
|
log.info('We are already in this room')
|
|
raise nbxmpp.NodeProcessed
|
|
|
|
app.nec.push_incoming_event(
|
|
NetworkEvent('muc-invitation',
|
|
account=self._account,
|
|
**data._asdict()))
|
|
raise nbxmpp.NodeProcessed
|
|
|
|
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(nbxmpp.NS_CONFERENCE):
|
|
type_ = InviteType.DIRECT
|
|
|
|
password = app.gc_passwords.get(room, None)
|
|
self._nbxmpp('MUC').invite(room, to, reason, password, continue_, type_)
|
|
|
|
def _config_received(self, result):
|
|
if result.is_error:
|
|
return
|
|
|
|
app.nec.push_incoming_event(NetworkEvent(
|
|
'muc-config',
|
|
conn=self._con,
|
|
dataform=result.form,
|
|
jid=result.jid))
|
|
|
|
|
|
def get_instance(*args, **kwargs):
|
|
return MUC(*args, **kwargs), 'MUC'
|