def expect_caps(q, conn, h): # we can now do audio and video calls cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged', predicate=lambda e: h in e.args[0]), ) assert_rccs_callable(cc.args[0][h], require_video=True) check_caps(conn, h)
def test(q, bus, conn, stream): client = 'http://example.com/perverse-client' contact_bare_jid = '*****@*****.**' contact_with_resource = '[email protected]/hi' contact_handle = conn.get_contact_handle_sync(contact_bare_jid) # Gabble gets a presence stanza from a bare JID, which is a tad surprising. features = [ ns.JINGLE_015, ns.JINGLE_015_AUDIO, ns.JINGLE_015_VIDEO, ns.GOOGLE_P2P, ] caps = { 'node': client, 'hash': 'sha-1', 'ver': compute_caps_hash([], features, {}), } p = make_presence(contact_bare_jid, status='Hello', caps=caps) stream.send(p) # Gabble looks up the hash event = q.expect('stream-iq', to=contact_bare_jid, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assertEquals(client + '#' + caps['ver'], query_node.attributes['node']) # The bare jid replies send_disco_reply(stream, event.stanza, [], features) # Gabble lets us know their caps have changed. (Gabble used to ignore the # reply.) cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][contact_handle]) # Gabble gets another presence stanza from the bare JID, with different # caps. features.append(ns.TUBES) caps = { 'node': client, 'hash': 'sha-1', 'ver': compute_caps_hash([], features, {}), } p = make_presence(contact_bare_jid, status='Get out the abacus', caps=caps) stream.send(p) # Gabble looks up the new hash disco2 = q.expect('stream-iq', to=contact_bare_jid, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', disco2.stanza)[0] assertEquals(client + '#' + caps['ver'], query_node.attributes['node']) # This time, before the bare JID replies, Gabble gets a presence from the # resourceful jid. features_ = features + [ns.CHAT_STATES] caps = { 'node': client, 'hash': 'sha-1', 'ver': compute_caps_hash([], features_, {}), } p = make_presence(contact_with_resource, status='Count this', caps=caps) stream.send(p) # Gabble throws away presence from the bare JID when it gets presence from # a resource (and vice versa), so it should now say the contact is # incapable. Gabble also looks up the resourceful JID's hash. cc, disco3 = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), EventPattern('stream-iq', to=contact_with_resource, query_ns='http://jabber.org/protocol/disco#info'), ) assert_rccs_not_callable(cc.args[0][contact_handle]) query_node = xpath.queryForNodes('/iq/query', disco3.stanza)[0] assertEquals(client + '#' + caps['ver'], query_node.attributes['node']) # The bare jid replies! Getting a disco reply from a bare JID when we've # got presence from resources used to crash Gabble, but now it just ignores # it. send_disco_reply(stream, disco2.stanza, [], features) # Now the resourceful JID replies: send_disco_reply(stream, disco3.stanza, [], features_) # Gabble should announce that the contact has acquired some caps. cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][contact_handle])
def test_hash(q, bus, conn, stream, contact, contact_handle, client): presence = make_presence(contact, status='hello') stream.send(presence) q.expect('dbus-signal', signal='PresencesChanged', args=[{contact_handle: (2, u'available', 'hello')}]) # no special capabilities for rcc in get_contacts_capabilities_sync(conn, [contact_handle])[contact_handle]: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) # send updated presence with Jingle caps info presence = make_presence(contact, status='hello', caps={'node': client, 'ver': '0.1', }) stream.send(presence) # Gabble looks up our capabilities event = q.expect('stream-iq', to=contact, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assert query_node.attributes['node'] == \ client + '#' + '0.1' # send good reply send_disco_reply(stream, event.stanza, [], jingle_av_features) # we can now do audio calls cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][contact_handle]) assertEquals(cc.args[0], get_contacts_capabilities_sync(conn, [contact_handle])) # Send presence without any capabilities. XEP-0115 §8.4 Caps Optimization # says “receivers of presence notifications MUST NOT expect an annotation # on every presence notification they receive”, so the contact should still # be media-capable afterwards. stream.send(make_presence(contact, status='very capable')) q.expect('dbus-signal', signal='PresencesChanged', args=[{contact_handle: (2, u'available', 'very capable')}]) # still exactly the same capabilities assertEquals(cc.args[0], get_contacts_capabilities_sync(conn, [contact_handle])) # send bogus presence caps = { 'node': client, 'ver': 'ceci=nest=pas=un=hash', 'hash': 'sha-1', } presence = make_presence(contact, status='hello', caps=caps) stream.send(presence) # Gabble looks up our capabilities event = q.expect('stream-iq', to=contact, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assert query_node.attributes['node'] == \ client + '#' + caps['ver'] # send bogus reply send_disco_reply(stream, event.stanza, [], ['http://jabber.org/protocol/bogus-feature']) # don't receive any D-Bus signal forbidden = [ EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ] q.forbid_events(forbidden) sync_dbus(bus, q, conn) sync_stream(q, stream) # send presence with empty caps presence = make_presence(contact, status='hello', caps={'node': client, 'ver': '0.0', }) stream.send(presence) # Gabble looks up our capabilities event = q.expect('stream-iq', to=contact, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assert query_node.attributes['node'] == \ client + '#' + '0.0' # still don't receive any D-Bus signal sync_dbus(bus, q, conn) # send good reply q.unforbid_events(forbidden) result = make_result_iq(stream, event.stanza) query = result.firstChildElement() stream.send(result) # we can now do nothing cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) for rcc in cc.args[0][contact_handle]: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) assert_rccs_not_callable(cc.args[0][contact_handle]) assertEquals(cc.args[0], get_contacts_capabilities_sync(conn, [contact_handle])) # send correct presence ver = compute_caps_hash(some_identities, jingle_av_features, fake_client_dataforms) caps = { 'node': client, 'ver': ver, 'hash': 'sha-1', } presence = make_presence(contact, status='hello', caps=caps) stream.send(presence) # Gabble looks up our capabilities event = q.expect('stream-iq', to=contact, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assert query_node.attributes['node'] == \ client + '#' + caps['ver'] # don't receive any D-Bus signal q.forbid_events(forbidden) sync_dbus(bus, q, conn) q.unforbid_events(forbidden) # send good reply send_disco_reply( stream, event.stanza, some_identities, jingle_av_features, fake_client_dataforms) # we can now do audio calls cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][contact_handle]) assertEquals(cc.args[0], get_contacts_capabilities_sync(conn, [contact_handle]))
def test_two_clients(q, bus, conn, stream, contact1, contact2, contact_handle1, contact_handle2, client, broken_hash): presence = make_presence(contact1, status='hello') stream.send(presence) q.expect('dbus-signal', signal='PresencesChanged', args=[{contact_handle1: (2, u'available', 'hello')}]) presence = make_presence(contact2, status='hello') stream.send(presence) q.expect('dbus-signal', signal='PresencesChanged', args=[{contact_handle2: (2, u'available', 'hello')}]) # no special capabilities for h in (contact_handle1, contact_handle2): for rcc in get_contacts_capabilities_sync(conn, [h])[h]: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) # send updated presence with Jingle caps info ver = compute_caps_hash(some_identities, jingle_av_features, {}) caps = { 'node': client, 'ver': ver, 'hash': 'sha-1', } presence = make_presence(contact1, status='hello', caps=caps) stream.send(presence) presence = make_presence(contact2, status='hello', caps=caps) stream.send(presence) # Gabble looks up our capabilities event = q.expect('stream-iq', to=contact1, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assert query_node.attributes['node'] == \ client + '#' + ver # don't receive any D-Bus signal forbidden = [ EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ] q.forbid_events(forbidden) sync_dbus(bus, q, conn) q.unforbid_events(forbidden) result = make_caps_disco_reply( stream, event.stanza, some_identities, jingle_av_features) if broken_hash: # make the hash break! query = result.firstChildElement() query.addElement('feature')['var'] = 'http://example.com/another-feature' stream.send(result) if broken_hash: # Gabble looks up our capabilities again because the first contact # failed to provide a valid hash event = q.expect('stream-iq', to=contact2, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assert query_node.attributes['node'] == \ client + '#' + ver # don't receive any D-Bus signal q.forbid_events(forbidden) sync_dbus(bus, q, conn) q.unforbid_events(forbidden) # send good reply send_disco_reply(stream, event.stanza, some_identities, jingle_av_features) # we can now do audio calls cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged', predicate=lambda e: contact_handle2 in e.args[0]), ) assert_rccs_callable(cc.args[0][contact_handle2]) if not broken_hash: # if the first contact failed to provide a good hash, it does not # deserve its capabilities to be understood by Gabble! cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged', predicate=lambda e: contact_handle1 in e.args[0]), ) assert_rccs_callable(cc.args[0][contact_handle1]) # don't receive any further signals q.forbid_events(forbidden) sync_dbus(bus, q, conn) q.unforbid_events(forbidden)
def test(q, bus, conn, stream): bob = conn.get_contact_handle_sync('*****@*****.**') presence = make_presence('[email protected]/Foo', status='hello') stream.send(presence) q.expect('dbus-signal', signal='PresencesChanged', args=[{bob: (cs.PRESENCE_AVAILABLE, u'available', 'hello')}]) basic_caps = [(bob, cs.CHANNEL_TYPE_TEXT, 3, 0)] # only Text for rcc in get_contacts_capabilities_sync(conn, [bob])[bob]: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) # holding the handle here: see below assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: get_contacts_capabilities_sync(conn, [bob])[bob], cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # send updated presence with Jingle audio/video caps info. we turn on both # audio and video at the same time to test that all of the capabilities are # discovered before any capabilities change signal is emitted presence = make_presence('[email protected]/Foo', status='hello', caps={ 'node': 'http://telepathy.freedesktop.org/fake-client', 'ver' : '0.1', 'ext' : 'video', }) stream.send(presence) # Gabble looks up both the version and the video bundles, in any order (version_event, video_event) = q.expect_many( EventPattern('stream-iq', to='[email protected]/Foo', query_ns='http://jabber.org/protocol/disco#info', query_node='http://telepathy.freedesktop.org/fake-client#0.1'), EventPattern('stream-iq', to='[email protected]/Foo', query_ns='http://jabber.org/protocol/disco#info', query_node='http://telepathy.freedesktop.org/fake-client#video')) # reply to the video bundle query first - this capability alone is not # sufficient to make us callable result = make_result_iq(stream, video_event.stanza) query = result.firstChildElement() feature = query.addElement('feature') feature['var'] = 'http://jabber.org/protocol/jingle/description/video' stream.send(result) # reply to the version bundle query, which should make us audio and # video callable result = make_result_iq(stream, version_event.stanza) query = result.firstChildElement() feature = query.addElement('feature') feature['var'] = 'http://jabber.org/protocol/jingle' feature = query.addElement('feature') feature['var'] = 'http://jabber.org/protocol/jingle/description/audio' feature = query.addElement('feature') feature['var'] = 'http://www.google.com/transport/p2p' stream.send(result) # we can now do audio and video calls cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged', predicate=lambda e: check_rccs_callable(e.args[0][bob])), ) assert_rccs_callable(cc.args[0][bob], require_video=True, mutable_contents=True) assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: cc.args[0][bob], cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # send updated presence without video support presence = make_presence('[email protected]/Foo', status='hello', caps={ 'node': 'http://telepathy.freedesktop.org/fake-client', 'ver' : '0.1', }) stream.send(presence) # we can now do only audio calls (and as a result have the ImmutableStreams # cap) cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][bob]) assert_rccs_not_callable(cc.args[0][bob], require_audio=False, require_video=True, mutable_contents=False) assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: cc.args[0][bob], cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # go offline presence = make_presence('[email protected]/Foo', type='unavailable') stream.send(presence) # can't do audio calls any more q.expect_many( EventPattern('dbus-signal', signal='PresencesChanged', args=[{bob: (cs.PRESENCE_OFFLINE, 'offline', '')}]), EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) # Contact went offline. Previously, this test asserted that the handle # became invalid, but that's not guaranteed to happen immediately; so we # now hold the handle (above), to guarantee that it does *not* become # invalid. rccs = get_contacts_capabilities_sync(conn, [bob])[bob] for rcc in rccs: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: rccs, cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # What about a handle that's not valid? assertEquals({}, conn.Contacts.GetContactAttributes( [31337], [cs.CONN_IFACE_CONTACT_CAPS], False))
def test(q, bus, conn, stream): bob = conn.get_contact_handle_sync('*****@*****.**') presence = make_presence('[email protected]/Foo', status='hello') stream.send(presence) q.expect('dbus-signal', signal='PresencesChanged', args=[{ bob: (cs.PRESENCE_AVAILABLE, u'available', 'hello') }]) basic_caps = [(bob, cs.CHANNEL_TYPE_TEXT, 3, 0)] # only Text for rcc in get_contacts_capabilities_sync(conn, [bob])[bob]: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) # holding the handle here: see below assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: get_contacts_capabilities_sync(conn, [bob])[bob], cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # send updated presence with Jingle audio/video caps info. we turn on both # audio and video at the same time to test that all of the capabilities are # discovered before any capabilities change signal is emitted presence = make_presence( '[email protected]/Foo', status='hello', caps={ 'node': 'http://telepathy.freedesktop.org/fake-client', 'ver': '0.1', 'ext': 'video', }) stream.send(presence) # Gabble looks up both the version and the video bundles, in any order (version_event, video_event) = q.expect_many( EventPattern( 'stream-iq', to='[email protected]/Foo', query_ns='http://jabber.org/protocol/disco#info', query_node='http://telepathy.freedesktop.org/fake-client#0.1'), EventPattern( 'stream-iq', to='[email protected]/Foo', query_ns='http://jabber.org/protocol/disco#info', query_node='http://telepathy.freedesktop.org/fake-client#video')) # reply to the video bundle query first - this capability alone is not # sufficient to make us callable result = make_result_iq(stream, video_event.stanza) query = result.firstChildElement() feature = query.addElement('feature') feature['var'] = 'http://jabber.org/protocol/jingle/description/video' stream.send(result) # reply to the version bundle query, which should make us audio and # video callable result = make_result_iq(stream, version_event.stanza) query = result.firstChildElement() feature = query.addElement('feature') feature['var'] = 'http://jabber.org/protocol/jingle' feature = query.addElement('feature') feature['var'] = 'http://jabber.org/protocol/jingle/description/audio' feature = query.addElement('feature') feature['var'] = 'http://www.google.com/transport/p2p' stream.send(result) # we can now do audio and video calls cc, = q.expect_many( EventPattern( 'dbus-signal', signal='ContactCapabilitiesChanged', predicate=lambda e: check_rccs_callable(e.args[0][bob])), ) assert_rccs_callable(cc.args[0][bob], require_video=True, mutable_contents=True) assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: cc.args[0][bob], cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # send updated presence without video support presence = make_presence( '[email protected]/Foo', status='hello', caps={ 'node': 'http://telepathy.freedesktop.org/fake-client', 'ver': '0.1', }) stream.send(presence) # we can now do only audio calls (and as a result have the ImmutableStreams # cap) cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][bob]) assert_rccs_not_callable(cc.args[0][bob], require_audio=False, require_video=True, mutable_contents=False) assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: cc.args[0][bob], cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # go offline presence = make_presence('[email protected]/Foo', type='unavailable') stream.send(presence) # can't do audio calls any more q.expect_many( EventPattern('dbus-signal', signal='PresencesChanged', args=[{ bob: (cs.PRESENCE_OFFLINE, 'offline', '') }]), EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) # Contact went offline. Previously, this test asserted that the handle # became invalid, but that's not guaranteed to happen immediately; so we # now hold the handle (above), to guarantee that it does *not* become # invalid. rccs = get_contacts_capabilities_sync(conn, [bob])[bob] for rcc in rccs: assertEquals(cs.CHANNEL_TYPE_TEXT, rcc[0].get(cs.CHANNEL_TYPE)) assertEquals( { bob: { cs.ATTR_CONTACT_CAPABILITIES: rccs, cs.CONN + '/contact-id': '*****@*****.**', }, }, conn.Contacts.GetContactAttributes([bob], [cs.CONN_IFACE_CONTACT_CAPS], True)) # What about a handle that's not valid? assertEquals({}, conn.Contacts.GetContactAttributes( [31337], [cs.CONN_IFACE_CONTACT_CAPS], False))
def check_caps(conn, h): caps = get_contacts_capabilities_sync(conn, [h]) assert_rccs_callable(caps[h], require_video=True)
def test(q, bus, conn, stream): client = 'http://example.com/perverse-client' contact_bare_jid = '*****@*****.**' contact_with_resource = '[email protected]/hi' contact_handle = conn.get_contact_handle_sync(contact_bare_jid) # Gabble gets a presence stanza from a bare JID, which is a tad surprising. features = [ ns.JINGLE_015, ns.JINGLE_015_AUDIO, ns.JINGLE_015_VIDEO, ns.GOOGLE_P2P, ] caps = {'node': client, 'hash': 'sha-1', 'ver': compute_caps_hash([], features, {}), } p = make_presence(contact_bare_jid, status='Hello', caps=caps) stream.send(p) # Gabble looks up the hash event = q.expect('stream-iq', to=contact_bare_jid, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] assertEquals(client + '#' + caps['ver'], query_node.attributes['node']) # The bare jid replies send_disco_reply(stream, event.stanza, [], features) # Gabble lets us know their caps have changed. (Gabble used to ignore the # reply.) cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][contact_handle]) # Gabble gets another presence stanza from the bare JID, with different # caps. features.append(ns.TUBES) caps = {'node': client, 'hash': 'sha-1', 'ver': compute_caps_hash([], features, {}), } p = make_presence(contact_bare_jid, status='Get out the abacus', caps=caps) stream.send(p) # Gabble looks up the new hash disco2 = q.expect('stream-iq', to=contact_bare_jid, query_ns='http://jabber.org/protocol/disco#info') query_node = xpath.queryForNodes('/iq/query', disco2.stanza)[0] assertEquals(client + '#' + caps['ver'], query_node.attributes['node']) # This time, before the bare JID replies, Gabble gets a presence from the # resourceful jid. features_ = features + [ns.CHAT_STATES] caps = {'node': client, 'hash': 'sha-1', 'ver': compute_caps_hash([], features_, {}), } p = make_presence(contact_with_resource, status='Count this', caps=caps) stream.send(p) # Gabble throws away presence from the bare JID when it gets presence from # a resource (and vice versa), so it should now say the contact is # incapable. Gabble also looks up the resourceful JID's hash. cc, disco3 = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), EventPattern('stream-iq', to=contact_with_resource, query_ns='http://jabber.org/protocol/disco#info'), ) assert_rccs_not_callable(cc.args[0][contact_handle]) query_node = xpath.queryForNodes('/iq/query', disco3.stanza)[0] assertEquals(client + '#' + caps['ver'], query_node.attributes['node']) # The bare jid replies! Getting a disco reply from a bare JID when we've # got presence from resources used to crash Gabble, but now it just ignores # it. send_disco_reply(stream, disco2.stanza, [], features) # Now the resourceful JID replies: send_disco_reply(stream, disco3.stanza, [], features_) # Gabble should announce that the contact has acquired some caps. cc, = q.expect_many( EventPattern('dbus-signal', signal='ContactCapabilitiesChanged'), ) assert_rccs_callable(cc.args[0][contact_handle])
def capabilities_changed(q, contact_handle): e = q.expect('dbus-signal', signal='ContactCapabilitiesChanged') assertContains(contact_handle, e.args[0]) assert_rccs_callable(e.args[0][contact_handle], require_video=True) assertContains(xiangqi_tube_cap, e.args[0][contact_handle])