395 lines
14 KiB
Python
395 lines
14 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/>.
|
||
|
|
||
|
# Presence handler
|
||
|
|
||
|
import time
|
||
|
|
||
|
import nbxmpp
|
||
|
from nbxmpp.namespaces import Namespace
|
||
|
from nbxmpp.structs import StanzaHandler
|
||
|
from nbxmpp.const import PresenceType
|
||
|
|
||
|
from gajim.common import app
|
||
|
from gajim.common import idle
|
||
|
from gajim.common.i18n import _
|
||
|
from gajim.common.nec import NetworkEvent
|
||
|
from gajim.common.helpers import should_log
|
||
|
from gajim.common.const import KindConstant
|
||
|
from gajim.common.const import ShowConstant
|
||
|
from gajim.common.modules.base import BaseModule
|
||
|
|
||
|
|
||
|
class Presence(BaseModule):
|
||
|
def __init__(self, con):
|
||
|
BaseModule.__init__(self, con)
|
||
|
|
||
|
self.handlers = [
|
||
|
StanzaHandler(name='presence',
|
||
|
callback=self._presence_received,
|
||
|
priority=50),
|
||
|
StanzaHandler(name='presence',
|
||
|
callback=self._subscribe_received,
|
||
|
typ='subscribe',
|
||
|
priority=49),
|
||
|
StanzaHandler(name='presence',
|
||
|
callback=self._subscribed_received,
|
||
|
typ='subscribed',
|
||
|
priority=49),
|
||
|
StanzaHandler(name='presence',
|
||
|
callback=self._unsubscribe_received,
|
||
|
typ='unsubscribe',
|
||
|
priority=49),
|
||
|
StanzaHandler(name='presence',
|
||
|
callback=self._unsubscribed_received,
|
||
|
typ='unsubscribed',
|
||
|
priority=49),
|
||
|
]
|
||
|
|
||
|
# keep the jids we auto added (transports contacts) to not send the
|
||
|
# SUBSCRIBED event to GUI
|
||
|
self.automatically_added = []
|
||
|
|
||
|
# list of jid to auto-authorize
|
||
|
self._jids_for_auto_auth = set()
|
||
|
|
||
|
def _presence_received(self, _con, stanza, properties):
|
||
|
if properties.from_muc:
|
||
|
# MUC occupant presences are already handled in MUC module
|
||
|
return
|
||
|
|
||
|
muc = self._con.get_module('MUC').get_manager().get(properties.jid)
|
||
|
if muc is not None:
|
||
|
# Presence from the MUC itself, used for MUC avatar
|
||
|
# handled in VCardAvatars module
|
||
|
return
|
||
|
|
||
|
self._log.info('Received from %s', properties.jid)
|
||
|
|
||
|
if properties.type == PresenceType.ERROR:
|
||
|
self._log.info('Error: %s %s', properties.jid, properties.error)
|
||
|
return
|
||
|
|
||
|
if self._account == 'Local':
|
||
|
app.nec.push_incoming_event(
|
||
|
NetworkEvent('raw-pres-received',
|
||
|
conn=self._con,
|
||
|
stanza=stanza))
|
||
|
return
|
||
|
|
||
|
if properties.is_self_presence:
|
||
|
app.nec.push_incoming_event(
|
||
|
NetworkEvent('our-show',
|
||
|
account=self._account,
|
||
|
show=properties.show.value))
|
||
|
return
|
||
|
|
||
|
jid = properties.jid.bare
|
||
|
roster_item = self._con.get_module('Roster').get_item(jid)
|
||
|
if not properties.is_self_bare and roster_item is None:
|
||
|
# Handle only presence from roster contacts
|
||
|
self._log.warning('Unknown presence received')
|
||
|
self._log.warning(stanza)
|
||
|
return
|
||
|
|
||
|
show = properties.show.value
|
||
|
if properties.type.is_unavailable:
|
||
|
show = 'offline'
|
||
|
|
||
|
event_attrs = {
|
||
|
'conn': self._con,
|
||
|
'stanza': stanza,
|
||
|
'prio': properties.priority,
|
||
|
'need_add_in_roster': False,
|
||
|
'popup': False,
|
||
|
'ptype': properties.type.value,
|
||
|
'jid': properties.jid.bare,
|
||
|
'resource': properties.jid.resource,
|
||
|
'id_': properties.id,
|
||
|
'fjid': str(properties.jid),
|
||
|
'timestamp': properties.timestamp,
|
||
|
'avatar_sha': properties.avatar_sha,
|
||
|
'user_nick': properties.nickname,
|
||
|
'idle_time': properties.idle_timestamp,
|
||
|
'show': show,
|
||
|
'new_show': show,
|
||
|
'old_show': 0,
|
||
|
'status': properties.status,
|
||
|
'contact_list': [],
|
||
|
'contact': None,
|
||
|
}
|
||
|
|
||
|
event_ = NetworkEvent('presence-received', **event_attrs)
|
||
|
|
||
|
# TODO: Refactor
|
||
|
self._update_contact(event_, properties)
|
||
|
|
||
|
app.nec.push_incoming_event(event_)
|
||
|
|
||
|
def _update_contact(self, event, properties):
|
||
|
# Note: A similar method also exists in connection_zeroconf
|
||
|
jid = properties.jid.bare
|
||
|
resource = properties.jid.resource
|
||
|
|
||
|
status_strings = ['offline', 'error', 'online', 'chat', 'away',
|
||
|
'xa', 'dnd']
|
||
|
|
||
|
event.new_show = status_strings.index(event.show)
|
||
|
|
||
|
# Update contact
|
||
|
contact_list = app.contacts.get_contacts(self._account, jid)
|
||
|
if not contact_list:
|
||
|
self._log.warning('No contact found')
|
||
|
return
|
||
|
|
||
|
event.contact_list = contact_list
|
||
|
|
||
|
contact = app.contacts.get_contact_strict(self._account,
|
||
|
properties.jid.bare,
|
||
|
properties.jid.resource)
|
||
|
if contact is None:
|
||
|
contact = app.contacts.get_first_contact_from_jid(self._account,
|
||
|
jid)
|
||
|
if contact is None:
|
||
|
self._log.warning('First contact not found')
|
||
|
return
|
||
|
|
||
|
if (self._is_resource_known(contact_list) and
|
||
|
not app.jid_is_transport(jid)):
|
||
|
# Another resource of an existing contact connected
|
||
|
# Add new contact
|
||
|
event.old_show = 0
|
||
|
contact = app.contacts.copy_contact(contact)
|
||
|
contact.resource = resource
|
||
|
app.contacts.add_contact(self._account, contact)
|
||
|
else:
|
||
|
# Convert the initial roster contact to a contact with resource
|
||
|
contact.resource = resource
|
||
|
event.old_show = 0
|
||
|
if contact.show in status_strings:
|
||
|
event.old_show = status_strings.index(contact.show)
|
||
|
|
||
|
event.need_add_in_roster = True
|
||
|
|
||
|
elif contact.show in status_strings:
|
||
|
event.old_show = status_strings.index(contact.show)
|
||
|
|
||
|
# Update contact with presence data
|
||
|
contact.show = event.show
|
||
|
contact.status = properties.status
|
||
|
contact.priority = properties.priority
|
||
|
contact.idle_time = properties.idle_timestamp
|
||
|
|
||
|
event.contact = contact
|
||
|
|
||
|
if not app.jid_is_transport(jid) and len(contact_list) == 1:
|
||
|
# It's not an agent
|
||
|
if event.old_show == 0 and event.new_show > 1:
|
||
|
if not jid in app.newly_added[self._account]:
|
||
|
app.newly_added[self._account].append(jid)
|
||
|
if jid in app.to_be_removed[self._account]:
|
||
|
app.to_be_removed[self._account].remove(jid)
|
||
|
elif event.old_show > 1 and event.new_show == 0 and \
|
||
|
self._con.state.is_available:
|
||
|
if not jid in app.to_be_removed[self._account]:
|
||
|
app.to_be_removed[self._account].append(jid)
|
||
|
if jid in app.newly_added[self._account]:
|
||
|
app.newly_added[self._account].remove(jid)
|
||
|
|
||
|
if app.jid_is_transport(jid):
|
||
|
return
|
||
|
|
||
|
if properties.type.is_unavailable:
|
||
|
# TODO: This causes problems when another
|
||
|
# resource signs off!
|
||
|
self._con.get_module('Bytestream').stop_all_active_file_transfers(
|
||
|
contact)
|
||
|
self._log_presence(properties)
|
||
|
|
||
|
@staticmethod
|
||
|
def _is_resource_known(contact_list):
|
||
|
if len(contact_list) > 1:
|
||
|
return True
|
||
|
|
||
|
if contact_list[0].resource == '':
|
||
|
return False
|
||
|
return contact_list[0].show not in ('not in roster', 'offline')
|
||
|
|
||
|
def _log_presence(self, properties):
|
||
|
if not app.settings.get('log_contact_status_changes'):
|
||
|
return
|
||
|
if not should_log(self._account, properties.jid.bare):
|
||
|
return
|
||
|
|
||
|
show = ShowConstant[properties.show.name]
|
||
|
if properties.type.is_unavailable:
|
||
|
show = ShowConstant.OFFLINE
|
||
|
|
||
|
app.storage.archive.insert_into_logs(self._account,
|
||
|
properties.jid.bare,
|
||
|
time.time(),
|
||
|
KindConstant.STATUS,
|
||
|
message=properties.status,
|
||
|
show=show)
|
||
|
|
||
|
def _subscribe_received(self, _con, _stanza, properties):
|
||
|
jid = properties.jid.bare
|
||
|
fjid = str(properties.jid)
|
||
|
|
||
|
is_transport = app.jid_is_transport(fjid)
|
||
|
auto_auth = app.settings.get_account_setting(self._account, 'autoauth')
|
||
|
|
||
|
self._log.info('Received Subscribe: %s, transport: %s, '
|
||
|
'auto_auth: %s, user_nick: %s',
|
||
|
properties.jid, is_transport,
|
||
|
auto_auth, properties.nickname)
|
||
|
|
||
|
if auto_auth or jid in self._jids_for_auto_auth:
|
||
|
self.send_presence(fjid, 'subscribed')
|
||
|
self._jids_for_auto_auth.discard(jid)
|
||
|
self._log.info('Auto respond with subscribed: %s', jid)
|
||
|
return
|
||
|
|
||
|
status = (properties.status or
|
||
|
_('I would like to add you to my roster.'))
|
||
|
|
||
|
app.nec.push_incoming_event(NetworkEvent(
|
||
|
'subscribe-presence-received',
|
||
|
conn=self._con,
|
||
|
jid=jid,
|
||
|
fjid=fjid,
|
||
|
status=status,
|
||
|
user_nick=properties.nickname,
|
||
|
is_transport=is_transport))
|
||
|
|
||
|
raise nbxmpp.NodeProcessed
|
||
|
|
||
|
def _subscribed_received(self, _con, _stanza, properties):
|
||
|
jid = properties.jid.bare
|
||
|
self._log.info('Received Subscribed: %s', properties.jid)
|
||
|
if jid in self.automatically_added:
|
||
|
self.automatically_added.remove(jid)
|
||
|
raise nbxmpp.NodeProcessed
|
||
|
|
||
|
app.nec.push_incoming_event(NetworkEvent(
|
||
|
'subscribed-presence-received',
|
||
|
account=self._account,
|
||
|
jid=properties.jid))
|
||
|
raise nbxmpp.NodeProcessed
|
||
|
|
||
|
def _unsubscribe_received(self, _con, _stanza, properties):
|
||
|
self._log.info('Received Unsubscribe: %s', properties.jid)
|
||
|
raise nbxmpp.NodeProcessed
|
||
|
|
||
|
def _unsubscribed_received(self, _con, _stanza, properties):
|
||
|
self._log.info('Received Unsubscribed: %s', properties.jid)
|
||
|
app.nec.push_incoming_event(NetworkEvent(
|
||
|
'unsubscribed-presence-received',
|
||
|
conn=self._con, jid=properties.jid.bare))
|
||
|
raise nbxmpp.NodeProcessed
|
||
|
|
||
|
def subscribed(self, jid):
|
||
|
if not app.account_is_available(self._account):
|
||
|
return
|
||
|
self._log.info('Subscribed: %s', jid)
|
||
|
self.send_presence(jid, 'subscribed')
|
||
|
|
||
|
def unsubscribed(self, jid):
|
||
|
if not app.account_is_available(self._account):
|
||
|
return
|
||
|
|
||
|
self._log.info('Unsubscribed: %s', jid)
|
||
|
self._jids_for_auto_auth.discard(jid)
|
||
|
self.send_presence(jid, 'unsubscribed')
|
||
|
|
||
|
def unsubscribe(self, jid, remove_auth=True):
|
||
|
if not app.account_is_available(self._account):
|
||
|
return
|
||
|
if remove_auth:
|
||
|
self._con.get_module('Roster').del_item(jid)
|
||
|
else:
|
||
|
self._log.info('Unsubscribe from %s', jid)
|
||
|
self._jids_for_auto_auth.discard(jid)
|
||
|
self._con.get_module('Roster').unsubscribe(jid)
|
||
|
self._con.get_module('Roster').set_item(jid)
|
||
|
|
||
|
def subscribe(self, jid, msg=None, name='', groups=None, auto_auth=False):
|
||
|
if not app.account_is_available(self._account):
|
||
|
return
|
||
|
if groups is None:
|
||
|
groups = []
|
||
|
|
||
|
self._log.info('Request Subscription to %s', jid)
|
||
|
|
||
|
if auto_auth:
|
||
|
self._jids_for_auto_auth.add(jid)
|
||
|
|
||
|
infos = {'jid': jid}
|
||
|
if name:
|
||
|
infos['name'] = name
|
||
|
iq = nbxmpp.Iq('set', Namespace.ROSTER)
|
||
|
query = iq.setQuery()
|
||
|
item = query.addChild('item', attrs=infos)
|
||
|
for group in groups:
|
||
|
item.addChild('group').setData(group)
|
||
|
self._con.connection.send(iq)
|
||
|
|
||
|
self.send_presence(jid,
|
||
|
'subscribe',
|
||
|
status=msg,
|
||
|
nick=app.nicks[self._account])
|
||
|
|
||
|
def get_presence(self, to=None, typ=None, priority=None,
|
||
|
show=None, status=None, nick=None, caps=True,
|
||
|
idle_time=False):
|
||
|
if show not in ('chat', 'away', 'xa', 'dnd'):
|
||
|
# Gajim sometimes passes invalid show values here
|
||
|
# until this is fixed this is a workaround
|
||
|
show = None
|
||
|
presence = nbxmpp.Presence(to, typ, priority, show, status)
|
||
|
if nick is not None:
|
||
|
nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
|
||
|
nick_tag.setData(nick)
|
||
|
|
||
|
if (idle_time and
|
||
|
app.is_installed('IDLE') and
|
||
|
app.settings.get('autoaway')):
|
||
|
idle_sec = idle.Monitor.get_idle_sec()
|
||
|
time_ = time.strftime('%Y-%m-%dT%H:%M:%SZ',
|
||
|
time.gmtime(time.time() - idle_sec))
|
||
|
|
||
|
idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
|
||
|
idle_node.setAttr('since', time_)
|
||
|
|
||
|
caps = self._con.get_module('Caps').caps
|
||
|
if caps is not None and typ != 'unavailable':
|
||
|
presence.setTag('c',
|
||
|
namespace=Namespace.CAPS,
|
||
|
attrs=caps._asdict())
|
||
|
|
||
|
return presence
|
||
|
|
||
|
def send_presence(self, *args, **kwargs):
|
||
|
if not app.account_is_connected(self._account):
|
||
|
return
|
||
|
presence = self.get_presence(*args, **kwargs)
|
||
|
app.plugin_manager.extension_point(
|
||
|
'send-presence', self._account, presence)
|
||
|
self._log.debug('Send presence:\n%s', presence)
|
||
|
self._con.connection.send(presence)
|
||
|
|
||
|
|
||
|
def get_instance(*args, **kwargs):
|
||
|
return Presence(*args, **kwargs), 'Presence'
|