summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonny Lamb <jonny.lamb@collabora.co.uk>2011-04-20 15:50:01 +0100
committerJonny Lamb <jonny.lamb@collabora.co.uk>2011-04-20 15:50:37 +0100
commit2ce6227468c403cc6dbbfb21d2dd0524d3cf9256 (patch)
treef77120baba032b0d66c4fffb75c42496e218b837
parentf712a7db97f646fbde443bad9f5246092a7c510e (diff)
caps_helper: copy newer version from gabble and everything it depends on
We need data form support. Signed-off-by: Jonny Lamb <jonny.lamb@collabora.co.uk>
-rw-r--r--tests/twisted/caps_helper.py337
-rw-r--r--tests/twisted/ns.py66
-rw-r--r--tests/twisted/salut/status.py2
-rw-r--r--tests/twisted/saluttest.py93
4 files changed, 469 insertions, 29 deletions
diff --git a/tests/twisted/caps_helper.py b/tests/twisted/caps_helper.py
index 76faadb..d18d109 100644
--- a/tests/twisted/caps_helper.py
+++ b/tests/twisted/caps_helper.py
@@ -1,42 +1,335 @@
+# vim: set fileencoding=utf-8 :
import hashlib
import base64
+import dbus
-from avahitest import txt_get_key
+from twisted.words.xish import domish, xpath
+from saluttest import make_result_iq, make_presence, elem_iq, elem
+from salutservicetest import (
+ EventPattern,
+ assertEquals, assertContains, assertDoesNotContain, assertLength,
+ )
+
+import config
import ns
+import salutconstants as cs
+
+# The caps we always have, regardless of any clients' caps
+FIXED_CAPS = [
+ ns.JINGLE,
+ ns.JINGLE_015,
+ ns.GOOGLE_FEAT_SESSION,
+ ns.JINGLE_TRANSPORT_RAWUDP,
+ ns.NICK,
+ ns.NICK + '+notify',
+ ns.CHAT_STATES,
+ ns.SI,
+ ns.IBB,
+ ns.BYTESTREAMS,
+ ]
+
+JINGLE_CAPS = [
+ # Additional Jingle transports
+ ns.JINGLE_TRANSPORT_ICEUDP,
+ ns.GOOGLE_P2P,
+ # Jingle content types
+ ns.GOOGLE_FEAT_VOICE,
+ ns.GOOGLE_FEAT_VIDEO,
+ ns.JINGLE_015_AUDIO,
+ ns.JINGLE_015_VIDEO,
+ ns.JINGLE_RTP,
+ ns.JINGLE_RTP_AUDIO,
+ ns.JINGLE_RTP_VIDEO,
+ ]
+
+VARIABLE_CAPS = (
+ JINGLE_CAPS +
+ [
+ ns.FILE_TRANSFER,
+
+ # FIXME: currently we always advertise these, but in future we should
+ # only advertise them if >= 1 client supports them:
+ # ns.TUBES,
+
+ # there is an unlimited set of these; only the ones actually relevant to
+ # the tests so far are shown here
+ ns.TUBES + '/stream#x-abiword',
+ ns.TUBES + '/stream#daap',
+ ns.TUBES + '/stream#http',
+ ns.TUBES + '/dbus#com.example.Go',
+ ns.TUBES + '/dbus#com.example.Xiangqi',
+ ])
+
+def check_caps(namespaces, desired):
+ """Assert that all the FIXED_CAPS are supported, and of the VARIABLE_CAPS,
+ every capability in desired is supported, and every other capability is
+ not.
+ """
+ for c in FIXED_CAPS:
+ assertContains(c, namespaces)
+
+ for c in VARIABLE_CAPS:
+ if c in desired:
+ assertContains(c, namespaces)
+ else:
+ assertDoesNotContain(c, namespaces)
+
+text_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT
+ })
+text_allowed_properties = dbus.Array([cs.TARGET_HANDLE])
+
+stream_tube_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAM_TUBE
+ })
+stream_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE,
+ cs.TARGET_ID, cs.STREAM_TUBE_SERVICE])
+
+dbus_tube_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_DBUS_TUBE
+ })
+dbus_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE,
+ cs.TARGET_ID, cs.DBUS_TUBE_SERVICE_NAME])
+
+ft_fixed_properties = dbus.Dictionary({
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER,
+ })
+ft_allowed_properties = dbus.Array([
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType',
+ cs.TARGET_HANDLE,
+ cs.TARGET_ID,
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentType',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Filename',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Size',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Description',
+ cs.CHANNEL_TYPE_FILE_TRANSFER + '.Date',
+ cs.FT_URI])
+
+fake_client_dataforms = {
+ 'urn:xmpp:dataforms:softwareinfo':
+ {'software': ['A Fake Client with Twisted'],
+ 'software_version': ['5.11.2-svn-20080512'],
+ 'os': ['Debian GNU/Linux unstable (sid) unstable sid'],
+ 'os_version': ['2.6.24-1-amd64'],
+ },
+}
def compute_caps_hash(identities, features, dataforms):
- S = ''
+ """
+ Accepts a list of slash-separated identities, a list of feature namespaces,
+ and a map from FORM_TYPE to (map from field name to values), returns the
+ verification string as defined by
+ <http://xmpp.org/extensions/xep-0115.html#ver>.
+ """
+ components = []
for identity in sorted(identities):
- S += '%s<' % identity
+ if len(identity.split('/')) != 4:
+ raise ValueError(
+ "expecting identities of the form " +
+ "'category/type/lang/client': got " + repr(identity))
+
+ components.append(identity)
for feature in sorted(features):
- S += '%s<' % feature
+ components.append(feature)
+
+ for form_type in sorted(dataforms.keys()):
+ components.append(form_type)
- # FIXME: support dataforms
+ for var in sorted(dataforms[form_type].keys()):
+ components.append(var)
+
+ for value in sorted(dataforms[form_type][var]):
+ components.append(value)
+
+ components.append('')
m = hashlib.sha1()
- m.update(S)
+ S = u'<'.join(components)
+ m.update(S.encode('utf-8'))
return base64.b64encode(m.digest())
-def check_caps(txt, ver):
- for (key, val) in { "1st": "test",
- "last": "suite",
- "status": "avail",
- "txtvers": "1" }.iteritems():
- v = txt_get_key(txt, key)
- assert v == val, (key, val, v)
+def make_caps_disco_reply(stream, req, identities, features, dataforms={}):
+ iq = make_result_iq(stream, req)
+ query = iq.firstChildElement()
+
+ for identity in identities:
+ category, type_, lang, name = identity.split('/')
+ el = query.addElement('identity')
+ el['category'] = category
+ el['type'] = type_
+ el['name'] = name
+
+ for f in features:
+ el = domish.Element((None, 'feature'))
+ el['var'] = f
+ query.addChild(el)
+
+ for type, fields in dataforms.iteritems():
+ x = query.addElement((ns.X_DATA, 'x'))
+ x['type'] = 'result'
+
+ field = x.addElement('field')
+ field['var'] = 'FORM_TYPE'
+ field['type'] = 'hidden'
+ field.addElement('value', content=type)
+
+ for var, values in fields.iteritems():
+ field = x.addElement('field')
+ field['var'] = var
+
+ for value in values:
+ field.addElement('value', content=value)
+
+ return iq
- assert txt_get_key(txt, "hash") == "sha-1"
- assert txt_get_key(txt, "node") == ns.TELEPATHY_CAPS
+def receive_presence_and_ask_caps(q, stream, expect_dbus=True):
+ # receive presence stanza
+ if expect_dbus:
+ presence, event_dbus = q.expect_many(
+ EventPattern('stream-presence'),
+ EventPattern('dbus-signal', signal='ContactCapabilitiesChanged')
+ )
+ assertLength(1, event_dbus.args)
+ signaled_caps = event_dbus.args[0]
+ else:
+ presence = q.expect('stream-presence')
+ signaled_caps = None
- v = txt_get_key(txt, "ver")
- assert v == ver, (v, ver)
+ return disco_caps(q, stream, presence) + (signaled_caps,)
+
+def disco_caps(q, stream, presence):
+ c_nodes = xpath.queryForNodes('/presence/c', presence.stanza)
+ assert c_nodes is not None
+ assertLength(1, c_nodes)
+ hash = c_nodes[0].attributes['hash']
+ ver = c_nodes[0].attributes['ver']
+ node = c_nodes[0].attributes['node']
+ assertEquals('sha-1', hash)
+
+ # ask caps
+ request = \
+ elem_iq(stream, 'get', from_='fake_contact@jabber.org/resource')(
+ elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver))
+ )
+ stream.send(request)
+
+ # receive caps
+ event = q.expect('stream-iq', query_ns=ns.DISCO_INFO, iq_id=request['id'])
+
+ # Check that Gabble's announcing the identity we think it should be.
+ identity_nodes = xpath.queryForNodes('/iq/query/identity', event.stanza)
+ assertLength(1, identity_nodes)
+ identity_node = identity_nodes[0]
+
+ assertEquals('client', identity_node['category'])
+ assertEquals(config.CLIENT_TYPE, identity_node['type'])
+ assertEquals(config.PACKAGE_STRING, identity_node['name'])
+ assertDoesNotContain('xml:lang', identity_node.attributes)
+
+ identity = 'client/%s//%s' % (config.CLIENT_TYPE, config.PACKAGE_STRING)
+
+ features = []
+ for feature in xpath.queryForNodes('/iq/query/feature', event.stanza):
+ features.append(feature['var'])
+
+ # Check if the hash matches the announced capabilities
+ assertEquals(compute_caps_hash([identity], features, {}), ver)
+
+ return (event, features)
+
+def caps_contain(event, cap):
+ node = xpath.queryForNodes('/iq/query/feature[@var="%s"]'
+ % cap,
+ event.stanza)
+ if node is None:
+ return False
+ if len(node) != 1:
+ return False
+ var = node[0].attributes['var']
+ if var is None:
+ return False
+ return var == cap
+
+def presence_and_disco(q, conn, stream, contact, disco,
+ client, caps,
+ features, identities=[], dataforms={},
+ initial=True, show=None):
+ h = send_presence(q, conn, stream, contact, caps, initial=initial,
+ show=show)
+
+ if disco:
+ stanza = expect_disco(q, contact, client, caps)
+ send_disco_reply(stream, stanza, identities, features, dataforms)
+
+ return h
+
+def send_presence(q, conn, stream, contact, caps, initial=True, show=None):
+ h = conn.RequestHandles(cs.HT_CONTACT, [contact])[0]
+
+ if initial:
+ stream.send(make_presence(contact, status='hello'))
+
+ q.expect_many(
+ EventPattern('dbus-signal', signal='PresenceUpdate',
+ args=[{h:
+ (0L, {u'available': {'message': 'hello'}})}]),
+ EventPattern('dbus-signal', signal='PresencesChanged',
+ args=[{h:
+ (2, u'available', 'hello')}]))
+
+ # no special capabilities
+ assertEquals([(h, cs.CHANNEL_TYPE_TEXT, 3, 0)],
+ conn.Capabilities.GetCapabilities([h]))
+
+ # send updated presence with caps info
+ stream.send(make_presence(contact, show=show, status='hello', caps=caps))
+
+ return h
+
+def expect_disco(q, contact, client, caps):
+ # Gabble looks up our capabilities
+ event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO)
+ assertEquals(client + '#' + caps['ver'], event.query['node'])
+
+ return event.stanza
+
+def send_disco_reply(stream, stanza, identities, features, dataforms={}):
+ stream.send(
+ make_caps_disco_reply(stream, stanza, identities, features, dataforms))
if __name__ == '__main__':
# example from XEP-0115
- assert compute_caps_hash(['client/pc//Exodus 0.9.1'],
- ["http://jabber.org/protocol/disco#info",
- "http://jabber.org/protocol/disco#items",
- "http://jabber.org/protocol/muc", "http://jabber.org/protocol/caps"],
- []) == 'QgayPKawpkPSDYmwT/WM94uAlu0='
+ assertEquals('QgayPKawpkPSDYmwT/WM94uAlu0=',
+ compute_caps_hash(['client/pc//Exodus 0.9.1'],
+ ["http://jabber.org/protocol/disco#info",
+ "http://jabber.org/protocol/disco#items",
+ "http://jabber.org/protocol/muc",
+ "http://jabber.org/protocol/caps"],
+ {}))
+
+ # another example from XEP-0115
+ identities = [u'client/pc/en/Psi 0.11', u'client/pc/el/Ψ 0.11']
+ features = [
+ u'http://jabber.org/protocol/caps',
+ u'http://jabber.org/protocol/disco#info',
+ u'http://jabber.org/protocol/disco#items',
+ u'http://jabber.org/protocol/muc',
+ ]
+ dataforms = {
+ u'urn:xmpp:dataforms:softwareinfo':
+ { u'ip_version': [u'ipv4', u'ipv6'],
+ u'os': [u'Mac'],
+ u'os_version': [u'10.5.1'],
+ u'software': [u'Psi'],
+ u'software_version': [u'0.11'],
+ },
+ }
+ assertEquals('q07IKJEyjvHSyhy//CH0CxmKi8w=',
+ compute_caps_hash(identities, features, dataforms))
diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py
index 07a78ca..fe9f6e2 100644
--- a/tests/twisted/ns.py
+++ b/tests/twisted/ns.py
@@ -1,11 +1,46 @@
AMP = "http://jabber.org/protocol/amp"
+BYTESTREAMS = 'http://jabber.org/protocol/bytestreams'
+CHAT_STATES = 'http://jabber.org/protocol/chatstates'
+CAPS = "http://jabber.org/protocol/caps"
DISCO_INFO = "http://jabber.org/protocol/disco#info"
DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
-IBB = "http://jabber.org/protocol/ibb"
-IQ_OOB = "jabber:iq:oob"
+FEATURE_NEG = 'http://jabber.org/protocol/feature-neg'
+FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer'
+GEOLOC = 'http://jabber.org/protocol/geoloc'
+GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session'
+GOOGLE_FEAT_SHARE = 'http://google.com/xmpp/protocol/share/v1'
+GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1'
+GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1'
+GOOGLE_JINGLE_INFO = 'google:jingleinfo'
+GOOGLE_P2P = "http://www.google.com/transport/p2p"
+GOOGLE_QUEUE = 'google:queue'
+GOOGLE_ROSTER = 'google:roster'
+GOOGLE_SESSION = "http://www.google.com/session"
+GOOGLE_SESSION_SHARE = "http://www.google.com/session/share"
+GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone"
+GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video"
+GOOGLE_MAIL_NOTIFY = "google:mail:notify"
+IBB = 'http://jabber.org/protocol/ibb'
+JINGLE_015 = "http://jabber.org/protocol/jingle"
+JINGLE_015_AUDIO = "http://jabber.org/protocol/jingle/description/audio"
+JINGLE_015_VIDEO = "http://jabber.org/protocol/jingle/description/video"
+JINGLE = "urn:xmpp:jingle:1"
+JINGLE_RTP = "urn:xmpp:jingle:apps:rtp:1"
+JINGLE_RTP_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"
+JINGLE_RTP_VIDEO = "urn:xmpp:jingle:apps:rtp:video"
+JINGLE_RTP_ERRORS = "urn:xmpp:jingle:apps:rtp:errors:1"
+JINGLE_RTP_INFO_1 = "urn:xmpp:jingle:apps:rtp:info:1"
+JINGLE_TRANSPORT_ICEUDP = "urn:xmpp:jingle:transports:ice-udp:1"
+JINGLE_TRANSPORT_RAWUDP = "urn:xmpp:jingle:transports:raw-udp:1"
MUC = 'http://jabber.org/protocol/muc'
+MUC_BYTESTREAM = 'http://telepathy.freedesktop.org/xmpp/protocol/muc-bytestream'
MUC_OWNER = '%s#owner' % MUC
MUC_USER = '%s#user' % MUC
+NICK = "http://jabber.org/protocol/nick"
+NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
+NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
+NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
+NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'
OLPC_ACTIVITIES = "http://laptop.org/xmpp/activities"
OLPC_ACTIVITIES_NOTIFY = "%s+notify" % OLPC_ACTIVITIES
OLPC_ACTIVITY = "http://laptop.org/xmpp/activity"
@@ -17,8 +52,27 @@ OLPC_BUDDY_PROPS_NOTIFY = "%s+notify" % OLPC_BUDDY_PROPS
OLPC_CURRENT_ACTIVITY = "http://laptop.org/xmpp/current-activity"
OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY
PUBSUB = "http://jabber.org/protocol/pubsub"
-SI = "http://jabber.org/protocol/si"
+PUBSUB_EVENT = "%s#event" % PUBSUB
+REGISTER = "jabber:iq:register"
+ROSTER = "jabber:iq:roster"
+SEARCH = 'jabber:iq:search'
+SI = 'http://jabber.org/protocol/si'
+SI_MULTIPLE = 'http://telepathy.freedesktop.org/xmpp/si-multiple'
STANZA = "urn:ietf:params:xml:ns:xmpp-stanzas"
-TELEPATHY_CAPS = 'http://telepathy.freedesktop.org/caps'
-TUBES = "http://telepathy.freedesktop.org/xmpp/tubes"
-X_OOB = "jabber:x:oob"
+STREAMS = "urn:ietf:params:xml:ns:xmpp-streams"
+TEMPPRES = "urn:xmpp:temppres:0"
+TUBES = 'http://telepathy.freedesktop.org/xmpp/tubes'
+MUJI = 'http://telepathy.freedesktop.org/xmpp/muji'
+VCARD_TEMP = 'vcard-temp'
+VCARD_TEMP_UPDATE = 'vcard-temp:x:update'
+X_DATA = 'jabber:x:data'
+X_DELAY = 'jabber:x:delay'
+XML = 'http://www.w3.org/XML/1998/namespace'
+X_OOB = 'jabber:x:oob'
+IQ_OOB = 'jabber:iq:oob'
+GABBLE_CAPS="http://telepathy.freedesktop.org/caps"
+PRESENCE_INVISIBLE = 'presence-invisible'
+PRIVACY = 'jabber:iq:privacy'
+INVISIBLE = 'urn:xmpp:invisible:0'
+GOOGLE_SHARED_STATUS = 'google:shared-status'
+VERSION = 'jabber:iq:version'
diff --git a/tests/twisted/salut/status.py b/tests/twisted/salut/status.py
index 6218d28..46e634b 100644
--- a/tests/twisted/salut/status.py
+++ b/tests/twisted/salut/status.py
@@ -63,7 +63,7 @@ def test(q, bus, conn):
# immediately.
# announce a contact with the right caps
- ver = compute_caps_hash([], [ycs.CAPABILITIES_PREFIX + CAP_NAME + '+notify'], [])
+ ver = compute_caps_hash([], [ycs.CAPABILITIES_PREFIX + CAP_NAME + '+notify'], {})
txt_record = { "txtvers": "1", "status": "avail",
"node": CLIENT_NAME, "ver": ver, "hash": "sha-1"}
contact_name = "test-status@" + get_host_name()
diff --git a/tests/twisted/saluttest.py b/tests/twisted/saluttest.py
index 7de7039..0193ab7 100644
--- a/tests/twisted/saluttest.py
+++ b/tests/twisted/saluttest.py
@@ -208,3 +208,96 @@ def wait_for_contact_in_publish(q, bus, conn, contact_name):
handle = h
return handle
+
+def elem(a, b=None, attrs={}, **kw):
+ r"""
+ >>> elem('foo')().toXml()
+ u'<foo/>'
+ >>> elem('foo', x='1')().toXml()
+ u"<foo x='1'/>"
+ >>> elem('foo', x='1')(u'hello').toXml()
+ u"<foo x='1'>hello</foo>"
+ >>> elem('foo', x='1')(u'hello',
+ ... elem('http://foo.org', 'bar', y='2')(u'bye')).toXml()
+ u"<foo x='1'>hello<bar xmlns='http://foo.org' y='2'>bye</bar></foo>"
+ >>> elem('foo', attrs={'xmlns:bar': 'urn:bar', 'bar:cake': 'yum'})(
+ ... elem('bar:e')(u'i')
+ ... ).toXml()
+ u"<foo xmlns:bar='urn:bar' bar:cake='yum'><bar:e>i</bar:e></foo>"
+ """
+
+ class _elem(domish.Element):
+ def __call__(self, *children):
+ _elem_add(self, *children)
+ return self
+
+ if b is not None:
+ elem = _elem((a, b))
+ else:
+ elem = _elem((None, a))
+
+ # Can't just update kw into attrs, because that *modifies the parameter's
+ # default*. Thanks python.
+ allattrs = {}
+ allattrs.update(kw)
+ allattrs.update(attrs)
+
+ # First, let's pull namespaces out
+ realattrs = {}
+ for k, v in allattrs.iteritems():
+ if k.startswith('xmlns:'):
+ abbr = k[len('xmlns:'):]
+ elem.localPrefixes[abbr] = v
+ else:
+ realattrs[k] = v
+
+ for k, v in realattrs.iteritems():
+ if k == 'from_':
+ elem['from'] = v
+ else:
+ elem[k] = v
+
+ return elem
+
+def elem_iq(server, type, **kw):
+ class _iq(IQ):
+ def __call__(self, *children):
+ _elem_add(self, *children)
+ return self
+
+ iq = _iq(server, type)
+
+ for k, v in kw.iteritems():
+ if k == 'from_':
+ iq['from'] = v
+ else:
+ iq[k] = v
+
+ return iq
+
+def make_presence(_from, to, type=None, show=None,
+ status=None, caps=None, photo=None):
+ presence = domish.Element((None, 'presence'))
+ presence['from'] = _from
+ presence['to'] = to
+
+ if type is not None:
+ presence['type'] = type
+
+ if show is not None:
+ presence.addElement('show', content=show)
+
+ if status is not None:
+ presence.addElement('status', content=status)
+
+ if caps is not None:
+ cel = presence.addElement(('http://jabber.org/protocol/caps', 'c'))
+ for key,value in caps.items():
+ cel[key] = value
+
+ # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x>
+ if photo is not None:
+ x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x'))
+ x.addElement('photo').addContent(photo)
+
+ return presence