summaryrefslogtreecommitdiff
path: root/slixmpp
diff options
context:
space:
mode:
Diffstat (limited to 'slixmpp')
-rw-r--r--slixmpp/__init__.py26
-rw-r--r--slixmpp/api.py200
-rw-r--r--slixmpp/basexmpp.py793
-rw-r--r--slixmpp/clientxmpp.py305
-rw-r--r--slixmpp/componentxmpp.py147
-rw-r--r--slixmpp/exceptions.py103
-rw-r--r--slixmpp/features/__init__.py16
-rw-r--r--slixmpp/features/feature_bind/__init__.py19
-rw-r--r--slixmpp/features/feature_bind/bind.py67
-rw-r--r--slixmpp/features/feature_bind/stanza.py21
-rw-r--r--slixmpp/features/feature_mechanisms/__init__.py22
-rw-r--r--slixmpp/features/feature_mechanisms/mechanisms.py249
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/__init__.py16
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/abort.py24
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/auth.py49
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/challenge.py39
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/failure.py76
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/mechanisms.py53
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/response.py39
-rw-r--r--slixmpp/features/feature_mechanisms/stanza/success.py38
-rw-r--r--slixmpp/features/feature_preapproval/__init__.py15
-rw-r--r--slixmpp/features/feature_preapproval/preapproval.py42
-rw-r--r--slixmpp/features/feature_preapproval/stanza.py17
-rw-r--r--slixmpp/features/feature_rosterver/__init__.py19
-rw-r--r--slixmpp/features/feature_rosterver/rosterver.py42
-rw-r--r--slixmpp/features/feature_rosterver/stanza.py17
-rw-r--r--slixmpp/features/feature_session/__init__.py19
-rw-r--r--slixmpp/features/feature_session/session.py54
-rw-r--r--slixmpp/features/feature_session/stanza.py20
-rw-r--r--slixmpp/features/feature_starttls/__init__.py19
-rw-r--r--slixmpp/features/feature_starttls/stanza.py45
-rw-r--r--slixmpp/features/feature_starttls/starttls.py65
-rw-r--r--slixmpp/jid.py449
-rw-r--r--slixmpp/plugins/__init__.py88
-rw-r--r--slixmpp/plugins/base.py352
-rw-r--r--slixmpp/plugins/gmail_notify.py149
-rw-r--r--slixmpp/plugins/google/auth/stanza.py47
-rw-r--r--slixmpp/plugins/google/gmail/notifications.py90
-rw-r--r--slixmpp/plugins/google/nosave/stanza.py59
-rw-r--r--slixmpp/plugins/google/settings/settings.py63
-rw-r--r--slixmpp/plugins/xep_0004/__init__.py22
-rw-r--r--slixmpp/plugins/xep_0004/dataforms.py57
-rw-r--r--slixmpp/plugins/xep_0004/stanza/__init__.py10
-rw-r--r--slixmpp/plugins/xep_0004/stanza/field.py185
-rw-r--r--slixmpp/plugins/xep_0004/stanza/form.py275
-rw-r--r--slixmpp/plugins/xep_0009/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0009/binding.py169
-rw-r--r--slixmpp/plugins/xep_0009/remote.py772
-rw-r--r--slixmpp/plugins/xep_0009/rpc.py223
-rw-r--r--slixmpp/plugins/xep_0009/stanza/RPC.py64
-rw-r--r--slixmpp/plugins/xep_0009/stanza/__init__.py9
-rw-r--r--slixmpp/plugins/xep_0012/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0012/last_activity.py156
-rw-r--r--slixmpp/plugins/xep_0012/stanza.py32
-rw-r--r--slixmpp/plugins/xep_0013/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0013/offline.py124
-rw-r--r--slixmpp/plugins/xep_0013/stanza.py53
-rw-r--r--slixmpp/plugins/xep_0016/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0016/privacy.py127
-rw-r--r--slixmpp/plugins/xep_0016/stanza.py103
-rw-r--r--slixmpp/plugins/xep_0020/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0020/feature_negotiation.py36
-rw-r--r--slixmpp/plugins/xep_0020/stanza.py17
-rw-r--r--slixmpp/plugins/xep_0027/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0027/gpg.py169
-rw-r--r--slixmpp/plugins/xep_0027/stanza.py53
-rw-r--r--slixmpp/plugins/xep_0030/__init__.py22
-rw-r--r--slixmpp/plugins/xep_0030/disco.py748
-rw-r--r--slixmpp/plugins/xep_0030/stanza/__init__.py10
-rw-r--r--slixmpp/plugins/xep_0030/stanza/info.py276
-rw-r--r--slixmpp/plugins/xep_0030/stanza/items.py152
-rw-r--r--slixmpp/plugins/xep_0030/static.py430
-rw-r--r--slixmpp/plugins/xep_0033/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0033/addresses.py37
-rw-r--r--slixmpp/plugins/xep_0033/stanza.py131
-rw-r--r--slixmpp/plugins/xep_0045.py418
-rw-r--r--slixmpp/plugins/xep_0047/__init__.py21
-rw-r--r--slixmpp/plugins/xep_0047/ibb.py183
-rw-r--r--slixmpp/plugins/xep_0047/stanza.py70
-rw-r--r--slixmpp/plugins/xep_0047/stream.py128
-rw-r--r--slixmpp/plugins/xep_0048/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0048/bookmarks.py76
-rw-r--r--slixmpp/plugins/xep_0048/stanza.py65
-rw-r--r--slixmpp/plugins/xep_0049/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0049/private_storage.py57
-rw-r--r--slixmpp/plugins/xep_0049/stanza.py17
-rw-r--r--slixmpp/plugins/xep_0050/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0050/adhoc.py665
-rw-r--r--slixmpp/plugins/xep_0050/stanza.py185
-rw-r--r--slixmpp/plugins/xep_0054/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0054/stanza.py571
-rw-r--r--slixmpp/plugins/xep_0054/vcard_temp.py147
-rw-r--r--slixmpp/plugins/xep_0059/__init__.py18
-rw-r--r--slixmpp/plugins/xep_0059/rsm.py145
-rw-r--r--slixmpp/plugins/xep_0059/stanza.py108
-rw-r--r--slixmpp/plugins/xep_0060/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0060/pubsub.py566
-rw-r--r--slixmpp/plugins/xep_0060/stanza/__init__.py12
-rw-r--r--slixmpp/plugins/xep_0060/stanza/base.py29
-rw-r--r--slixmpp/plugins/xep_0060/stanza/pubsub.py272
-rw-r--r--slixmpp/plugins/xep_0060/stanza/pubsub_errors.py86
-rw-r--r--slixmpp/plugins/xep_0060/stanza/pubsub_event.py151
-rw-r--r--slixmpp/plugins/xep_0060/stanza/pubsub_owner.py134
-rw-r--r--slixmpp/plugins/xep_0065/__init__.py8
-rw-r--r--slixmpp/plugins/xep_0065/proxy.py279
-rw-r--r--slixmpp/plugins/xep_0065/socks5.py265
-rw-r--r--slixmpp/plugins/xep_0065/stanza.py47
-rw-r--r--slixmpp/plugins/xep_0066/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0066/oob.py158
-rw-r--r--slixmpp/plugins/xep_0066/stanza.py33
-rw-r--r--slixmpp/plugins/xep_0071/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0071/stanza.py81
-rw-r--r--slixmpp/plugins/xep_0071/xhtml_im.py30
-rw-r--r--slixmpp/plugins/xep_0077/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0077/register.py114
-rw-r--r--slixmpp/plugins/xep_0077/stanza.py73
-rw-r--r--slixmpp/plugins/xep_0078/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0078/legacyauth.py139
-rw-r--r--slixmpp/plugins/xep_0078/stanza.py41
-rw-r--r--slixmpp/plugins/xep_0079/__init__.py18
-rw-r--r--slixmpp/plugins/xep_0079/amp.py79
-rw-r--r--slixmpp/plugins/xep_0079/stanza.py96
-rw-r--r--slixmpp/plugins/xep_0080/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0080/geoloc.py121
-rw-r--r--slixmpp/plugins/xep_0080/stanza.py266
-rw-r--r--slixmpp/plugins/xep_0082.py228
-rw-r--r--slixmpp/plugins/xep_0084/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0084/avatar.py110
-rw-r--r--slixmpp/plugins/xep_0084/stanza.py78
-rw-r--r--slixmpp/plugins/xep_0085/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0085/chat_states.py56
-rw-r--r--slixmpp/plugins/xep_0085/stanza.py94
-rw-r--r--slixmpp/plugins/xep_0086/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0086/legacy_error.py46
-rw-r--r--slixmpp/plugins/xep_0086/stanza.py91
-rw-r--r--slixmpp/plugins/xep_0091/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0091/legacy_delay.py29
-rw-r--r--slixmpp/plugins/xep_0091/stanza.py47
-rw-r--r--slixmpp/plugins/xep_0092/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0092/stanza.py42
-rw-r--r--slixmpp/plugins/xep_0092/version.py87
-rw-r--r--slixmpp/plugins/xep_0095/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0095/stanza.py25
-rw-r--r--slixmpp/plugins/xep_0095/stream_initiation.py213
-rw-r--r--slixmpp/plugins/xep_0096/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0096/file_transfer.py59
-rw-r--r--slixmpp/plugins/xep_0096/stanza.py48
-rw-r--r--slixmpp/plugins/xep_0106.py26
-rw-r--r--slixmpp/plugins/xep_0107/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0107/stanza.py55
-rw-r--r--slixmpp/plugins/xep_0107/user_mood.py85
-rw-r--r--slixmpp/plugins/xep_0108/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0108/stanza.py83
-rw-r--r--slixmpp/plugins/xep_0108/user_activity.py82
-rw-r--r--slixmpp/plugins/xep_0115/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0115/caps.py334
-rw-r--r--slixmpp/plugins/xep_0115/stanza.py19
-rw-r--r--slixmpp/plugins/xep_0115/static.py146
-rw-r--r--slixmpp/plugins/xep_0118/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0118/stanza.py25
-rw-r--r--slixmpp/plugins/xep_0118/user_tune.py92
-rw-r--r--slixmpp/plugins/xep_0122/__init__.py11
-rw-r--r--slixmpp/plugins/xep_0122/data_validation.py19
-rw-r--r--slixmpp/plugins/xep_0122/stanza.py93
-rw-r--r--slixmpp/plugins/xep_0128/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0128/extended_disco.py99
-rw-r--r--slixmpp/plugins/xep_0128/static.py73
-rw-r--r--slixmpp/plugins/xep_0131/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0131/headers.py41
-rw-r--r--slixmpp/plugins/xep_0131/stanza.py51
-rw-r--r--slixmpp/plugins/xep_0133.py53
-rw-r--r--slixmpp/plugins/xep_0138.py145
-rw-r--r--slixmpp/plugins/xep_0152/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0152/reachability.py90
-rw-r--r--slixmpp/plugins/xep_0152/stanza.py29
-rw-r--r--slixmpp/plugins/xep_0153/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0153/stanza.py29
-rw-r--r--slixmpp/plugins/xep_0153/vcard_avatar.py178
-rw-r--r--slixmpp/plugins/xep_0163.py120
-rw-r--r--slixmpp/plugins/xep_0172/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0172/stanza.py67
-rw-r--r--slixmpp/plugins/xep_0172/user_nick.py83
-rw-r--r--slixmpp/plugins/xep_0184/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0184/receipt.py129
-rw-r--r--slixmpp/plugins/xep_0184/stanza.py72
-rw-r--r--slixmpp/plugins/xep_0186/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0186/invisible_command.py44
-rw-r--r--slixmpp/plugins/xep_0186/stanza.py23
-rw-r--r--slixmpp/plugins/xep_0191/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0191/blocking.py89
-rw-r--r--slixmpp/plugins/xep_0191/stanza.py50
-rw-r--r--slixmpp/plugins/xep_0196/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0196/stanza.py20
-rw-r--r--slixmpp/plugins/xep_0196/user_gaming.py93
-rw-r--r--slixmpp/plugins/xep_0198/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0198/stanza.py150
-rw-r--r--slixmpp/plugins/xep_0198/stream_management.py313
-rw-r--r--slixmpp/plugins/xep_0199/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0199/ping.py187
-rw-r--r--slixmpp/plugins/xep_0199/stanza.py36
-rw-r--r--slixmpp/plugins/xep_0202/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0202/stanza.py127
-rw-r--r--slixmpp/plugins/xep_0202/time.py99
-rw-r--r--slixmpp/plugins/xep_0203/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0203/delay.py37
-rw-r--r--slixmpp/plugins/xep_0203/stanza.py46
-rw-r--r--slixmpp/plugins/xep_0221/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0221/media.py27
-rw-r--r--slixmpp/plugins/xep_0221/stanza.py42
-rw-r--r--slixmpp/plugins/xep_0222.py120
-rw-r--r--slixmpp/plugins/xep_0223.py119
-rw-r--r--slixmpp/plugins/xep_0224/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0224/attention.py75
-rw-r--r--slixmpp/plugins/xep_0224/stanza.py40
-rw-r--r--slixmpp/plugins/xep_0231/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0231/bob.py141
-rw-r--r--slixmpp/plugins/xep_0231/stanza.py36
-rw-r--r--slixmpp/plugins/xep_0235/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0235/oauth.py32
-rw-r--r--slixmpp/plugins/xep_0235/stanza.py80
-rw-r--r--slixmpp/plugins/xep_0242.py21
-rw-r--r--slixmpp/plugins/xep_0249/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0249/invite.py83
-rw-r--r--slixmpp/plugins/xep_0249/stanza.py39
-rw-r--r--slixmpp/plugins/xep_0256.py73
-rw-r--r--slixmpp/plugins/xep_0257/__init__.py17
-rw-r--r--slixmpp/plugins/xep_0257/client_cert_management.py70
-rw-r--r--slixmpp/plugins/xep_0257/stanza.py87
-rw-r--r--slixmpp/plugins/xep_0258/__init__.py18
-rw-r--r--slixmpp/plugins/xep_0258/security_labels.py44
-rw-r--r--slixmpp/plugins/xep_0258/stanza.py141
-rw-r--r--slixmpp/plugins/xep_0270.py20
-rw-r--r--slixmpp/plugins/xep_0279/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0279/ipcheck.py41
-rw-r--r--slixmpp/plugins/xep_0279/stanza.py30
-rw-r--r--slixmpp/plugins/xep_0280/__init__.py17
-rw-r--r--slixmpp/plugins/xep_0280/carbons.py85
-rw-r--r--slixmpp/plugins/xep_0280/stanza.py64
-rw-r--r--slixmpp/plugins/xep_0297/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0297/forwarded.py64
-rw-r--r--slixmpp/plugins/xep_0297/stanza.py36
-rw-r--r--slixmpp/plugins/xep_0302.py21
-rw-r--r--slixmpp/plugins/xep_0308/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0308/correction.py52
-rw-r--r--slixmpp/plugins/xep_0308/stanza.py16
-rw-r--r--slixmpp/plugins/xep_0313/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0313/mam.py85
-rw-r--r--slixmpp/plugins/xep_0313/stanza.py139
-rw-r--r--slixmpp/plugins/xep_0319/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0319/idle.py75
-rw-r--r--slixmpp/plugins/xep_0319/stanza.py28
-rw-r--r--slixmpp/plugins/xep_0323/__init__.py18
-rw-r--r--slixmpp/plugins/xep_0323/device.py258
-rw-r--r--slixmpp/plugins/xep_0323/sensordata.py712
-rw-r--r--slixmpp/plugins/xep_0323/stanza/__init__.py12
-rw-r--r--slixmpp/plugins/xep_0323/stanza/base.py13
-rw-r--r--slixmpp/plugins/xep_0323/stanza/sensordata.py792
-rw-r--r--slixmpp/plugins/xep_0323/timerreset.py69
-rw-r--r--slixmpp/plugins/xep_0325/__init__.py18
-rw-r--r--slixmpp/plugins/xep_0325/control.py548
-rw-r--r--slixmpp/plugins/xep_0325/device.py125
-rw-r--r--slixmpp/plugins/xep_0325/stanza/__init__.py12
-rw-r--r--slixmpp/plugins/xep_0325/stanza/base.py13
-rw-r--r--slixmpp/plugins/xep_0325/stanza/control.py526
-rw-r--r--slixmpp/plugins/xep_0332/__init__.py17
-rw-r--r--slixmpp/plugins/xep_0332/http.py159
-rw-r--r--slixmpp/plugins/xep_0332/stanza/__init__.py13
-rw-r--r--slixmpp/plugins/xep_0332/stanza/data.py30
-rw-r--r--slixmpp/plugins/xep_0332/stanza/request.py71
-rw-r--r--slixmpp/plugins/xep_0332/stanza/response.py66
-rw-r--r--slixmpp/roster/__init__.py11
-rw-r--r--slixmpp/roster/item.py497
-rw-r--r--slixmpp/roster/multi.py224
-rw-r--r--slixmpp/roster/single.py337
-rw-r--r--slixmpp/stanza/__init__.py15
-rw-r--r--slixmpp/stanza/atom.py43
-rw-r--r--slixmpp/stanza/error.py164
-rw-r--r--slixmpp/stanza/htmlim.py14
-rw-r--r--slixmpp/stanza/iq.py261
-rw-r--r--slixmpp/stanza/message.py188
-rw-r--r--slixmpp/stanza/nick.py17
-rw-r--r--slixmpp/stanza/presence.py173
-rw-r--r--slixmpp/stanza/rootstanza.py90
-rw-r--r--slixmpp/stanza/roster.py152
-rw-r--r--slixmpp/stanza/stream_error.py83
-rw-r--r--slixmpp/stanza/stream_features.py57
-rw-r--r--slixmpp/stringprep.py121
-rw-r--r--slixmpp/stringprep.pyx89
-rw-r--r--slixmpp/test/__init__.py11
-rw-r--r--slixmpp/test/livesocket.py171
-rw-r--r--slixmpp/test/mocksocket.py245
-rw-r--r--slixmpp/test/slixtest.py710
-rw-r--r--slixmpp/thirdparty/__init__.py7
-rw-r--r--slixmpp/thirdparty/gnupg.py1017
-rw-r--r--slixmpp/thirdparty/mini_dateutil.py273
-rw-r--r--slixmpp/thirdparty/orderedset.py89
-rw-r--r--slixmpp/util/__init__.py15
-rw-r--r--slixmpp/util/misc_ops.py143
-rw-r--r--slixmpp/util/sasl/__init__.py17
-rw-r--r--slixmpp/util/sasl/client.py174
-rw-r--r--slixmpp/util/sasl/mechanisms.py548
-rw-r--r--slixmpp/util/stringprep_profiles.py151
-rw-r--r--slixmpp/version.py13
-rw-r--r--slixmpp/xmlstream/__init__.py17
-rw-r--r--slixmpp/xmlstream/asyncio.py50
-rw-r--r--slixmpp/xmlstream/cert.py184
-rw-r--r--slixmpp/xmlstream/handler/__init__.py16
-rw-r--r--slixmpp/xmlstream/handler/base.py79
-rw-r--r--slixmpp/xmlstream/handler/callback.py79
-rw-r--r--slixmpp/xmlstream/handler/collector.py66
-rw-r--r--slixmpp/xmlstream/handler/coroutine_callback.py84
-rw-r--r--slixmpp/xmlstream/handler/waiter.py83
-rw-r--r--slixmpp/xmlstream/handler/xmlcallback.py36
-rw-r--r--slixmpp/xmlstream/handler/xmlwaiter.py33
-rw-r--r--slixmpp/xmlstream/matcher/__init__.py17
-rw-r--r--slixmpp/xmlstream/matcher/base.py31
-rw-r--r--slixmpp/xmlstream/matcher/id.py29
-rw-r--r--slixmpp/xmlstream/matcher/idsender.py47
-rw-r--r--slixmpp/xmlstream/matcher/many.py40
-rw-r--r--slixmpp/xmlstream/matcher/stanzapath.py43
-rw-r--r--slixmpp/xmlstream/matcher/xmlmask.py117
-rw-r--r--slixmpp/xmlstream/matcher/xpath.py59
-rw-r--r--slixmpp/xmlstream/resolver.py314
-rw-r--r--slixmpp/xmlstream/stanzabase.py1631
-rw-r--r--slixmpp/xmlstream/tostring.py183
-rw-r--r--slixmpp/xmlstream/xmlstream.py943
326 files changed, 36029 insertions, 0 deletions
diff --git a/slixmpp/__init__.py b/slixmpp/__init__.py
new file mode 100644
index 00000000..c09446df
--- /dev/null
+++ b/slixmpp/__init__.py
@@ -0,0 +1,26 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import asyncio
+asyncio.sslproto._is_sslproto_available=lambda: False
+import logging
+logging.getLogger(__name__).addHandler(logging.NullHandler())
+
+
+from slixmpp.stanza import Message, Presence, Iq
+from slixmpp.jid import JID, InvalidJID
+from slixmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin
+from slixmpp.xmlstream.handler import *
+from slixmpp.xmlstream import XMLStream
+from slixmpp.xmlstream.matcher import *
+from slixmpp.xmlstream.asyncio import asyncio, future_wrapper
+from slixmpp.basexmpp import BaseXMPP
+from slixmpp.clientxmpp import ClientXMPP
+from slixmpp.componentxmpp import ComponentXMPP
+
+from slixmpp.version import __version__, __version_info__
diff --git a/slixmpp/api.py b/slixmpp/api.py
new file mode 100644
index 00000000..f09e0365
--- /dev/null
+++ b/slixmpp/api.py
@@ -0,0 +1,200 @@
+from slixmpp.xmlstream import JID
+
+
+class APIWrapper(object):
+
+ def __init__(self, api, name):
+ self.api = api
+ self.name = name
+ if name not in self.api.settings:
+ self.api.settings[name] = {}
+
+ def __getattr__(self, attr):
+ """Curry API management commands with the API name."""
+ if attr == 'name':
+ return self.name
+ elif attr == 'settings':
+ return self.api.settings[self.name]
+ elif attr == 'register':
+ def partial(handler, op, jid=None, node=None, default=False):
+ register = getattr(self.api, attr)
+ return register(handler, self.name, op, jid, node, default)
+ return partial
+ elif attr == 'register_default':
+ def partial(handler, op, jid=None, node=None):
+ return getattr(self.api, attr)(handler, self.name, op)
+ return partial
+ elif attr in ('run', 'restore_default', 'unregister'):
+ def partial(*args, **kwargs):
+ return getattr(self.api, attr)(self.name, *args, **kwargs)
+ return partial
+ return None
+
+ def __getitem__(self, attr):
+ def partial(jid=None, node=None, ifrom=None, args=None):
+ return self.api.run(self.name, attr, jid, node, ifrom, args)
+ return partial
+
+
+class APIRegistry(object):
+
+ def __init__(self, xmpp):
+ self._handlers = {}
+ self._handler_defaults = {}
+ self.xmpp = xmpp
+ self.settings = {}
+
+ def _setup(self, ctype, op):
+ """Initialize the API callback dictionaries.
+
+ :param string ctype: The name of the API to initialize.
+ :param string op: The API operation to initialize.
+ """
+ if ctype not in self.settings:
+ self.settings[ctype] = {}
+ if ctype not in self._handler_defaults:
+ self._handler_defaults[ctype] = {}
+ if ctype not in self._handlers:
+ self._handlers[ctype] = {}
+ if op not in self._handlers[ctype]:
+ self._handlers[ctype][op] = {'global': None,
+ 'jid': {},
+ 'node': {}}
+
+ def wrap(self, ctype):
+ """Return a wrapper object that targets a specific API."""
+ return APIWrapper(self, ctype)
+
+ def purge(self, ctype):
+ """Remove all information for a given API."""
+ del self.settings[ctype]
+ del self._handler_defaults[ctype]
+ del self._handlers[ctype]
+
+ def run(self, ctype, op, jid=None, node=None, ifrom=None, args=None):
+ """Execute an API callback, based on specificity.
+
+ The API callback that is executed is chosen based on the combination
+ of the provided JID and node:
+
+ JID | node | Handler
+ ==============================
+ Given | Given | Node handler
+ Given | None | JID handler
+ None | None | Global handler
+
+ A node handler is responsible for servicing a single node at a single
+ JID, while a JID handler may respond for any node at a given JID, and
+ the global handler will answer to any JID+node combination.
+
+ Handlers should check that the JID ``ifrom`` is authorized to perform
+ the desired action.
+
+ :param string ctype: The name of the API to use.
+ :param string op: The API operation to perform.
+ :param JID jid: Optionally provide specific JID.
+ :param string node: Optionally provide specific node.
+ :param JID ifrom: Optionally provide the requesting JID.
+ :param tuple args: Optional positional arguments to the handler.
+ """
+ self._setup(ctype, op)
+
+ if not jid:
+ jid = self.xmpp.boundjid
+ elif jid and not isinstance(jid, JID):
+ jid = JID(jid)
+ elif jid == JID(''):
+ jid = self.xmpp.boundjid
+
+ if node is None:
+ node = ''
+
+ if self.xmpp.is_component:
+ if self.settings[ctype].get('component_bare', False):
+ jid = jid.bare
+ else:
+ jid = jid.full
+ else:
+ if self.settings[ctype].get('client_bare', False):
+ jid = jid.bare
+ else:
+ jid = jid.full
+
+ jid = JID(jid)
+
+ handler = self._handlers[ctype][op]['node'].get((jid, node), None)
+ if handler is None:
+ handler = self._handlers[ctype][op]['jid'].get(jid, None)
+ if handler is None:
+ handler = self._handlers[ctype][op].get('global', None)
+
+ if handler:
+ try:
+ return handler(jid, node, ifrom, args)
+ except TypeError:
+ # To preserve backward compatibility, drop the ifrom
+ # parameter for existing handlers that don't understand it.
+ return handler(jid, node, args)
+
+ def register(self, handler, ctype, op, jid=None, node=None, default=False):
+ """Register an API callback, with JID+node specificity.
+
+ The API callback can later be executed based on the
+ specificity of the provided JID+node combination.
+
+ See :meth:`~ApiRegistry.run` for more details.
+
+ :param string ctype: The name of the API to use.
+ :param string op: The API operation to perform.
+ :param JID jid: Optionally provide specific JID.
+ :param string node: Optionally provide specific node.
+ """
+ self._setup(ctype, op)
+ if jid is None and node is None:
+ if handler is None:
+ handler = self._handler_defaults[op]
+ self._handlers[ctype][op]['global'] = handler
+ elif jid is not None and node is None:
+ self._handlers[ctype][op]['jid'][jid] = handler
+ else:
+ self._handlers[ctype][op]['node'][(jid, node)] = handler
+
+ if default:
+ self.register_default(handler, ctype, op)
+
+ def register_default(self, handler, ctype, op):
+ """Register a default, global handler for an operation.
+
+ :param func handler: The default, global handler for the operation.
+ :param string ctype: The name of the API to modify.
+ :param string op: The API operation to use.
+ """
+ self._setup(ctype, op)
+ self._handler_defaults[ctype][op] = handler
+
+ def unregister(self, ctype, op, jid=None, node=None):
+ """Remove an API callback.
+
+ The API callback chosen for removal is based on the
+ specificity of the provided JID+node combination.
+
+ See :meth:`~ApiRegistry.run` for more details.
+
+ :param string ctype: The name of the API to use.
+ :param string op: The API operation to perform.
+ :param JID jid: Optionally provide specific JID.
+ :param string node: Optionally provide specific node.
+ """
+ self._setup(ctype, op)
+ self.register(None, ctype, op, jid, node)
+
+ def restore_default(self, ctype, op, jid=None, node=None):
+ """Reset an API callback to use a default handler.
+
+ :param string ctype: The name of the API to use.
+ :param string op: The API operation to perform.
+ :param JID jid: Optionally provide specific JID.
+ :param string node: Optionally provide specific node.
+ """
+ self.unregister(ctype, op, jid, node)
+ self.register(self._handler_defaults[ctype][op], ctype, op, jid, node)
diff --git a/slixmpp/basexmpp.py b/slixmpp/basexmpp.py
new file mode 100644
index 00000000..83741bd7
--- /dev/null
+++ b/slixmpp/basexmpp.py
@@ -0,0 +1,793 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.basexmpp
+ ~~~~~~~~~~~~~~~~~~
+
+ This module provides the common XMPP functionality
+ for both clients and components.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+import threading
+
+from slixmpp import plugins, roster, stanza
+from slixmpp.api import APIRegistry
+from slixmpp.exceptions import IqError, IqTimeout
+
+from slixmpp.stanza import Message, Presence, Iq, StreamError
+from slixmpp.stanza.roster import Roster
+from slixmpp.stanza.nick import Nick
+
+from slixmpp.xmlstream import XMLStream, JID
+from slixmpp.xmlstream import ET, register_stanza_plugin
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.stanzabase import XML_NS
+
+from slixmpp.plugins import PluginManager, load_plugin
+
+
+log = logging.getLogger(__name__)
+
+class BaseXMPP(XMLStream):
+
+ """
+ The BaseXMPP class adapts the generic XMLStream class for use
+ with XMPP. It also provides a plugin mechanism to easily extend
+ and add support for new XMPP features.
+
+ :param default_ns: Ensure that the correct default XML namespace
+ is used during initialization.
+ """
+
+ def __init__(self, jid='', default_ns='jabber:client', **kwargs):
+ XMLStream.__init__(self, **kwargs)
+
+ self.default_ns = default_ns
+ self.stream_ns = 'http://etherx.jabber.org/streams'
+ self.namespace_map[self.stream_ns] = 'stream'
+
+ #: An identifier for the stream as given by the server.
+ self.stream_id = None
+
+ #: The JabberID (JID) requested for this connection.
+ self.requested_jid = JID(jid)
+
+ #: The JabberID (JID) used by this connection,
+ #: as set after session binding. This may even be a
+ #: different bare JID than what was requested.
+ self.boundjid = JID(jid)
+
+ self._expected_server_name = self.boundjid.host
+ self._redirect_attempts = 0
+
+ #: The maximum number of consecutive see-other-host
+ #: redirections that will be followed before quitting.
+ self.max_redirects = 5
+
+ self.session_bind_event = threading.Event()
+
+ #: A dictionary mapping plugin names to plugins.
+ self.plugin = PluginManager(self)
+
+ #: Configuration options for whitelisted plugins.
+ #: If a plugin is registered without any configuration,
+ #: and there is an entry here, it will be used.
+ self.plugin_config = {}
+
+ #: A list of plugins that will be loaded if
+ #: :meth:`register_plugins` is called.
+ self.plugin_whitelist = []
+
+ #: The main roster object. This roster supports multiple
+ #: owner JIDs, as in the case for components. For clients
+ #: which only have a single JID, see :attr:`client_roster`.
+ self.roster = roster.Roster(self)
+ self.roster.add(self.boundjid)
+
+ #: The single roster for the bound JID. This is the
+ #: equivalent of::
+ #:
+ #: self.roster[self.boundjid.bare]
+ self.client_roster = self.roster[self.boundjid]
+
+ #: The distinction between clients and components can be
+ #: important, primarily for choosing how to handle the
+ #: ``'to'`` and ``'from'`` JIDs of stanzas.
+ self.is_component = False
+
+ #: Messages may optionally be tagged with ID values. Setting
+ #: :attr:`use_message_ids` to `True` will assign all outgoing
+ #: messages an ID. Some plugin features require enabling
+ #: this option.
+ self.use_message_ids = False
+
+ #: Presence updates may optionally be tagged with ID values.
+ #: Setting :attr:`use_message_ids` to `True` will assign all
+ #: outgoing messages an ID.
+ self.use_presence_ids = False
+
+ #: The API registry is a way to process callbacks based on
+ #: JID+node combinations. Each callback in the registry is
+ #: marked with:
+ #:
+ #: - An API name, e.g. xep_0030
+ #: - The name of an action, e.g. get_info
+ #: - The JID that will be affected
+ #: - The node that will be affected
+ #:
+ #: API handlers with no JID or node will act as global handlers,
+ #: while those with a JID and no node will service all nodes
+ #: for a JID, and handlers with both a JID and node will be
+ #: used only for that specific combination. The handler that
+ #: provides the most specificity will be used.
+ self.api = APIRegistry(self)
+
+ #: Flag indicating that the initial presence broadcast has
+ #: been sent. Until this happens, some servers may not
+ #: behave as expected when sending stanzas.
+ self.sentpresence = False
+
+ #: A reference to :mod:`slixmpp.stanza` to make accessing
+ #: stanza classes easier.
+ self.stanza = stanza
+
+ self.register_handler(
+ Callback('IM',
+ MatchXPath('{%s}message/{%s}body' % (self.default_ns,
+ self.default_ns)),
+ self._handle_message))
+
+ self.register_handler(
+ Callback('IMError',
+ MatchXPath('{%s}message/{%s}error' % (self.default_ns,
+ self.default_ns)),
+ self._handle_message_error))
+
+ self.register_handler(
+ Callback('Presence',
+ MatchXPath("{%s}presence" % self.default_ns),
+ self._handle_presence))
+
+ self.register_handler(
+ Callback('Stream Error',
+ MatchXPath("{%s}error" % self.stream_ns),
+ self._handle_stream_error))
+
+ self.add_event_handler('session_start',
+ self._handle_session_start)
+ self.add_event_handler('disconnected',
+ self._handle_disconnected)
+ self.add_event_handler('presence_available',
+ self._handle_available)
+ self.add_event_handler('presence_dnd',
+ self._handle_available)
+ self.add_event_handler('presence_xa',
+ self._handle_available)
+ self.add_event_handler('presence_chat',
+ self._handle_available)
+ self.add_event_handler('presence_away',
+ self._handle_available)
+ self.add_event_handler('presence_unavailable',
+ self._handle_unavailable)
+ self.add_event_handler('presence_subscribe',
+ self._handle_subscribe)
+ self.add_event_handler('presence_subscribed',
+ self._handle_subscribed)
+ self.add_event_handler('presence_unsubscribe',
+ self._handle_unsubscribe)
+ self.add_event_handler('presence_unsubscribed',
+ self._handle_unsubscribed)
+ self.add_event_handler('roster_subscription_request',
+ self._handle_new_subscription)
+
+ # Set up the XML stream with XMPP's root stanzas.
+ self.register_stanza(Message)
+ self.register_stanza(Iq)
+ self.register_stanza(Presence)
+ self.register_stanza(StreamError)
+
+ # Initialize a few default stanza plugins.
+ register_stanza_plugin(Iq, Roster)
+ register_stanza_plugin(Message, Nick)
+
+ def start_stream_handler(self, xml):
+ """Save the stream ID once the streams have been established.
+
+ :param xml: The incoming stream's root element.
+ """
+ self.stream_id = xml.get('id', '')
+ self.stream_version = xml.get('version', '')
+ self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
+
+ if not self.is_component and not self.stream_version:
+ log.warning('Legacy XMPP 0.9 protocol detected.')
+ self.event('legacy_protocol')
+
+ def process(self, *, forever=True, timeout=None):
+ self.init_plugins()
+ XMLStream.process(self, forever=forever, timeout=timeout)
+
+ def init_plugins(self):
+ for name in self.plugin:
+ if not hasattr(self.plugin[name], 'post_inited'):
+ if hasattr(self.plugin[name], 'post_init'):
+ self.plugin[name].post_init()
+ self.plugin[name].post_inited = True
+
+ def register_plugin(self, plugin, pconfig=None, module=None):
+ """Register and configure a plugin for use in this stream.
+
+ :param plugin: The name of the plugin class. Plugin names must
+ be unique.
+ :param pconfig: A dictionary of configuration data for the plugin.
+ Defaults to an empty dictionary.
+ :param module: Optional refence to the module containing the plugin
+ class if using custom plugins.
+ """
+
+ # Use the global plugin config cache, if applicable
+ if not pconfig:
+ pconfig = self.plugin_config.get(plugin, {})
+
+ if not self.plugin.registered(plugin):
+ load_plugin(plugin, module)
+ self.plugin.enable(plugin, pconfig)
+
+ def register_plugins(self):
+ """Register and initialize all built-in plugins.
+
+ Optionally, the list of plugins loaded may be limited to those
+ contained in :attr:`plugin_whitelist`.
+
+ Plugin configurations stored in :attr:`plugin_config` will be used.
+ """
+ if self.plugin_whitelist:
+ plugin_list = self.plugin_whitelist
+ else:
+ plugin_list = plugins.__all__
+
+ for plugin in plugin_list:
+ if plugin in plugins.__all__:
+ self.register_plugin(plugin)
+ else:
+ raise NameError("Plugin %s not in plugins.__all__." % plugin)
+
+ def __getitem__(self, key):
+ """Return a plugin given its name, if it has been registered."""
+ if key in self.plugin:
+ return self.plugin[key]
+ else:
+ log.warning("Plugin '%s' is not loaded.", key)
+ return False
+
+ def get(self, key, default):
+ """Return a plugin given its name, if it has been registered."""
+ return self.plugin.get(key, default)
+
+ def Message(self, *args, **kwargs):
+ """Create a Message stanza associated with this stream."""
+ msg = Message(self, *args, **kwargs)
+ msg['lang'] = self.default_lang
+ return msg
+
+ def Iq(self, *args, **kwargs):
+ """Create an Iq stanza associated with this stream."""
+ return Iq(self, *args, **kwargs)
+
+ def Presence(self, *args, **kwargs):
+ """Create a Presence stanza associated with this stream."""
+ pres = Presence(self, *args, **kwargs)
+ pres['lang'] = self.default_lang
+ return pres
+
+ def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
+ """Create a new Iq stanza with a given Id and from JID.
+
+ :param id: An ideally unique ID value for this stanza thread.
+ Defaults to 0.
+ :param ifrom: The from :class:`~slixmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param itype: The :class:`~slixmpp.stanza.iq.Iq`'s type,
+ one of: ``'get'``, ``'set'``, ``'result'``,
+ or ``'error'``.
+ :param iquery: Optional namespace for adding a query element.
+ """
+ iq = self.Iq()
+ iq['id'] = str(id)
+ iq['to'] = ito
+ iq['from'] = ifrom
+ iq['type'] = itype
+ iq['query'] = iquery
+ return iq
+
+ def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
+ """Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'get'``.
+
+ Optionally, a query element may be added.
+
+ :param queryxmlns: The namespace of the query to use.
+ :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq['query'] = queryxmlns
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
+ """
+ Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type
+ ``'result'`` with the given ID value.
+
+ :param id: An ideally unique ID value. May use :meth:`new_id()`.
+ :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ if id is None:
+ id = self.new_id()
+ iq['id'] = id
+ iq['type'] = 'result'
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
+ """
+ Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'set'``.
+
+ Optionally, a substanza may be given to use as the
+ stanza's payload.
+
+ :param sub: Either an
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza object or an
+ :class:`~xml.etree.ElementTree.Element` XML object
+ to use as the :class:`~slixmpp.stanza.iq.Iq`'s payload.
+ :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['type'] = 'set'
+ if sub != None:
+ iq.append(sub)
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_error(self, id, type='cancel',
+ condition='feature-not-implemented',
+ text=None, ito=None, ifrom=None, iq=None):
+ """
+ Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'error'``.
+
+ :param id: An ideally unique ID value. May use :meth:`new_id()`.
+ :param type: The type of the error, such as ``'cancel'`` or
+ ``'modify'``. Defaults to ``'cancel'``.
+ :param condition: The error condition. Defaults to
+ ``'feature-not-implemented'``.
+ :param text: A message describing the cause of the error.
+ :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['id'] = id
+ iq['error']['type'] = type
+ iq['error']['condition'] = condition
+ iq['error']['text'] = text
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
+ """
+ Create or modify an :class:`~slixmpp.stanza.iq.Iq` stanza
+ to use the given query namespace.
+
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ :param xmlns: The query's namespace.
+ :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['query'] = xmlns
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_query_roster(self, iq=None):
+ """Create a roster query element.
+
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if iq:
+ iq['query'] = 'jabber:iq:roster'
+ return ET.Element("{jabber:iq:roster}query")
+
+ def make_message(self, mto, mbody=None, msubject=None, mtype=None,
+ mhtml=None, mfrom=None, mnick=None):
+ """
+ Create and initialize a new
+ :class:`~slixmpp.stanza.message.Message` stanza.
+
+ :param mto: The recipient of the message.
+ :param mbody: The main contents of the message.
+ :param msubject: Optional subject for the message.
+ :param mtype: The message's type, such as ``'chat'`` or
+ ``'groupchat'``.
+ :param mhtml: Optional HTML body content in the form of a string.
+ :param mfrom: The sender of the message. if sending from a client,
+ be aware that some servers require that the full JID
+ of the sender be used.
+ :param mnick: Optional nickname of the sender.
+ """
+ message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
+ message['body'] = mbody
+ message['subject'] = msubject
+ if mnick is not None:
+ message['nick'] = mnick
+ if mhtml is not None:
+ message['html']['body'] = mhtml
+ return message
+
+ def make_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, ptype=None, pfrom=None, pnick=None):
+ """
+ Create and initialize a new
+ :class:`~slixmpp.stanza.presence.Presence` stanza.
+
+ :param pshow: The presence's show value.
+ :param pstatus: The presence's status message.
+ :param ppriority: This connection's priority.
+ :param pto: The recipient of a directed presence.
+ :param ptype: The type of presence, such as ``'subscribe'``.
+ :param pfrom: The sender of the presence.
+ :param pnick: Optional nickname of the presence's sender.
+ """
+ presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
+ if pshow is not None:
+ presence['type'] = pshow
+ if pfrom is None and self.is_component:
+ presence['from'] = self.boundjid.full
+ presence['priority'] = ppriority
+ presence['status'] = pstatus
+ presence['nick'] = pnick
+ return presence
+
+ def send_message(self, mto, mbody, msubject=None, mtype=None,
+ mhtml=None, mfrom=None, mnick=None):
+ """
+ Create, initialize, and send a new
+ :class:`~slixmpp.stanza.message.Message` stanza.
+
+ :param mto: The recipient of the message.
+ :param mbody: The main contents of the message.
+ :param msubject: Optional subject for the message.
+ :param mtype: The message's type, such as ``'chat'`` or
+ ``'groupchat'``.
+ :param mhtml: Optional HTML body content in the form of a string.
+ :param mfrom: The sender of the message. if sending from a client,
+ be aware that some servers require that the full JID
+ of the sender be used.
+ :param mnick: Optional nickname of the sender.
+ """
+ self.make_message(mto, mbody, msubject, mtype,
+ mhtml, mfrom, mnick).send()
+
+ def send_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, pfrom=None, ptype=None, pnick=None):
+ """
+ Create, initialize, and send a new
+ :class:`~slixmpp.stanza.presence.Presence` stanza.
+
+ :param pshow: The presence's show value.
+ :param pstatus: The presence's status message.
+ :param ppriority: This connection's priority.
+ :param pto: The recipient of a directed presence.
+ :param ptype: The type of presence, such as ``'subscribe'``.
+ :param pfrom: The sender of the presence.
+ :param pnick: Optional nickname of the presence's sender.
+ """
+ self.make_presence(pshow, pstatus, ppriority, pto,
+ ptype, pfrom, pnick).send()
+
+ def send_presence_subscription(self, pto, pfrom=None,
+ ptype='subscribe', pnick=None):
+ """
+ Create, initialize, and send a new
+ :class:`~slixmpp.stanza.presence.Presence` stanza of
+ type ``'subscribe'``.
+
+ :param pto: The recipient of a directed presence.
+ :param pfrom: The sender of the presence.
+ :param ptype: The type of presence, such as ``'subscribe'``.
+ :param pnick: Optional nickname of the presence's sender.
+ """
+ self.make_presence(ptype=ptype,
+ pfrom=pfrom,
+ pto=JID(pto).bare,
+ pnick=pnick).send()
+
+ @property
+ def jid(self):
+ """Attribute accessor for bare jid"""
+ log.warning("jid property deprecated. Use boundjid.bare")
+ return self.boundjid.bare
+
+ @jid.setter
+ def jid(self, value):
+ log.warning("jid property deprecated. Use boundjid.bare")
+ self.boundjid.bare = value
+
+ @property
+ def fulljid(self):
+ """Attribute accessor for full jid"""
+ log.warning("fulljid property deprecated. Use boundjid.full")
+ return self.boundjid.full
+
+ @fulljid.setter
+ def fulljid(self, value):
+ log.warning("fulljid property deprecated. Use boundjid.full")
+ self.boundjid.full = value
+
+ @property
+ def resource(self):
+ """Attribute accessor for jid resource"""
+ log.warning("resource property deprecated. Use boundjid.resource")
+ return self.boundjid.resource
+
+ @resource.setter
+ def resource(self, value):
+ log.warning("fulljid property deprecated. Use boundjid.resource")
+ self.boundjid.resource = value
+
+ @property
+ def username(self):
+ """Attribute accessor for jid usernode"""
+ log.warning("username property deprecated. Use boundjid.user")
+ return self.boundjid.user
+
+ @username.setter
+ def username(self, value):
+ log.warning("username property deprecated. Use boundjid.user")
+ self.boundjid.user = value
+
+ @property
+ def server(self):
+ """Attribute accessor for jid host"""
+ log.warning("server property deprecated. Use boundjid.host")
+ return self.boundjid.server
+
+ @server.setter
+ def server(self, value):
+ log.warning("server property deprecated. Use boundjid.host")
+ self.boundjid.server = value
+
+ @property
+ def auto_authorize(self):
+ """Auto accept or deny subscription requests.
+
+ If ``True``, auto accept subscription requests.
+ If ``False``, auto deny subscription requests.
+ If ``None``, don't automatically respond.
+ """
+ return self.roster.auto_authorize
+
+ @auto_authorize.setter
+ def auto_authorize(self, value):
+ self.roster.auto_authorize = value
+
+ @property
+ def auto_subscribe(self):
+ """Auto send requests for mutual subscriptions.
+
+ If ``True``, auto send mutual subscription requests.
+ """
+ return self.roster.auto_subscribe
+
+ @auto_subscribe.setter
+ def auto_subscribe(self, value):
+ self.roster.auto_subscribe = value
+
+ def set_jid(self, jid):
+ """Rip a JID apart and claim it as our own."""
+ log.debug("setting jid to %s", jid)
+ self.boundjid = JID(jid)
+
+ def getjidresource(self, fulljid):
+ if '/' in fulljid:
+ return fulljid.split('/', 1)[-1]
+ else:
+ return ''
+
+ def getjidbare(self, fulljid):
+ return fulljid.split('/', 1)[0]
+
+ def _handle_session_start(self, event):
+ """Reset redirection attempt count."""
+ self._redirect_attempts = 0
+
+ def _handle_disconnected(self, event):
+ """When disconnected, reset the roster"""
+ self.roster.reset()
+ self.session_bind_event.clear()
+
+ def _handle_stream_error(self, error):
+ self.event('stream_error', error)
+
+ if error['condition'] == 'see-other-host':
+ other_host = error['see_other_host']
+ if not other_host:
+ log.warning("No other host specified.")
+ return
+
+ if self._redirect_attempts > self.max_redirects:
+ log.error("Exceeded maximum number of redirection attempts.")
+ return
+
+ self._redirect_attempts += 1
+
+ host = other_host
+ port = 5222
+
+ if '[' in other_host and ']' in other_host:
+ host = other_host.split(']')[0][1:]
+ elif ':' in other_host:
+ host = other_host.split(':')[0]
+
+ port_sec = other_host.split(']')[-1]
+ if ':' in port_sec:
+ port = int(port_sec.split(':')[1])
+
+ self.address = (host, port)
+ self.default_domain = host
+ self.dns_records = None
+ self.reconnect_delay = None
+ self.reconnect()
+
+ def _handle_message(self, msg):
+ """Process incoming message stanzas."""
+ if not self.is_component and not msg['to'].bare:
+ msg['to'] = self.boundjid
+ self.event('message', msg)
+
+ def _handle_message_error(self, msg):
+ """Process incoming message error stanzas."""
+ if not self.is_component and not msg['to'].bare:
+ msg['to'] = self.boundjid
+ self.event('message_error', msg)
+
+ def _handle_available(self, pres):
+ self.roster[pres['to']][pres['from']].handle_available(pres)
+
+ def _handle_unavailable(self, pres):
+ self.roster[pres['to']][pres['from']].handle_unavailable(pres)
+
+ def _handle_new_subscription(self, pres):
+ """Attempt to automatically handle subscription requests.
+
+ Subscriptions will be approved if the request is from
+ a whitelisted JID, of :attr:`auto_authorize` is True. They
+ will be rejected if :attr:`auto_authorize` is False. Setting
+ :attr:`auto_authorize` to ``None`` will disable automatic
+ subscription handling (except for whitelisted JIDs).
+
+ If a subscription is accepted, a request for a mutual
+ subscription will be sent if :attr:`auto_subscribe` is ``True``.
+ """
+ roster = self.roster[pres['to']]
+ item = self.roster[pres['to']][pres['from']]
+ if item['whitelisted']:
+ item.authorize()
+ if roster.auto_subscribe:
+ item.subscribe()
+ elif roster.auto_authorize:
+ item.authorize()
+ if roster.auto_subscribe:
+ item.subscribe()
+ elif roster.auto_authorize == False:
+ item.unauthorize()
+
+ def _handle_removed_subscription(self, pres):
+ self.roster[pres['to']][pres['from']].handle_unauthorize(pres)
+
+ def _handle_subscribe(self, pres):
+ self.roster[pres['to']][pres['from']].handle_subscribe(pres)
+
+ def _handle_subscribed(self, pres):
+ self.roster[pres['to']][pres['from']].handle_subscribed(pres)
+
+ def _handle_unsubscribe(self, pres):
+ self.roster[pres['to']][pres['from']].handle_unsubscribe(pres)
+
+ def _handle_unsubscribed(self, pres):
+ self.roster[pres['to']][pres['from']].handle_unsubscribed(pres)
+
+ def _handle_presence(self, presence):
+ """Process incoming presence stanzas.
+
+ Update the roster with presence information.
+ """
+ if not self.is_component and not presence['to'].bare:
+ presence['to'] = self.boundjid
+
+ self.event('presence', presence)
+ self.event('presence_%s' % presence['type'], presence)
+
+ # Check for changes in subscription state.
+ if presence['type'] in ('subscribe', 'subscribed',
+ 'unsubscribe', 'unsubscribed'):
+ self.event('changed_subscription', presence)
+ return
+ elif not presence['type'] in ('available', 'unavailable') and \
+ not presence['type'] in presence.showtypes:
+ return
+
+ def exception(self, exception):
+ """Process any uncaught exceptions, notably
+ :class:`~slixmpp.exceptions.IqError` and
+ :class:`~slixmpp.exceptions.IqTimeout` exceptions.
+
+ :param exception: An unhandled :class:`Exception` object.
+ """
+ if isinstance(exception, IqError):
+ iq = exception.iq
+ log.error('%s: %s', iq['error']['condition'],
+ iq['error']['text'])
+ log.warning('You should catch IqError exceptions')
+ elif isinstance(exception, IqTimeout):
+ iq = exception.iq
+ log.error('Request timed out: %s', iq)
+ log.warning('You should catch IqTimeout exceptions')
+ elif isinstance(exception, SyntaxError):
+ # Hide stream parsing errors that occur when the
+ # stream is disconnected (they've been handled, we
+ # don't need to make a mess in the logs).
+ pass
+ else:
+ log.exception(exception)
diff --git a/slixmpp/clientxmpp.py b/slixmpp/clientxmpp.py
new file mode 100644
index 00000000..40d20333
--- /dev/null
+++ b/slixmpp/clientxmpp.py
@@ -0,0 +1,305 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.clientxmpp
+ ~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to client connections.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+
+from slixmpp.stanza import StreamFeatures
+from slixmpp.basexmpp import BaseXMPP
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream import XMLStream
+from slixmpp.xmlstream.matcher import StanzaPath, MatchXPath
+from slixmpp.xmlstream.handler import Callback
+
+# Flag indicating if DNS SRV records are available for use.
+try:
+ import dns.resolver
+except ImportError:
+ DNSPYTHON = False
+else:
+ DNSPYTHON = True
+
+
+log = logging.getLogger(__name__)
+
+
+class ClientXMPP(BaseXMPP):
+
+ """
+ Slixmpp's client class. (Use only for good, not for evil.)
+
+ Typical use pattern:
+
+ .. code-block:: python
+
+ xmpp = ClientXMPP('user@server.tld/resource', 'password')
+ # ... Register plugins and event handlers ...
+ xmpp.connect()
+ xmpp.process(block=False) # block=True will block the current
+ # thread. By default, block=False
+
+ :param jid: The JID of the XMPP user account.
+ :param password: The password for the XMPP user account.
+ :param plugin_config: A dictionary of plugin configurations.
+ :param plugin_whitelist: A list of approved plugins that
+ will be loaded when calling
+ :meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`.
+ :param escape_quotes: **Deprecated.**
+ """
+
+ def __init__(self, jid, password, plugin_config=None,
+ plugin_whitelist=None, escape_quotes=True, sasl_mech=None,
+ lang='en', **kwargs):
+ if not plugin_whitelist:
+ plugin_whitelist = []
+ if not plugin_config:
+ plugin_config = {}
+
+ BaseXMPP.__init__(self, jid, 'jabber:client', **kwargs)
+
+ self.escape_quotes = escape_quotes
+ self.plugin_config = plugin_config
+ self.plugin_whitelist = plugin_whitelist
+ self.default_port = 5222
+ self.default_lang = lang
+
+ self.credentials = {}
+
+ self.password = password
+
+ self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % (
+ self.boundjid.host,
+ "xmlns:stream='%s'" % self.stream_ns,
+ "xmlns='%s'" % self.default_ns,
+ "xml:lang='%s'" % self.default_lang,
+ "version='1.0'")
+ self.stream_footer = "</stream:stream>"
+
+ self.features = set()
+ self._stream_feature_handlers = {}
+ self._stream_feature_order = []
+
+ self.dns_service = 'xmpp-client'
+
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+
+ self.add_event_handler('connected', self._reset_connection_state)
+ self.add_event_handler('session_bind', self._handle_session_bind)
+ self.add_event_handler('roster_update', self._handle_roster)
+
+ self.register_stanza(StreamFeatures)
+
+ self.register_handler(
+ Callback('Stream Features',
+ MatchXPath('{%s}features' % self.stream_ns),
+ self._handle_stream_features))
+ self.register_handler(
+ Callback('Roster Update',
+ StanzaPath('iq@type=set/roster'),
+ lambda iq: self.event('roster_update', iq)))
+
+ # Setup default stream features
+ self.register_plugin('feature_starttls')
+ self.register_plugin('feature_bind')
+ self.register_plugin('feature_session')
+ self.register_plugin('feature_rosterver')
+ self.register_plugin('feature_preapproval')
+ self.register_plugin('feature_mechanisms')
+
+ if sasl_mech:
+ self['feature_mechanisms'].use_mech = sasl_mech
+
+ @property
+ def password(self):
+ return self.credentials.get('password', '')
+
+ @password.setter
+ def password(self, value):
+ self.credentials['password'] = value
+
+ def connect(self, address=tuple(), use_ssl=False,
+ force_starttls=True, disable_starttls=False):
+ """Connect to the XMPP server.
+
+ When no address is given, a SRV lookup for the server will
+ be attempted. If that fails, the server user in the JID
+ will be used.
+
+ :param address: A tuple containing the server's host and port.
+ :param use_tls: Indicates if TLS should be used for the
+ connection. Defaults to ``True``.
+ :param use_ssl: Indicates if the older SSL connection method
+ should be used. Defaults to ``False``.
+ """
+
+ # If an address was provided, disable using DNS SRV lookup;
+ # otherwise, use the domain from the client JID with the standard
+ # XMPP client port and allow SRV lookup.
+ if address:
+ self.dns_service = None
+ else:
+ address = (self.boundjid.host, 5222)
+ self.dns_service = 'xmpp-client'
+
+ return XMLStream.connect(self, address[0], address[1], use_ssl=use_ssl,
+ force_starttls=force_starttls, disable_starttls=disable_starttls)
+
+ def register_feature(self, name, handler, restart=False, order=5000):
+ """Register a stream feature handler.
+
+ :param name: The name of the stream feature.
+ :param handler: The function to execute if the feature is received.
+ :param restart: Indicates if feature processing should halt with
+ this feature. Defaults to ``False``.
+ :param order: The relative ordering in which the feature should
+ be negotiated. Lower values will be attempted
+ earlier when available.
+ """
+ self._stream_feature_handlers[name] = (handler, restart)
+ self._stream_feature_order.append((order, name))
+ self._stream_feature_order.sort()
+
+ def unregister_feature(self, name, order):
+ if name in self._stream_feature_handlers:
+ del self._stream_feature_handlers[name]
+ self._stream_feature_order.remove((order, name))
+ self._stream_feature_order.sort()
+
+ def update_roster(self, jid, **kwargs):
+ """Add or change a roster item.
+
+ :param jid: The JID of the entry to modify.
+ :param name: The user's nickname for this JID.
+ :param subscription: The subscription status. May be one of
+ ``'to'``, ``'from'``, ``'both'``, or
+ ``'none'``. If set to ``'remove'``,
+ the entry will be deleted.
+ :param groups: The roster groups that contain this item.
+ :param timeout: The length of time (in seconds) to wait
+ for a response before continuing if blocking
+ is used. Defaults to
+ :attr:`~slixmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
+ :param callback: Optional reference to a stream handler function.
+ Will be executed when the roster is received.
+ Implies ``block=False``.
+ """
+ current = self.client_roster[jid]
+
+ name = kwargs.get('name', current['name'])
+ subscription = kwargs.get('subscription', current['subscription'])
+ groups = kwargs.get('groups', current['groups'])
+
+ timeout = kwargs.get('timeout', None)
+ callback = kwargs.get('callback', None)
+
+ return self.client_roster.update(jid, name, subscription, groups,
+ timeout, callback)
+
+ def del_roster_item(self, jid):
+ """Remove an item from the roster.
+
+ This is done by setting its subscription status to ``'remove'``.
+
+ :param jid: The JID of the item to remove.
+ """
+ return self.client_roster.remove(jid)
+
+ def get_roster(self, callback=None, timeout=None, timeout_callback=None):
+ """Request the roster from the server.
+
+ :param callback: Reference to a stream handler function. Will
+ be executed when the roster is received.
+ """
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq.enable('roster')
+ if 'rosterver' in self.features:
+ iq['roster']['ver'] = self.client_roster.version
+
+ if callback is None:
+ callback = lambda resp: self.event('roster_update', resp)
+ else:
+ orig_cb = callback
+ def wrapped(resp):
+ self.event('roster_update', resp)
+ orig_cb(resp)
+ callback = wrapped
+
+ iq.send(callback, timeout, timeout_callback)
+
+ def _reset_connection_state(self, event=None):
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+ self.features = set()
+
+ def _handle_stream_features(self, features):
+ """Process the received stream features.
+
+ :param features: The features stanza.
+ """
+ for order, name in self._stream_feature_order:
+ if name in features['features']:
+ handler, restart = self._stream_feature_handlers[name]
+ if handler(features) and restart:
+ # Don't continue if the feature requires
+ # restarting the XML stream.
+ return True
+ log.debug('Finished processing stream features.')
+ self.event('stream_negotiated')
+
+ def _handle_roster(self, iq):
+ """Update the roster after receiving a roster stanza.
+
+ :param iq: The roster stanza.
+ """
+ if iq['type'] == 'set':
+ if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
+ raise XMPPError(condition='service-unavailable')
+
+ roster = self.client_roster
+ if iq['roster']['ver']:
+ roster.version = iq['roster']['ver']
+ items = iq['roster']['items']
+
+ valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
+ for jid, item in items.items():
+ if item['subscription'] in valid_subscriptions:
+ roster[jid]['name'] = item['name']
+ roster[jid]['groups'] = item['groups']
+ roster[jid]['from'] = item['subscription'] in ('from', 'both')
+ roster[jid]['to'] = item['subscription'] in ('to', 'both')
+ roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
+
+ roster[jid].save(remove=(item['subscription'] == 'remove'))
+
+ if iq['type'] == 'set':
+ resp = self.Iq(stype='result',
+ sto=iq['from'],
+ sid=iq['id'])
+ resp.enable('roster')
+ resp.send()
+
+ def _handle_session_bind(self, jid):
+ """Set the client roster to the JID set by the server.
+
+ :param :class:`slixmpp.xmlstream.jid.JID` jid: The bound JID as
+ dictated by the server. The same as :attr:`boundjid`.
+ """
+ self.client_roster = self.roster[jid]
diff --git a/slixmpp/componentxmpp.py b/slixmpp/componentxmpp.py
new file mode 100644
index 00000000..868798d1
--- /dev/null
+++ b/slixmpp/componentxmpp.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.clientxmpp
+ ~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to external server component connections.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+import hashlib
+
+from slixmpp.basexmpp import BaseXMPP
+from slixmpp.xmlstream import XMLStream
+from slixmpp.xmlstream import ET
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.xmlstream.handler import Callback
+
+
+log = logging.getLogger(__name__)
+
+
+class ComponentXMPP(BaseXMPP):
+
+ """
+ Slixmpp's basic XMPP server component.
+
+ Use only for good, not for evil.
+
+ :param jid: The JID of the component.
+ :param secret: The secret or password for the component.
+ :param host: The server accepting the component.
+ :param port: The port used to connect to the server.
+ :param plugin_config: A dictionary of plugin configurations.
+ :param plugin_whitelist: A list of approved plugins that
+ will be loaded when calling
+ :meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`.
+ :param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
+ should be used instead of the standard
+ ``'jabber:component:accept'`` namespace.
+ Defaults to ``False``.
+ """
+
+ def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False):
+
+ if not plugin_whitelist:
+ plugin_whitelist = []
+ if not plugin_config:
+ plugin_config = {}
+
+ if use_jc_ns:
+ default_ns = 'jabber:client'
+ else:
+ default_ns = 'jabber:component:accept'
+ BaseXMPP.__init__(self, jid, default_ns)
+
+ self.auto_authorize = None
+ self.stream_header = '<stream:stream %s %s to="%s">' % (
+ 'xmlns="jabber:component:accept"',
+ 'xmlns:stream="%s"' % self.stream_ns,
+ jid)
+ self.stream_footer = "</stream:stream>"
+ self.server_host = host
+ self.server_port = port
+ self.secret = secret
+
+ self.plugin_config = plugin_config
+ self.plugin_whitelist = plugin_whitelist
+ self.is_component = True
+
+ self.sessionstarted = False
+
+ self.register_handler(
+ Callback('Handshake',
+ MatchXPath('{jabber:component:accept}handshake'),
+ self._handle_handshake))
+ self.add_event_handler('presence_probe',
+ self._handle_probe)
+
+ def connect(self, host=None, port=None, use_ssl=False):
+ """Connect to the server.
+
+
+ :param host: The name of the desired server for the connection.
+ Defaults to :attr:`server_host`.
+ :param port: Port to connect to on the server.
+ Defauts to :attr:`server_port`.
+ :param use_ssl: Flag indicating if SSL should be used by connecting
+ directly to a port using SSL.
+ """
+ if host is None:
+ host = self.server_host
+ if port is None:
+ port = self.server_port
+
+ self.server_name = self.boundjid.host
+
+ log.debug("Connecting to %s:%s", host, port)
+ return XMLStream.connect(self, host=host, port=port,
+ use_ssl=use_ssl)
+
+ def incoming_filter(self, xml):
+ """
+ Pre-process incoming XML stanzas by converting any
+ ``'jabber:client'`` namespaced elements to the component's
+ default namespace.
+
+ :param xml: The XML stanza to pre-process.
+ """
+ if xml.tag.startswith('{jabber:client}'):
+ xml.tag = xml.tag.replace('jabber:client', self.default_ns)
+ return xml
+
+ def start_stream_handler(self, xml):
+ """
+ Once the streams are established, attempt to handshake
+ with the server to be accepted as a component.
+
+ :param xml: The incoming stream's root element.
+ """
+ BaseXMPP.start_stream_handler(self, xml)
+
+ # Construct a hash of the stream ID and the component secret.
+ sid = xml.get('id', '')
+ pre_hash = bytes('%s%s' % (sid, self.secret), 'utf-8')
+
+ handshake = ET.Element('{jabber:component:accept}handshake')
+ handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
+ self.send_xml(handshake)
+
+ def _handle_handshake(self, xml):
+ """The handshake has been accepted.
+
+ :param xml: The reply handshake stanza.
+ """
+ self.session_bind_event.set()
+ self.sessionstarted = True
+ self.event('session_bind', self.boundjid)
+ self.event('session_start')
+
+ def _handle_probe(self, pres):
+ self.roster[pres['to']][pres['from']].handle_probe(pres)
diff --git a/slixmpp/exceptions.py b/slixmpp/exceptions.py
new file mode 100644
index 00000000..a6c09a0b
--- /dev/null
+++ b/slixmpp/exceptions.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.exceptions
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+
+class XMPPError(Exception):
+
+ """
+ A generic exception that may be raised while processing an XMPP stanza
+ to indicate that an error response stanza should be sent.
+
+ The exception method for stanza objects extending
+ :class:`~slixmpp.stanza.rootstanza.RootStanza` will create an error
+ stanza and initialize any additional substanzas using the extension
+ information included in the exception.
+
+ Meant for use in Slixmpp plugins and applications using Slixmpp.
+
+ Extension information can be included to add additional XML elements
+ to the generated error stanza.
+
+ :param condition: The XMPP defined error condition.
+ Defaults to ``'undefined-condition'``.
+ :param text: Human readable text describing the error.
+ :param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``.
+ Defaults to ``'cancel'``.
+ :param extension: Tag name of the extension's XML content.
+ :param extension_ns: XML namespace of the extensions' XML content.
+ :param extension_args: Content and attributes for the extension
+ element. Same as the additional arguments to
+ the :class:`~xml.etree.ElementTree.Element`
+ constructor.
+ :param clear: Indicates if the stanza's contents should be
+ removed before replying with an error.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, condition='undefined-condition', text='',
+ etype='cancel', extension=None, extension_ns=None,
+ extension_args=None, clear=True):
+ if extension_args is None:
+ extension_args = {}
+
+ self.condition = condition
+ self.text = text
+ self.etype = etype
+ self.clear = clear
+ self.extension = extension
+ self.extension_ns = extension_ns
+ self.extension_args = extension_args
+
+ def format(self):
+ """
+ Format the error in a simple user-readable string.
+ """
+ text = [self.etype, self.condition]
+ if self.text:
+ text.append(self.text)
+ if self.extension:
+ text.append(self.extension)
+ # TODO: handle self.extension_args
+ return ': '.join(text)
+
+
+class IqTimeout(XMPPError):
+
+ """
+ An exception which indicates that an IQ request response has not been
+ received within the alloted time window.
+ """
+
+ def __init__(self, iq):
+ super(IqTimeout, self).__init__(
+ condition='remote-server-timeout',
+ etype='cancel')
+
+ #: The :class:`~slixmpp.stanza.iq.Iq` stanza whose response
+ #: did not arrive before the timeout expired.
+ self.iq = iq
+
+
+class IqError(XMPPError):
+
+ """
+ An exception raised when an Iq stanza of type 'error' is received
+ after making a blocking send call.
+ """
+
+ def __init__(self, iq):
+ super(IqError, self).__init__(
+ condition=iq['error']['condition'],
+ text=iq['error']['text'],
+ etype=iq['error']['type'])
+
+ #: The :class:`~slixmpp.stanza.iq.Iq` error result stanza.
+ self.iq = iq
diff --git a/slixmpp/features/__init__.py b/slixmpp/features/__init__.py
new file mode 100644
index 00000000..5b728ee8
--- /dev/null
+++ b/slixmpp/features/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+__all__ = [
+ 'feature_starttls',
+ 'feature_mechanisms',
+ 'feature_bind',
+ 'feature_session',
+ 'feature_rosterver',
+ 'feature_preapproval'
+]
diff --git a/slixmpp/features/feature_bind/__init__.py b/slixmpp/features/feature_bind/__init__.py
new file mode 100644
index 00000000..65f5b626
--- /dev/null
+++ b/slixmpp/features/feature_bind/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.features.feature_bind.bind import FeatureBind
+from slixmpp.features.feature_bind.stanza import Bind
+
+
+register_plugin(FeatureBind)
+
+
+# Retain some backwards compatibility
+feature_bind = FeatureBind
diff --git a/slixmpp/features/feature_bind/bind.py b/slixmpp/features/feature_bind/bind.py
new file mode 100644
index 00000000..c031ab72
--- /dev/null
+++ b/slixmpp/features/feature_bind/bind.py
@@ -0,0 +1,67 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.jid import JID
+from slixmpp.stanza import Iq, StreamFeatures
+from slixmpp.features.feature_bind import stanza
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+
+
+log = logging.getLogger(__name__)
+
+
+class FeatureBind(BasePlugin):
+
+ name = 'feature_bind'
+ description = 'RFC 6120: Stream Feature: Resource Binding'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_feature('bind',
+ self._handle_bind_resource,
+ restart=False,
+ order=10000)
+
+ register_stanza_plugin(Iq, stanza.Bind)
+ register_stanza_plugin(StreamFeatures, stanza.Bind)
+
+ def _handle_bind_resource(self, features):
+ """
+ Handle requesting a specific resource.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource)
+ self.features = features
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('bind')
+ if self.xmpp.requested_jid.resource:
+ iq['bind']['resource'] = self.xmpp.requested_jid.resource
+
+ iq.send(callback=self._on_bind_response)
+
+ def _on_bind_response(self, response):
+ self.xmpp.boundjid = JID(response['bind']['jid'])
+ self.xmpp.bound = True
+ self.xmpp.event('session_bind', self.xmpp.boundjid)
+ self.xmpp.session_bind_event.set()
+
+ self.xmpp.features.add('bind')
+
+ log.info("JID set to: %s", self.xmpp.boundjid.full)
+
+ if 'session' not in self.features['features']:
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.event('session_start')
diff --git a/slixmpp/features/feature_bind/stanza.py b/slixmpp/features/feature_bind/stanza.py
new file mode 100644
index 00000000..b9ecd97c
--- /dev/null
+++ b/slixmpp/features/feature_bind/stanza.py
@@ -0,0 +1,21 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Bind(ElementBase):
+
+ """
+ """
+
+ name = 'bind'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
+ interfaces = set(('resource', 'jid'))
+ sub_interfaces = interfaces
+ plugin_attrib = 'bind'
diff --git a/slixmpp/features/feature_mechanisms/__init__.py b/slixmpp/features/feature_mechanisms/__init__.py
new file mode 100644
index 00000000..7532eaa2
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/__init__.py
@@ -0,0 +1,22 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms
+from slixmpp.features.feature_mechanisms.stanza import Mechanisms
+from slixmpp.features.feature_mechanisms.stanza import Auth
+from slixmpp.features.feature_mechanisms.stanza import Success
+from slixmpp.features.feature_mechanisms.stanza import Failure
+
+
+register_plugin(FeatureMechanisms)
+
+
+# Retain some backwards compatibility
+feature_mechanisms = FeatureMechanisms
diff --git a/slixmpp/features/feature_mechanisms/mechanisms.py b/slixmpp/features/feature_mechanisms/mechanisms.py
new file mode 100644
index 00000000..8e507afc
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/mechanisms.py
@@ -0,0 +1,249 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import ssl
+import logging
+
+from slixmpp.util import sasl
+from slixmpp.util.stringprep_profiles import StringPrepError
+from slixmpp.stanza import StreamFeatures
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.features.feature_mechanisms import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class FeatureMechanisms(BasePlugin):
+
+ name = 'feature_mechanisms'
+ description = 'RFC 6120: Stream Feature: SASL'
+ dependencies = set()
+ stanza = stanza
+ default_config = {
+ 'use_mech': None,
+ 'use_mechs': None,
+ 'min_mech': None,
+ 'sasl_callback': None,
+ 'security_callback': None,
+ 'encrypted_plain': True,
+ 'unencrypted_plain': False,
+ 'unencrypted_digest': False,
+ 'unencrypted_cram': False,
+ 'unencrypted_scram': True,
+ 'order': 100
+ }
+
+ def plugin_init(self):
+ if self.sasl_callback is None:
+ self.sasl_callback = self._default_credentials
+
+ if self.security_callback is None:
+ self.security_callback = self._default_security
+
+ creds = self.sasl_callback(set(['username']), set())
+ if not self.use_mech and not creds['username']:
+ self.use_mech = 'ANONYMOUS'
+
+ self.mech = None
+ self.mech_list = set()
+ self.attempted_mechs = set()
+
+ register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
+
+ self.xmpp.register_stanza(stanza.Success)
+ self.xmpp.register_stanza(stanza.Failure)
+ self.xmpp.register_stanza(stanza.Auth)
+ self.xmpp.register_stanza(stanza.Challenge)
+ self.xmpp.register_stanza(stanza.Response)
+ self.xmpp.register_stanza(stanza.Abort)
+
+ self.xmpp.register_handler(
+ Callback('SASL Success',
+ MatchXPath(stanza.Success.tag_name()),
+ self._handle_success,
+ instream=True))
+ self.xmpp.register_handler(
+ Callback('SASL Failure',
+ MatchXPath(stanza.Failure.tag_name()),
+ self._handle_fail,
+ instream=True))
+ self.xmpp.register_handler(
+ Callback('SASL Challenge',
+ MatchXPath(stanza.Challenge.tag_name()),
+ self._handle_challenge))
+
+ self.xmpp.register_feature('mechanisms',
+ self._handle_sasl_auth,
+ restart=True,
+ order=self.order)
+
+ def _default_credentials(self, required_values, optional_values):
+ creds = self.xmpp.credentials
+ result = {}
+ values = required_values.union(optional_values)
+ for value in values:
+ if value == 'username':
+ result[value] = creds.get('username', self.xmpp.requested_jid.user)
+ elif value == 'email':
+ jid = self.xmpp.requested_jid.bare
+ result[value] = creds.get('email', jid)
+ elif value == 'channel_binding':
+ if hasattr(self.xmpp.socket, 'get_channel_binding'):
+ result[value] = self.xmpp.socket.get_channel_binding()
+ else:
+ log.debug("Channel binding not supported.")
+ log.debug("Use Python 3.3+ for channel binding and " + \
+ "SCRAM-SHA-1-PLUS support")
+ result[value] = None
+ elif value == 'host':
+ result[value] = creds.get('host', self.xmpp.requested_jid.domain)
+ elif value == 'realm':
+ result[value] = creds.get('realm', self.xmpp.requested_jid.domain)
+ elif value == 'service-name':
+ result[value] = creds.get('service-name', self.xmpp._service_name)
+ elif value == 'service':
+ result[value] = creds.get('service', 'xmpp')
+ elif value in creds:
+ result[value] = creds[value]
+ return result
+
+ def _default_security(self, values):
+ result = {}
+ for value in values:
+ if value == 'encrypted':
+ if 'starttls' in self.xmpp.features:
+ result[value] = True
+ elif isinstance(self.xmpp.socket, ssl.SSLSocket):
+ result[value] = True
+ else:
+ result[value] = False
+ else:
+ result[value] = self.config.get(value, False)
+ return result
+
+ def _handle_sasl_auth(self, features):
+ """
+ Handle authenticating using SASL.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ if 'mechanisms' in self.xmpp.features:
+ # SASL authentication has already succeeded, but the
+ # server has incorrectly offered it again.
+ return False
+
+ enforce_limit = False
+ limited_mechs = self.use_mechs
+
+ if limited_mechs is None:
+ limited_mechs = set()
+ elif limited_mechs and not isinstance(limited_mechs, set):
+ limited_mechs = set(limited_mechs)
+ enforce_limit = True
+
+ if self.use_mech:
+ limited_mechs.add(self.use_mech)
+ enforce_limit = True
+
+ if enforce_limit:
+ self.use_mechs = limited_mechs
+
+ self.mech_list = set(features['mechanisms'])
+
+ return self._send_auth()
+
+ def _send_auth(self):
+ mech_list = self.mech_list - self.attempted_mechs
+ try:
+ self.mech = sasl.choose(mech_list,
+ self.sasl_callback,
+ self.security_callback,
+ limit=self.use_mechs,
+ min_mech=self.min_mech)
+ except sasl.SASLNoAppropriateMechanism:
+ log.error("No appropriate login method.")
+ self.xmpp.event("failed_all_auth")
+ if not self.attempted_mechs:
+ # Only trigger this event if we didn't try at least one
+ # method
+ self.xmpp.event("no_auth")
+ self.attempted_mechs = set()
+ return self.xmpp.disconnect()
+ except StringPrepError:
+ log.exception("A credential value did not pass SASLprep.")
+ self.xmpp.disconnect()
+
+ resp = stanza.Auth(self.xmpp)
+ resp['mechanism'] = self.mech.name
+ try:
+ resp['value'] = self.mech.process()
+ except sasl.SASLCancelled:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
+ except sasl.SASLMutualAuthFailed:
+ log.error("Mutual authentication failed! " + \
+ "A security breach is possible.")
+ self.attempted_mechs.add(self.mech.name)
+ self.xmpp.disconnect()
+ except sasl.SASLFailed:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
+ else:
+ resp.send()
+
+ return True
+
+ def _handle_challenge(self, stanza):
+ """SASL challenge received. Process and send response."""
+ resp = self.stanza.Response(self.xmpp)
+ try:
+ resp['value'] = self.mech.process(stanza['value'])
+ except sasl.SASLCancelled:
+ self.stanza.Abort(self.xmpp).send()
+ except sasl.SASLMutualAuthFailed:
+ log.error("Mutual authentication failed! " + \
+ "A security breach is possible.")
+ self.attempted_mechs.add(self.mech.name)
+ self.xmpp.disconnect()
+ except sasl.SASLFailed:
+ self.stanza.Abort(self.xmpp).send()
+ else:
+ if resp.get_value() == '':
+ resp.del_value()
+ resp.send()
+
+ def _handle_success(self, stanza):
+ """SASL authentication succeeded. Restart the stream."""
+ try:
+ final = self.mech.process(stanza['value'])
+ except sasl.SASLMutualAuthFailed:
+ log.error("Mutual authentication failed! " + \
+ "A security breach is possible.")
+ self.attempted_mechs.add(self.mech.name)
+ self.xmpp.disconnect()
+ else:
+ self.attempted_mechs = set()
+ self.xmpp.authenticated = True
+ self.xmpp.features.add('mechanisms')
+ self.xmpp.event('auth_success', stanza)
+ # Restart the stream
+ self.xmpp.init_parser()
+ self.xmpp.send_raw(self.xmpp.stream_header)
+
+ def _handle_fail(self, stanza):
+ """SASL authentication failed. Disconnect and shutdown."""
+ self.attempted_mechs.add(self.mech.name)
+ log.info("Authentication failed: %s", stanza['condition'])
+ self.xmpp.event("failed_auth", stanza)
+ self._send_auth()
+ return True
diff --git a/slixmpp/features/feature_mechanisms/stanza/__init__.py b/slixmpp/features/feature_mechanisms/stanza/__init__.py
new file mode 100644
index 00000000..4d515bf2
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms
+from slixmpp.features.feature_mechanisms.stanza.auth import Auth
+from slixmpp.features.feature_mechanisms.stanza.success import Success
+from slixmpp.features.feature_mechanisms.stanza.failure import Failure
+from slixmpp.features.feature_mechanisms.stanza.challenge import Challenge
+from slixmpp.features.feature_mechanisms.stanza.response import Response
+from slixmpp.features.feature_mechanisms.stanza.abort import Abort
diff --git a/slixmpp/features/feature_mechanisms/stanza/abort.py b/slixmpp/features/feature_mechanisms/stanza/abort.py
new file mode 100644
index 00000000..fca29aee
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/abort.py
@@ -0,0 +1,24 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import StanzaBase
+
+
+class Abort(StanzaBase):
+
+ """
+ """
+
+ name = 'abort'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set()
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
diff --git a/slixmpp/features/feature_mechanisms/stanza/auth.py b/slixmpp/features/feature_mechanisms/stanza/auth.py
new file mode 100644
index 00000000..c32069ec
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/auth.py
@@ -0,0 +1,49 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import StanzaBase
+
+
+class Auth(StanzaBase):
+
+ """
+ """
+
+ name = 'auth'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('mechanism', 'value'))
+ plugin_attrib = name
+
+ #: Some SASL mechs require sending values as is,
+ #: without converting base64.
+ plain_mechs = set(['X-MESSENGER-OAUTH2'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ if not self['mechanism'] in self.plain_mechs:
+ return base64.b64decode(bytes(self.xml.text))
+ else:
+ return self.xml.text
+
+ def set_value(self, values):
+ if not self['mechanism'] in self.plain_mechs:
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ elif values == b'':
+ self.xml.text = '='
+ else:
+ self.xml.text = bytes(values).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/slixmpp/features/feature_mechanisms/stanza/challenge.py b/slixmpp/features/feature_mechanisms/stanza/challenge.py
new file mode 100644
index 00000000..21a061ee
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/challenge.py
@@ -0,0 +1,39 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import StanzaBase
+
+
+class Challenge(StanzaBase):
+
+ """
+ """
+
+ name = 'challenge'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('value',))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/slixmpp/features/feature_mechanisms/stanza/failure.py b/slixmpp/features/feature_mechanisms/stanza/failure.py
new file mode 100644
index 00000000..cc0ac877
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/failure.py
@@ -0,0 +1,76 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import StanzaBase, ET
+
+
+class Failure(StanzaBase):
+
+ """
+ """
+
+ name = 'failure'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('condition', 'text'))
+ plugin_attrib = name
+ sub_interfaces = set(('text',))
+ conditions = set(('aborted', 'account-disabled', 'credentials-expired',
+ 'encryption-required', 'incorrect-encoding', 'invalid-authzid',
+ 'invalid-mechanism', 'malformed-request', 'mechansism-too-weak',
+ 'not-authorized', 'temporary-auth-failure'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup.
+
+ Sets a default error type and condition, and changes the
+ parent stanza's type to 'error'.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # StanzaBase overrides self.namespace
+ self.namespace = Failure.namespace
+
+ if StanzaBase.setup(self, xml):
+ #If we had to generate XML then set default values.
+ self['condition'] = 'not-authorized'
+
+ self.xml.tag = self.tag_name()
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.xml:
+ if "{%s}" % self.namespace in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return 'not-authorized'
+
+ def set_condition(self, value):
+ """
+ Set the tag name of the condition element.
+
+ Arguments:
+ value -- The tag name of the condition element.
+ """
+ if value in self.conditions:
+ del self['condition']
+ self.xml.append(ET.Element("{%s}%s" % (self.namespace, value)))
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.xml:
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.xml.remove(child)
+ return self
diff --git a/slixmpp/features/feature_mechanisms/stanza/mechanisms.py b/slixmpp/features/feature_mechanisms/stanza/mechanisms.py
new file mode 100644
index 00000000..4437e155
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/mechanisms.py
@@ -0,0 +1,53 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Mechanisms(ElementBase):
+
+ """
+ """
+
+ name = 'mechanisms'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('mechanisms', 'required'))
+ plugin_attrib = name
+ is_extension = True
+
+ def get_required(self):
+ """
+ """
+ return True
+
+ def get_mechanisms(self):
+ """
+ """
+ results = []
+ mechs = self.findall('{%s}mechanism' % self.namespace)
+ if mechs:
+ for mech in mechs:
+ results.append(mech.text)
+ return results
+
+ def set_mechanisms(self, values):
+ """
+ """
+ self.del_mechanisms()
+ for val in values:
+ mech = ET.Element('{%s}mechanism' % self.namespace)
+ mech.text = val
+ self.append(mech)
+
+ def del_mechanisms(self):
+ """
+ """
+ mechs = self.findall('{%s}mechanism' % self.namespace)
+ if mechs:
+ for mech in mechs:
+ self.xml.remove(mech)
diff --git a/slixmpp/features/feature_mechanisms/stanza/response.py b/slixmpp/features/feature_mechanisms/stanza/response.py
new file mode 100644
index 00000000..8da236ba
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/response.py
@@ -0,0 +1,39 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import StanzaBase
+
+
+class Response(StanzaBase):
+
+ """
+ """
+
+ name = 'response'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('value',))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/slixmpp/features/feature_mechanisms/stanza/success.py b/slixmpp/features/feature_mechanisms/stanza/success.py
new file mode 100644
index 00000000..f7cde0f8
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/stanza/success.py
@@ -0,0 +1,38 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import StanzaBase
+
+class Success(StanzaBase):
+
+ """
+ """
+
+ name = 'success'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(['value'])
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ if values:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = '='
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/slixmpp/features/feature_preapproval/__init__.py b/slixmpp/features/feature_preapproval/__init__.py
new file mode 100644
index 00000000..f22be050
--- /dev/null
+++ b/slixmpp/features/feature_preapproval/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.features.feature_preapproval.preapproval import FeaturePreApproval
+from slixmpp.features.feature_preapproval.stanza import PreApproval
+
+
+register_plugin(FeaturePreApproval)
diff --git a/slixmpp/features/feature_preapproval/preapproval.py b/slixmpp/features/feature_preapproval/preapproval.py
new file mode 100644
index 00000000..1d60d7e7
--- /dev/null
+++ b/slixmpp/features/feature_preapproval/preapproval.py
@@ -0,0 +1,42 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import StreamFeatures
+from slixmpp.features.feature_preapproval import stanza
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin
+
+
+log = logging.getLogger(__name__)
+
+
+class FeaturePreApproval(BasePlugin):
+
+ name = 'feature_preapproval'
+ description = 'RFC 6121: Stream Feature: Subscription Pre-Approval'
+ dependences = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_feature('preapproval',
+ self._handle_preapproval,
+ restart=False,
+ order=9001)
+
+ register_stanza_plugin(StreamFeatures, stanza.PreApproval)
+
+ def _handle_preapproval(self, features):
+ """Save notice that the server support subscription pre-approvals.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ log.debug("Server supports subscription pre-approvals.")
+ self.xmpp.features.add('preapproval')
diff --git a/slixmpp/features/feature_preapproval/stanza.py b/slixmpp/features/feature_preapproval/stanza.py
new file mode 100644
index 00000000..03d721ef
--- /dev/null
+++ b/slixmpp/features/feature_preapproval/stanza.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class PreApproval(ElementBase):
+
+ name = 'sub'
+ namespace = 'urn:xmpp:features:pre-approval'
+ interfaces = set()
+ plugin_attrib = 'preapproval'
diff --git a/slixmpp/features/feature_rosterver/__init__.py b/slixmpp/features/feature_rosterver/__init__.py
new file mode 100644
index 00000000..d338b584
--- /dev/null
+++ b/slixmpp/features/feature_rosterver/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.features.feature_rosterver.rosterver import FeatureRosterVer
+from slixmpp.features.feature_rosterver.stanza import RosterVer
+
+
+register_plugin(FeatureRosterVer)
+
+
+# Retain some backwards compatibility
+feature_rosterver = FeatureRosterVer
diff --git a/slixmpp/features/feature_rosterver/rosterver.py b/slixmpp/features/feature_rosterver/rosterver.py
new file mode 100644
index 00000000..2c2c8c84
--- /dev/null
+++ b/slixmpp/features/feature_rosterver/rosterver.py
@@ -0,0 +1,42 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import StreamFeatures
+from slixmpp.features.feature_rosterver import stanza
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin
+
+
+log = logging.getLogger(__name__)
+
+
+class FeatureRosterVer(BasePlugin):
+
+ name = 'feature_rosterver'
+ description = 'RFC 6121: Stream Feature: Roster Versioning'
+ dependences = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_feature('rosterver',
+ self._handle_rosterver,
+ restart=False,
+ order=9000)
+
+ register_stanza_plugin(StreamFeatures, stanza.RosterVer)
+
+ def _handle_rosterver(self, features):
+ """Enable using roster versioning.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ log.debug("Enabling roster versioning.")
+ self.xmpp.features.add('rosterver')
diff --git a/slixmpp/features/feature_rosterver/stanza.py b/slixmpp/features/feature_rosterver/stanza.py
new file mode 100644
index 00000000..c9a4a2da
--- /dev/null
+++ b/slixmpp/features/feature_rosterver/stanza.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class RosterVer(ElementBase):
+
+ name = 'ver'
+ namespace = 'urn:xmpp:features:rosterver'
+ interfaces = set()
+ plugin_attrib = 'rosterver'
diff --git a/slixmpp/features/feature_session/__init__.py b/slixmpp/features/feature_session/__init__.py
new file mode 100644
index 00000000..0ac950c6
--- /dev/null
+++ b/slixmpp/features/feature_session/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.features.feature_session.session import FeatureSession
+from slixmpp.features.feature_session.stanza import Session
+
+
+register_plugin(FeatureSession)
+
+
+# Retain some backwards compatibility
+feature_session = FeatureSession
diff --git a/slixmpp/features/feature_session/session.py b/slixmpp/features/feature_session/session.py
new file mode 100644
index 00000000..0635455a
--- /dev/null
+++ b/slixmpp/features/feature_session/session.py
@@ -0,0 +1,54 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import Iq, StreamFeatures
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+
+from slixmpp.features.feature_session import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class FeatureSession(BasePlugin):
+
+ name = 'feature_session'
+ description = 'RFC 3920: Stream Feature: Start Session'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_feature('session',
+ self._handle_start_session,
+ restart=False,
+ order=10001)
+
+ register_stanza_plugin(Iq, stanza.Session)
+ register_stanza_plugin(StreamFeatures, stanza.Session)
+
+ def _handle_start_session(self, features):
+ """
+ Handle the start of the session.
+
+ Arguments:
+ feature -- The stream features element.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('session')
+ iq.send(callback=self._on_start_session_response)
+
+ def _on_start_session_response(self, response):
+ self.xmpp.features.add('session')
+
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.event('session_start')
diff --git a/slixmpp/features/feature_session/stanza.py b/slixmpp/features/feature_session/stanza.py
new file mode 100644
index 00000000..f68483d6
--- /dev/null
+++ b/slixmpp/features/feature_session/stanza.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Session(ElementBase):
+
+ """
+ """
+
+ name = 'session'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-session'
+ interfaces = set()
+ plugin_attrib = 'session'
diff --git a/slixmpp/features/feature_starttls/__init__.py b/slixmpp/features/feature_starttls/__init__.py
new file mode 100644
index 00000000..81a88650
--- /dev/null
+++ b/slixmpp/features/feature_starttls/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.features.feature_starttls.starttls import FeatureSTARTTLS
+from slixmpp.features.feature_starttls.stanza import *
+
+
+register_plugin(FeatureSTARTTLS)
+
+
+# Retain some backwards compatibility
+feature_starttls = FeatureSTARTTLS
diff --git a/slixmpp/features/feature_starttls/stanza.py b/slixmpp/features/feature_starttls/stanza.py
new file mode 100644
index 00000000..df50897e
--- /dev/null
+++ b/slixmpp/features/feature_starttls/stanza.py
@@ -0,0 +1,45 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import StanzaBase, ElementBase
+
+
+class STARTTLS(ElementBase):
+
+ """
+ """
+
+ name = 'starttls'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set(('required',))
+ plugin_attrib = name
+
+ def get_required(self):
+ """
+ """
+ return True
+
+
+class Proceed(StanzaBase):
+
+ """
+ """
+
+ name = 'proceed'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set()
+
+
+class Failure(StanzaBase):
+
+ """
+ """
+
+ name = 'failure'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set()
diff --git a/slixmpp/features/feature_starttls/starttls.py b/slixmpp/features/feature_starttls/starttls.py
new file mode 100644
index 00000000..d472dad7
--- /dev/null
+++ b/slixmpp/features/feature_starttls/starttls.py
@@ -0,0 +1,65 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import StreamFeatures
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.features.feature_starttls import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class FeatureSTARTTLS(BasePlugin):
+
+ name = 'feature_starttls'
+ description = 'RFC 6120: Stream Feature: STARTTLS'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('STARTTLS Proceed',
+ MatchXPath(stanza.Proceed.tag_name()),
+ self._handle_starttls_proceed,
+ instream=True))
+ self.xmpp.register_feature('starttls',
+ self._handle_starttls,
+ restart=True,
+ order=self.config.get('order', 0))
+
+ self.xmpp.register_stanza(stanza.Proceed)
+ self.xmpp.register_stanza(stanza.Failure)
+ register_stanza_plugin(StreamFeatures, stanza.STARTTLS)
+
+ def _handle_starttls(self, features):
+ """
+ Handle notification that the server supports TLS.
+
+ Arguments:
+ features -- The stream:features element.
+ """
+ if 'starttls' in self.xmpp.features:
+ # We have already negotiated TLS, but the server is
+ # offering it again, against spec.
+ return False
+ elif self.xmpp.disable_starttls:
+ return False
+ else:
+ self.xmpp.send(features['starttls'])
+ return True
+
+ def _handle_starttls_proceed(self, proceed):
+ """Restart the XML stream when TLS is accepted."""
+ log.debug("Starting TLS")
+ if self.xmpp.start_tls():
+ self.xmpp.features.add('starttls')
diff --git a/slixmpp/jid.py b/slixmpp/jid.py
new file mode 100644
index 00000000..2e23e242
--- /dev/null
+++ b/slixmpp/jid.py
@@ -0,0 +1,449 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.jid
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module allows for working with Jabber IDs (JIDs).
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import re
+import socket
+
+from copy import deepcopy
+from functools import lru_cache
+
+from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError
+
+HAVE_INET_PTON = hasattr(socket, 'inet_pton')
+
+#: The basic regex pattern that a JID must match in order to determine
+#: the local, domain, and resource parts. This regex does NOT do any
+#: validation, which requires application of nodeprep, resourceprep, etc.
+JID_PATTERN = re.compile(
+ "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
+)
+
+#: The set of escape sequences for the characters not allowed by nodeprep.
+JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f',
+ '\\3a', '\\3c', '\\3e', '\\40', '\\5c'}
+
+#: The reverse mapping of escape sequences to their original forms.
+JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
+ '\\22': '"',
+ '\\26': '&',
+ '\\27': "'",
+ '\\2f': '/',
+ '\\3a': ':',
+ '\\3c': '<',
+ '\\3e': '>',
+ '\\40': '@',
+ '\\5c': '\\'}
+
+
+# TODO: Find the best cache size for a standard usage.
+@lru_cache(maxsize=1024)
+def _parse_jid(data):
+ """
+ Parse string data into the node, domain, and resource
+ components of a JID, if possible.
+
+ :param string data: A string that is potentially a JID.
+
+ :raises InvalidJID:
+
+ :returns: tuple of the validated local, domain, and resource strings
+ """
+ match = JID_PATTERN.match(data)
+ if not match:
+ raise InvalidJID('JID could not be parsed')
+
+ (node, domain, resource) = match.groups()
+
+ node = _validate_node(node)
+ domain = _validate_domain(domain)
+ resource = _validate_resource(resource)
+
+ return node, domain, resource
+
+
+def _validate_node(node):
+ """Validate the local, or username, portion of a JID.
+
+ :raises InvalidJID:
+
+ :returns: The local portion of a JID, as validated by nodeprep.
+ """
+ if node is None:
+ return None
+
+ try:
+ node = nodeprep(node)
+ except StringprepError:
+ raise InvalidJID('Nodeprep failed')
+
+ if not node:
+ raise InvalidJID('Localpart must not be 0 bytes')
+ if len(node) > 1023:
+ raise InvalidJID('Localpart must be less than 1024 bytes')
+ return node
+
+
+def _validate_domain(domain):
+ """Validate the domain portion of a JID.
+
+ IP literal addresses are left as-is, if valid. Domain names
+ are stripped of any trailing label separators (`.`), and are
+ checked with the nameprep profile of stringprep. If the given
+ domain is actually a punyencoded version of a domain name, it
+ is converted back into its original Unicode form. Domains must
+ also not start or end with a dash (`-`).
+
+ :raises InvalidJID:
+
+ :returns: The validated domain name
+ """
+ ip_addr = False
+
+ # First, check if this is an IPv4 address
+ try:
+ socket.inet_aton(domain)
+ ip_addr = True
+ except socket.error:
+ pass
+
+ # Check if this is an IPv6 address
+ if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']':
+ try:
+ ip = domain[1:-1]
+ socket.inet_pton(socket.AF_INET6, ip)
+ ip_addr = True
+ except (socket.error, ValueError):
+ pass
+
+ if not ip_addr:
+ # This is a domain name, which must be checked further
+
+ if domain and domain[-1] == '.':
+ domain = domain[:-1]
+
+ try:
+ domain = idna(domain)
+ except StringprepError:
+ raise InvalidJID('idna validation failed')
+
+ if ':' in domain:
+ raise InvalidJID('Domain containing a port')
+ for label in domain.split('.'):
+ if not label:
+ raise InvalidJID('Domain containing too many dots')
+ if '-' in (label[0], label[-1]):
+ raise InvalidJID('Domain started or ended with -')
+
+ if not domain:
+ raise InvalidJID('Domain must not be 0 bytes')
+ if len(domain) > 1023:
+ raise InvalidJID('Domain must be less than 1024 bytes')
+
+ return domain
+
+
+def _validate_resource(resource):
+ """Validate the resource portion of a JID.
+
+ :raises InvalidJID:
+
+ :returns: The local portion of a JID, as validated by resourceprep.
+ """
+ if resource is None:
+ return None
+
+ try:
+ resource = resourceprep(resource)
+ except StringprepError:
+ raise InvalidJID('Resourceprep failed')
+
+ if not resource:
+ raise InvalidJID('Resource must not be 0 bytes')
+ if len(resource) > 1023:
+ raise InvalidJID('Resource must be less than 1024 bytes')
+ return resource
+
+
+def _unescape_node(node):
+ """Unescape a local portion of a JID.
+
+ .. note::
+ The unescaped local portion is meant ONLY for presentation,
+ and should not be used for other purposes.
+ """
+ unescaped = []
+ seq = ''
+ for i, char in enumerate(node):
+ if char == '\\':
+ seq = node[i:i+3]
+ if seq not in JID_ESCAPE_SEQUENCES:
+ seq = ''
+ if seq:
+ if len(seq) == 3:
+ unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char))
+
+ # Pop character off the escape sequence, and ignore it
+ seq = seq[1:]
+ else:
+ unescaped.append(char)
+ return ''.join(unescaped)
+
+
+def _format_jid(local=None, domain=None, resource=None):
+ """Format the given JID components into a full or bare JID.
+
+ :param string local: Optional. The local portion of the JID.
+ :param string domain: Required. The domain name portion of the JID.
+ :param strin resource: Optional. The resource portion of the JID.
+
+ :return: A full or bare JID string.
+ """
+ result = []
+ if local is not None:
+ result.append(local)
+ result.append('@')
+ if domain is not None:
+ result.append(domain)
+ if resource is not None:
+ result.append('/')
+ result.append(resource)
+ return ''.join(result)
+
+
+class InvalidJID(ValueError):
+ """
+ Raised when attempting to create a JID that does not pass validation.
+
+ It can also be raised if modifying an existing JID in such a way as
+ to make it invalid, such trying to remove the domain from an existing
+ full JID while the local and resource portions still exist.
+ """
+
+# pylint: disable=R0903
+class UnescapedJID:
+
+ """
+ .. versionadded:: 1.1.10
+ """
+
+ __slots__ = ('_node', '_domain', '_resource')
+
+ def __init__(self, node, domain, resource):
+ self._node = node
+ self._domain = domain
+ self._resource = resource
+
+ def __getattribute__(self, name):
+ """Retrieve the given JID component.
+
+ :param name: one of: user, server, domain, resource,
+ full, or bare.
+ """
+ if name == 'resource':
+ return self._resource or ''
+ if name in ('user', 'username', 'local', 'node'):
+ return self._node or ''
+ if name in ('server', 'domain', 'host'):
+ return self._domain or ''
+ if name in ('full', 'jid'):
+ return _format_jid(self._node, self._domain, self._resource)
+ if name == 'bare':
+ return _format_jid(self._node, self._domain)
+ return object.__getattribute__(self, name)
+
+ def __str__(self):
+ """Use the full JID as the string value."""
+ return _format_jid(self._node, self._domain, self._resource)
+
+ def __repr__(self):
+ """Use the full JID as the representation."""
+ return _format_jid(self._node, self._domain, self._resource)
+
+
+class JID:
+
+ """
+ A representation of a Jabber ID, or JID.
+
+ Each JID may have three components: a user, a domain, and an optional
+ resource. For example: user@domain/resource
+
+ When a resource is not used, the JID is called a bare JID.
+ The JID is a full JID otherwise.
+
+ **JID Properties:**
+ :full: The string value of the full JID.
+ :jid: Alias for ``full``.
+ :bare: The string value of the bare JID.
+ :node: The node portion of the JID.
+ :user: Alias for ``node``.
+ :local: Alias for ``node``.
+ :username: Alias for ``node``.
+ :domain: The domain name portion of the JID.
+ :server: Alias for ``domain``.
+ :host: Alias for ``domain``.
+ :resource: The resource portion of the JID.
+
+ :param string jid:
+ A string of the form ``'[user@]domain[/resource]'``.
+
+ :raises InvalidJID:
+ """
+
+ __slots__ = ('_node', '_domain', '_resource')
+
+ def __init__(self, jid=None):
+ if not jid:
+ self._node = None
+ self._domain = None
+ self._resource = None
+ elif not isinstance(jid, JID):
+ self._node, self._domain, self._resource = _parse_jid(jid)
+ else:
+ self._node = jid._node
+ self._domain = jid._domain
+ self._resource = jid._resource
+
+ def unescape(self):
+ """Return an unescaped JID object.
+
+ Using an unescaped JID is preferred for displaying JIDs
+ to humans, and they should NOT be used for any other
+ purposes than for presentation.
+
+ :return: :class:`UnescapedJID`
+
+ .. versionadded:: 1.1.10
+ """
+ return UnescapedJID(_unescape_node(self._node),
+ self._domain,
+ self._resource)
+
+ @property
+ def node(self):
+ return self._node or ''
+
+ @property
+ def user(self):
+ return self._node or ''
+
+ @property
+ def local(self):
+ return self._node or ''
+
+ @property
+ def username(self):
+ return self._node or ''
+
+ @property
+ def domain(self):
+ return self._domain or ''
+
+ @property
+ def server(self):
+ return self._domain or ''
+
+ @property
+ def host(self):
+ return self._domain or ''
+
+ @property
+ def resource(self):
+ return self._resource or ''
+
+ @property
+ def bare(self):
+ return _format_jid(self._node, self._domain)
+
+ @property
+ def full(self):
+ return _format_jid(self._node, self._domain, self._resource)
+
+ @property
+ def jid(self):
+ return _format_jid(self._node, self._domain, self._resource)
+
+ @node.setter
+ def node(self, value):
+ self._node = _validate_node(value)
+
+ @user.setter
+ def user(self, value):
+ self._node = _validate_node(value)
+
+ @local.setter
+ def local(self, value):
+ self._node = _validate_node(value)
+
+ @username.setter
+ def username(self, value):
+ self._node = _validate_node(value)
+
+ @domain.setter
+ def domain(self, value):
+ self._domain = _validate_domain(value)
+
+ @server.setter
+ def server(self, value):
+ self._domain = _validate_domain(value)
+
+ @host.setter
+ def host(self, value):
+ self._domain = _validate_domain(value)
+
+ @bare.setter
+ def bare(self, value):
+ node, domain, resource = _parse_jid(value)
+ assert not resource
+ self._node = node
+ self._domain = domain
+
+ @resource.setter
+ def resource(self, value):
+ self._resource = _validate_resource(value)
+
+ @full.setter
+ def full(self, value):
+ self._node, self._domain, self._resource = _parse_jid(value)
+
+ @jid.setter
+ def jid(self, value):
+ self._node, self._domain, self._resource = _parse_jid(value)
+
+ def __str__(self):
+ """Use the full JID as the string value."""
+ return _format_jid(self._node, self._domain, self._resource)
+
+ def __repr__(self):
+ """Use the full JID as the representation."""
+ return _format_jid(self._node, self._domain, self._resource)
+
+ # pylint: disable=W0212
+ def __eq__(self, other):
+ """Two JIDs are equal if they have the same full JID value."""
+ if isinstance(other, UnescapedJID):
+ return False
+ if not isinstance(other, JID):
+ other = JID(other)
+
+ return (self._node == other._node and
+ self._domain == other._domain and
+ self._resource == other._resource)
+
+ def __ne__(self, other):
+ """Two JIDs are considered unequal if they are not equal."""
+ return not self == other
+
+ def __hash__(self):
+ """Hash a JID based on the string version of its full JID."""
+ return hash(_format_jid(self._node, self._domain, self._resource))
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
new file mode 100644
index 00000000..d28cf281
--- /dev/null
+++ b/slixmpp/plugins/__init__.py
@@ -0,0 +1,88 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin
+from slixmpp.plugins.base import register_plugin, load_plugin
+
+
+__all__ = [
+ # XEPS
+ 'xep_0004', # Data Forms
+ 'xep_0009', # Jabber-RPC
+ 'xep_0012', # Last Activity
+ 'xep_0013', # Flexible Offline Message Retrieval
+ 'xep_0016', # Privacy Lists
+ 'xep_0020', # Feature Negotiation
+ 'xep_0027', # Current Jabber OpenPGP Usage
+ 'xep_0030', # Service Discovery
+ 'xep_0033', # Extended Stanza Addresses
+ 'xep_0045', # Multi-User Chat (Client)
+ 'xep_0047', # In-Band Bytestreams
+ 'xep_0048', # Bookmarks
+ 'xep_0049', # Private XML Storage
+ 'xep_0050', # Ad-hoc Commands
+ 'xep_0054', # vcard-temp
+ 'xep_0059', # Result Set Management
+ 'xep_0060', # Pubsub (Client)
+ 'xep_0065', # SOCKS5 Bytestreams
+ 'xep_0066', # Out of Band Data
+ 'xep_0071', # XHTML-IM
+ 'xep_0077', # In-Band Registration
+# 'xep_0078', # Non-SASL auth. Don't automatically load
+ 'xep_0079', # Advanced Message Processing
+ 'xep_0080', # User Location
+ 'xep_0082', # XMPP Date and Time Profiles
+ 'xep_0084', # User Avatar
+ 'xep_0085', # Chat State Notifications
+ 'xep_0086', # Legacy Error Codes
+ 'xep_0091', # Legacy Delayed Delivery
+ 'xep_0092', # Software Version
+ 'xep_0106', # JID Escaping
+ 'xep_0107', # User Mood
+ 'xep_0108', # User Activity
+ 'xep_0115', # Entity Capabilities
+ 'xep_0118', # User Tune
+ 'xep_0122', # Data Forms Validation
+ 'xep_0128', # Extended Service Discovery
+ 'xep_0131', # Standard Headers and Internet Metadata
+ 'xep_0133', # Service Administration
+ 'xep_0152', # Reachability Addresses
+ 'xep_0153', # vCard-Based Avatars
+ 'xep_0163', # Personal Eventing Protocol
+ 'xep_0172', # User Nickname
+ 'xep_0184', # Message Receipts
+ 'xep_0186', # Invisible Command
+ 'xep_0191', # Blocking Command
+ 'xep_0196', # User Gaming
+ 'xep_0198', # Stream Management
+ 'xep_0199', # Ping
+ 'xep_0202', # Entity Time
+ 'xep_0203', # Delayed Delivery
+ 'xep_0221', # Data Forms Media Element
+ 'xep_0222', # Persistent Storage of Public Data via Pubsub
+ 'xep_0223', # Persistent Storage of Private Data via Pubsub
+ 'xep_0224', # Attention
+ 'xep_0231', # Bits of Binary
+ 'xep_0235', # OAuth Over XMPP
+ 'xep_0242', # XMPP Client Compliance 2009
+ 'xep_0249', # Direct MUC Invitations
+ 'xep_0256', # Last Activity in Presence
+ 'xep_0257', # Client Certificate Management for SASL EXTERNAL
+ 'xep_0258', # Security Labels in XMPP
+ 'xep_0270', # XMPP Compliance Suites 2010
+ 'xep_0279', # Server IP Check
+ 'xep_0280', # Message Carbons
+ 'xep_0297', # Stanza Forwarding
+ 'xep_0302', # XMPP Compliance Suites 2012
+ 'xep_0308', # Last Message Correction
+ 'xep_0313', # Message Archive Management
+ 'xep_0319', # Last User Interaction in Presence
+ 'xep_0323', # IoT Systems Sensor Data
+ 'xep_0325', # IoT Systems Control
+ 'xep_0332', # HTTP Over XMPP Transport
+]
diff --git a/slixmpp/plugins/base.py b/slixmpp/plugins/base.py
new file mode 100644
index 00000000..0fe083bc
--- /dev/null
+++ b/slixmpp/plugins/base.py
@@ -0,0 +1,352 @@
+# -*- encoding: utf-8 -*-
+
+"""
+ slixmpp.plugins.base
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to client connections.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import sys
+import copy
+import logging
+import threading
+
+
+log = logging.getLogger(__name__)
+
+
+#: Associate short string names of plugins with implementations. The
+#: plugin names are based on the spec used by the plugin, such as
+#: `'xep_0030'` for a plugin that implements XEP-0030.
+PLUGIN_REGISTRY = {}
+
+#: In order to do cascading plugin disabling, reverse dependencies
+#: must be tracked.
+PLUGIN_DEPENDENTS = {}
+
+#: Only allow one thread to manipulate the plugin registry at a time.
+REGISTRY_LOCK = threading.RLock()
+
+
+class PluginNotFound(Exception):
+ """Raised if an unknown plugin is accessed."""
+
+
+def register_plugin(impl, name=None):
+ """Add a new plugin implementation to the registry.
+
+ :param class impl: The plugin class.
+
+ The implementation class must provide a :attr:`~BasePlugin.name`
+ value that will be used as a short name for enabling and disabling
+ the plugin. The name should be based on the specification used by
+ the plugin. For example, a plugin implementing XEP-0030 would be
+ named `'xep_0030'`.
+ """
+ if name is None:
+ name = impl.name
+ with REGISTRY_LOCK:
+ PLUGIN_REGISTRY[name] = impl
+ if name not in PLUGIN_DEPENDENTS:
+ PLUGIN_DEPENDENTS[name] = set()
+ for dep in impl.dependencies:
+ if dep not in PLUGIN_DEPENDENTS:
+ PLUGIN_DEPENDENTS[dep] = set()
+ PLUGIN_DEPENDENTS[dep].add(name)
+
+
+def load_plugin(name, module=None):
+ """Find and import a plugin module so that it can be registered.
+
+ This function is called to import plugins that have selected for
+ enabling, but no matching registered plugin has been found.
+
+ :param str name: The name of the plugin. It is expected that
+ plugins are in packages matching their name,
+ even though the plugin class name does not
+ have to match.
+ :param str module: The name of the base module to search
+ for the plugin.
+ """
+ try:
+ if not module:
+ try:
+ module = 'slixmpp.plugins.%s' % name
+ __import__(module)
+ mod = sys.modules[module]
+ except ImportError:
+ module = 'slixmpp.features.%s' % name
+ __import__(module)
+ mod = sys.modules[module]
+ elif isinstance(module, str):
+ __import__(module)
+ mod = sys.modules[module]
+ else:
+ mod = module
+
+ # Add older style plugins to the registry.
+ if hasattr(mod, name):
+ plugin = getattr(mod, name)
+ if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
+ plugin.name = name
+ # Mark the plugin as an older style plugin so
+ # we can work around dependency issues.
+ plugin.old_style = True
+ register_plugin(plugin, name)
+ except ImportError:
+ log.exception("Unable to load plugin: %s", name)
+
+
+class PluginManager(object):
+ def __init__(self, xmpp, config=None):
+ #: We will track all enabled plugins in a set so that we
+ #: can enable plugins in batches and pull in dependencies
+ #: without problems.
+ self._enabled = set()
+
+ #: Maintain references to active plugins.
+ self._plugins = {}
+
+ self._plugin_lock = threading.RLock()
+
+ #: Globally set default plugin configuration. This will
+ #: be used for plugins that are auto-enabled through
+ #: dependency loading.
+ self.config = config if config else {}
+
+ self.xmpp = xmpp
+
+ def register(self, plugin, enable=True):
+ """Register a new plugin, and optionally enable it.
+
+ :param class plugin: The implementation class of the plugin
+ to register.
+ :param bool enable: If ``True``, immediately enable the
+ plugin after registration.
+ """
+ register_plugin(plugin)
+ if enable:
+ self.enable(plugin.name)
+
+ def enable(self, name, config=None, enabled=None):
+ """Enable a plugin, including any dependencies.
+
+ :param string name: The short name of the plugin.
+ :param dict config: Optional settings dictionary for
+ configuring plugin behaviour.
+ """
+ if enabled is None:
+ enabled = set()
+
+ with self._plugin_lock:
+ if name not in self._enabled:
+ enabled.add(name)
+ self._enabled.add(name)
+ if not self.registered(name):
+ load_plugin(name)
+
+ plugin_class = PLUGIN_REGISTRY.get(name, None)
+ if not plugin_class:
+ raise PluginNotFound(name)
+
+ if config is None:
+ config = self.config.get(name, None)
+
+ plugin = plugin_class(self.xmpp, config)
+ self._plugins[name] = plugin
+ for dep in plugin.dependencies:
+ self.enable(dep, enabled=enabled)
+ plugin._init()
+
+ for name in enabled:
+ if hasattr(self._plugins[name], 'old_style'):
+ # Older style plugins require post_init()
+ # to run just before stream processing begins,
+ # so we don't call it here.
+ pass
+ else:
+ self._plugins[name].post_init()
+
+ def enable_all(self, names=None, config=None):
+ """Enable all registered plugins.
+
+ :param list names: A list of plugin names to enable. If
+ none are provided, all registered plugins
+ will be enabled.
+ :param dict config: A dictionary mapping plugin names to
+ configuration dictionaries, as used by
+ :meth:`~PluginManager.enable`.
+ """
+ names = names if names else PLUGIN_REGISTRY.keys()
+ if config is None:
+ config = {}
+ for name in names:
+ self.enable(name, config.get(name, {}))
+
+ def enabled(self, name):
+ """Check if a plugin has been enabled.
+
+ :param string name: The name of the plugin to check.
+ :return: boolean
+ """
+ return name in self._enabled
+
+ def registered(self, name):
+ """Check if a plugin has been registered.
+
+ :param string name: The name of the plugin to check.
+ :return: boolean
+ """
+ return name in PLUGIN_REGISTRY
+
+ def disable(self, name, _disabled=None):
+ """Disable a plugin, including any dependent upon it.
+
+ :param string name: The name of the plugin to disable.
+ :param set _disabled: Private set used to track the
+ disabled status of plugins during
+ the cascading process.
+ """
+ if _disabled is None:
+ _disabled = set()
+ with self._plugin_lock:
+ if name not in _disabled and name in self._enabled:
+ _disabled.add(name)
+ plugin = self._plugins.get(name, None)
+ if plugin is None:
+ raise PluginNotFound(name)
+ for dep in PLUGIN_DEPENDENTS[name]:
+ self.disable(dep, _disabled)
+ plugin._end()
+ if name in self._enabled:
+ self._enabled.remove(name)
+ del self._plugins[name]
+
+ def __keys__(self):
+ """Return the set of enabled plugins."""
+ return self._plugins.keys()
+
+ def __getitem__(self, name):
+ """
+ Allow plugins to be accessed through the manager as if
+ it were a dictionary.
+ """
+ plugin = self._plugins.get(name, None)
+ if plugin is None:
+ raise PluginNotFound(name)
+ return plugin
+
+ def __iter__(self):
+ """Return an iterator over the set of enabled plugins."""
+ return self._plugins.__iter__()
+
+ def __len__(self):
+ """Return the number of enabled plugins."""
+ return len(self._plugins)
+
+
+class BasePlugin(object):
+
+ #: A short name for the plugin based on the implemented specification.
+ #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
+ name = ''
+
+ #: A longer name for the plugin, describing its purpose. For example,
+ #: a plugin for XEP-0030 would use `'Service Discovery'` as its
+ #: description value.
+ description = ''
+
+ #: Some plugins may depend on others in order to function properly.
+ #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
+ #: be initialized as needed if this plugin is enabled.
+ dependencies = set()
+
+ #: The basic, standard configuration for the plugin, which may
+ #: be overridden when initializing the plugin. The configuration
+ #: fields included here may be accessed directly as attributes of
+ #: the plugin. For example, including the configuration field 'foo'
+ #: would mean accessing `plugin.foo` returns the current value of
+ #: `plugin.config['foo']`.
+ default_config = {}
+
+ def __init__(self, xmpp, config=None):
+ self.xmpp = xmpp
+ if self.xmpp:
+ self.api = self.xmpp.api.wrap(self.name)
+
+ #: A plugin's behaviour may be configurable, in which case those
+ #: configuration settings will be provided as a dictionary.
+ self.config = copy.copy(self.default_config)
+ if config:
+ self.config.update(config)
+
+ def __getattr__(self, key):
+ """Provide direct access to configuration fields.
+
+ If the standard configuration includes the option `'foo'`, then
+ accessing `self.foo` should be the same as `self.config['foo']`.
+ """
+ if key in self.default_config:
+ return self.config.get(key, None)
+ else:
+ return object.__getattribute__(self, key)
+
+ def __setattr__(self, key, value):
+ """Provide direct assignment to configuration fields.
+
+ If the standard configuration includes the option `'foo'`, then
+ assigning to `self.foo` should be the same as assigning to
+ `self.config['foo']`.
+ """
+ if key in self.default_config:
+ self.config[key] = value
+ else:
+ super(BasePlugin, self).__setattr__(key, value)
+
+ def _init(self):
+ """Initialize plugin state, such as registering event handlers.
+
+ Also sets up required event handlers.
+ """
+ if self.xmpp is not None:
+ self.xmpp.add_event_handler('session_bind', self.session_bind)
+ if self.xmpp.session_bind_event.is_set():
+ self.session_bind(self.xmpp.boundjid.full)
+ self.plugin_init()
+ log.debug('Loaded Plugin: %s', self.description)
+
+ def _end(self):
+ """Cleanup plugin state, and prepare for plugin removal.
+
+ Also removes required event handlers.
+ """
+ if self.xmpp is not None:
+ self.xmpp.del_event_handler('session_bind', self.session_bind)
+ self.plugin_end()
+ log.debug('Disabled Plugin: %s' % self.description)
+
+ def plugin_init(self):
+ """Initialize plugin state, such as registering event handlers."""
+ pass
+
+ def plugin_end(self):
+ """Cleanup plugin state, and prepare for plugin removal."""
+ pass
+
+ def session_bind(self, jid):
+ """Initialize plugin state based on the bound JID."""
+ pass
+
+ def post_init(self):
+ """Initialize any cross-plugin state.
+
+ Only needed if the plugin has circular dependencies.
+ """
+ pass
diff --git a/slixmpp/plugins/gmail_notify.py b/slixmpp/plugins/gmail_notify.py
new file mode 100644
index 00000000..8071984c
--- /dev/null
+++ b/slixmpp/plugins/gmail_notify.py
@@ -0,0 +1,149 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from slixmpp.plugins import BasePlugin
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import register_stanza_plugin, ElementBase, ET, JID
+from .. stanza.iq import Iq
+
+
+log = logging.getLogger(__name__)
+
+
+class GmailQuery(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'query'
+ plugin_attrib = 'gmail'
+ interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search'))
+
+ def getSearch(self):
+ return self['q']
+
+ def setSearch(self, search):
+ self['q'] = search
+
+ def delSearch(self):
+ del self['q']
+
+
+class MailBox(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mailbox'
+ plugin_attrib = 'mailbox'
+ interfaces = set(('result-time', 'total-matched', 'total-estimate',
+ 'url', 'threads', 'matched', 'estimate'))
+
+ def getThreads(self):
+ threads = []
+ for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace,
+ MailThread.name)):
+ threads.append(MailThread(xml=threadXML, parent=None))
+ return threads
+
+ def getMatched(self):
+ return self['total-matched']
+
+ def getEstimate(self):
+ return self['total-estimate'] == '1'
+
+
+class MailThread(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mail-thread-info'
+ plugin_attrib = 'thread'
+ interfaces = set(('tid', 'participation', 'messages', 'date',
+ 'senders', 'url', 'labels', 'subject', 'snippet'))
+ sub_interfaces = set(('labels', 'subject', 'snippet'))
+
+ def getSenders(self):
+ senders = []
+ sendersXML = self.xml.find('{%s}senders' % self.namespace)
+ if sendersXML is not None:
+ for senderXML in sendersXML.findall('{%s}sender' % self.namespace):
+ senders.append(MailSender(xml=senderXML, parent=None))
+ return senders
+
+
+class MailSender(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'sender'
+ plugin_attrib = 'sender'
+ interfaces = set(('address', 'name', 'originator', 'unread'))
+
+ def getOriginator(self):
+ return self.xml.attrib.get('originator', '0') == '1'
+
+ def getUnread(self):
+ return self.xml.attrib.get('unread', '0') == '1'
+
+
+class NewMail(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'new-mail'
+ plugin_attrib = 'new-mail'
+
+
+class gmail_notify(BasePlugin):
+ """
+ Google Talk: Gmail Notifications
+ """
+
+ def plugin_init(self):
+ self.description = 'Google Talk: Gmail Notifications'
+
+ self.xmpp.registerHandler(
+ Callback('Gmail Result',
+ MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
+ MailBox.namespace,
+ MailBox.name)),
+ self.handle_gmail))
+
+ self.xmpp.registerHandler(
+ Callback('Gmail New Mail',
+ MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
+ NewMail.namespace,
+ NewMail.name)),
+ self.handle_new_mail))
+
+ register_stanza_plugin(Iq, GmailQuery)
+ register_stanza_plugin(Iq, MailBox)
+ register_stanza_plugin(Iq, NewMail)
+
+ self.last_result_time = None
+
+ def handle_gmail(self, iq):
+ mailbox = iq['mailbox']
+ approx = ' approximately' if mailbox['estimated'] else ''
+ log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched'])
+ self.last_result_time = mailbox['result-time']
+ self.xmpp.event('gmail_messages', iq)
+
+ def handle_new_mail(self, iq):
+ log.info("Gmail: New emails received!")
+ self.xmpp.event('gmail_notify')
+ self.checkEmail()
+
+ def getEmail(self, query=None):
+ return self.search(query)
+
+ def checkEmail(self):
+ return self.search(newer=self.last_result_time)
+
+ def search(self, query=None, newer=None):
+ if query is None:
+ log.info("Gmail: Checking for new emails")
+ else:
+ log.info('Gmail: Searching for emails matching: "%s"', query)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.boundjid.bare
+ iq['gmail']['q'] = query
+ iq['gmail']['newer-than-time'] = newer
+ return iq.send()
diff --git a/slixmpp/plugins/google/auth/stanza.py b/slixmpp/plugins/google/auth/stanza.py
new file mode 100644
index 00000000..c5c693ee
--- /dev/null
+++ b/slixmpp/plugins/google/auth/stanza.py
@@ -0,0 +1,47 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class GoogleAuth(ElementBase):
+ name = 'auth'
+ namespace = 'http://www.google.com/talk/protocol/auth'
+ plugin_attrib = 'google'
+ interfaces = set(['client_uses_full_bind_result', 'service'])
+
+ discovery_attr= '{%s}client-uses-full-bind-result' % namespace
+ service_attr= '{%s}service' % namespace
+
+ def setup(self, xml):
+ """Don't create XML for the plugin."""
+ self.xml = ET.Element('')
+
+ def get_client_uses_full_bind_result(self):
+ return self.parent()._get_attr(self.discovery_attr) == 'true'
+
+ def set_client_uses_full_bind_result(self, value):
+ if value in (True, 'true'):
+ self.parent()._set_attr(self.discovery_attr, 'true')
+ else:
+ self.parent()._del_attr(self.discovery_attr)
+
+ def del_client_uses_full_bind_result(self):
+ self.parent()._del_attr(self.discovery_attr)
+
+ def get_service(self):
+ return self.parent()._get_attr(self.service_attr, '')
+
+ def set_service(self, value):
+ if value:
+ self.parent()._set_attr(self.service_attr, value)
+ else:
+ self.parent()._del_attr(self.service_attr)
+
+ def del_service(self):
+ self.parent()._del_attr(self.service_attr)
diff --git a/slixmpp/plugins/google/gmail/notifications.py b/slixmpp/plugins/google/gmail/notifications.py
new file mode 100644
index 00000000..e6785ccb
--- /dev/null
+++ b/slixmpp/plugins/google/gmail/notifications.py
@@ -0,0 +1,90 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import Iq
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.google.gmail import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class Gmail(BasePlugin):
+
+ """
+ Google: Gmail Notifications
+
+ Also see <https://developers.google.com/talk/jep_extensions/gmail>.
+ """
+
+ name = 'gmail'
+ description = 'Google: Gmail Notifications'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.GmailQuery)
+ register_stanza_plugin(Iq, stanza.MailBox)
+ register_stanza_plugin(Iq, stanza.NewMail)
+
+ self.xmpp.register_handler(
+ Callback('Gmail New Mail',
+ MatchXPath('{%s}iq/{%s}%s' % (
+ self.xmpp.default_ns,
+ stanza.NewMail.namespace,
+ stanza.NewMail.name)),
+ self._handle_new_mail))
+
+ self._last_result_time = None
+ self._last_result_tid = None
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Gmail New Mail')
+
+ def _handle_new_mail(self, iq):
+ log.info('Gmail: New email!')
+ iq.reply().send()
+ self.xmpp.event('gmail_notification')
+
+ def check(self, timeout=None, callback=None):
+ last_time = self._last_result_time
+ last_tid = self._last_result_tid
+
+ callback = lambda iq: self._update_last_results(iq, callback)
+
+ return self.search(newer_time=last_time,
+ newer_tid=last_tid,
+ timeout=timeout,
+ callback=callback)
+
+ def _update_last_results(self, iq, callback=None):
+ self._last_result_time = iq['gmail_messages']['result_time']
+ threads = iq['gmail_messages']['threads']
+ if threads:
+ self._last_result_tid = threads[0]['tid']
+ if callback:
+ callback(iq)
+
+ def search(self, query=None, newer_time=None, newer_tid=None,
+ timeout=None, callback=None):
+ if not query:
+ log.info('Gmail: Checking for new email')
+ else:
+ log.info('Gmail: Searching for emails matching: "%s"', query)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.boundjid.bare
+ iq['gmail']['search'] = query
+ iq['gmail']['newer_than_time'] = newer_time
+ iq['gmail']['newer_than_tid'] = newer_tid
+ return iq.send(timeout=timeout, callback=callback)
diff --git a/slixmpp/plugins/google/nosave/stanza.py b/slixmpp/plugins/google/nosave/stanza.py
new file mode 100644
index 00000000..b060a486
--- /dev/null
+++ b/slixmpp/plugins/google/nosave/stanza.py
@@ -0,0 +1,59 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class NoSave(ElementBase):
+ name = 'x'
+ namespace = 'google:nosave'
+ plugin_attrib = 'google_nosave'
+ interfaces = set(['value'])
+
+ def get_value(self):
+ return self._get_attr('value', '') == 'enabled'
+
+ def set_value(self, value):
+ self._set_attr('value', 'enabled' if value else 'disabled')
+
+
+class NoSaveQuery(ElementBase):
+ name = 'query'
+ namespace = 'google:nosave'
+ plugin_attrib = 'google_nosave'
+ interfaces = set()
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'google:nosave'
+ plugin_attrib = 'item'
+ plugin_multi_attrib = 'items'
+ interfaces = set(['jid', 'source', 'value'])
+
+ def get_value(self):
+ return self._get_attr('value', '') == 'enabled'
+
+ def set_value(self, value):
+ self._set_attr('value', 'enabled' if value else 'disabled')
+
+ def get_jid(self):
+ return JID(self._get_attr('jid', ''))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_source(self):
+ return JID(self._get_attr('source', ''))
+
+ def set_source(self, value):
+ self._set_attr('source', str(value))
+
+
+register_stanza_plugin(NoSaveQuery, Item)
diff --git a/slixmpp/plugins/google/settings/settings.py b/slixmpp/plugins/google/settings/settings.py
new file mode 100644
index 00000000..84a8dfa9
--- /dev/null
+++ b/slixmpp/plugins/google/settings/settings.py
@@ -0,0 +1,63 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Iq
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.google.settings import stanza
+
+
+class GoogleSettings(BasePlugin):
+
+ """
+ Google: Gmail Notifications
+
+ Also see <https://developers.google.com/talk/jep_extensions/usersettings>.
+ """
+
+ name = 'google_settings'
+ description = 'Google: User Settings'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.UserSettings)
+
+ self.xmpp.register_handler(
+ Callback('Google Settings',
+ StanzaPath('iq@type=set/google_settings'),
+ self._handle_settings_change))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Google Settings')
+
+ def get(self, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('google_settings')
+ return iq.send(timeout=timeout, callback=callback)
+
+ def update(self, settings, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('google_settings')
+
+ for setting, value in settings.items():
+ iq['google_settings'][setting] = value
+
+ return iq.send(timeout=timeout, callback=callback)
+
+ def _handle_settings_change(self, iq):
+ reply = self.xmpp.Iq()
+ reply['type'] = 'result'
+ reply['id'] = iq['id']
+ reply['to'] = iq['from']
+ reply.send()
+ self.xmpp.event('google_settings_change', iq)
diff --git a/slixmpp/plugins/xep_0004/__init__.py b/slixmpp/plugins/xep_0004/__init__.py
new file mode 100644
index 00000000..3eb2b7a5
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/__init__.py
@@ -0,0 +1,22 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0004.stanza import Form
+from slixmpp.plugins.xep_0004.stanza import FormField, FieldOption
+from slixmpp.plugins.xep_0004.dataforms import XEP_0004
+
+
+register_plugin(XEP_0004)
+
+
+# Retain some backwards compatibility
+xep_0004 = XEP_0004
+xep_0004.makeForm = xep_0004.make_form
+xep_0004.buildForm = xep_0004.build_form
diff --git a/slixmpp/plugins/xep_0004/dataforms.py b/slixmpp/plugins/xep_0004/dataforms.py
new file mode 100644
index 00000000..90a87774
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/dataforms.py
@@ -0,0 +1,57 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Message
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0004 import stanza
+from slixmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption
+
+
+class XEP_0004(BasePlugin):
+
+ """
+ XEP-0004: Data Forms
+ """
+
+ name = 'xep_0004'
+ description = 'XEP-0004: Data Forms'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Data Form',
+ StanzaPath('message/form'),
+ self.handle_form))
+
+ register_stanza_plugin(FormField, FieldOption, iterable=True)
+ register_stanza_plugin(Form, FormField, iterable=True)
+ register_stanza_plugin(Message, Form)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Data Form')
+ self.xmpp['xep_0030'].del_feature(feature='jabber:x:data')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('jabber:x:data')
+
+ def make_form(self, ftype='form', title='', instructions=''):
+ f = Form()
+ f['type'] = ftype
+ f['title'] = title
+ f['instructions'] = instructions
+ return f
+
+ def handle_form(self, message):
+ self.xmpp.event("message_xform", message)
+
+ def build_form(self, xml):
+ return Form(xml=xml)
diff --git a/slixmpp/plugins/xep_0004/stanza/__init__.py b/slixmpp/plugins/xep_0004/stanza/__init__.py
new file mode 100644
index 00000000..9daaab75
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/stanza/__init__.py
@@ -0,0 +1,10 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0004.stanza.field import FormField, FieldOption
+from slixmpp.plugins.xep_0004.stanza.form import Form
diff --git a/slixmpp/plugins/xep_0004/stanza/field.py b/slixmpp/plugins/xep_0004/stanza/field.py
new file mode 100644
index 00000000..42f1210b
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/stanza/field.py
@@ -0,0 +1,185 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class FormField(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'field'
+ plugin_attrib = 'field'
+ plugin_multi_attrib = 'fields'
+ interfaces = set(('answer', 'desc', 'required', 'value',
+ 'label', 'type', 'var'))
+ sub_interfaces = set(('desc',))
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi',
+ 'jid-single', 'list-multi', 'list-single',
+ 'text-multi', 'text-private', 'text-single'))
+
+ true_values = set((True, '1', 'true'))
+ option_types = set(('list-multi', 'list-single'))
+ multi_line_types = set(('hidden', 'text-multi'))
+ multi_value_types = set(('hidden', 'jid-multi',
+ 'list-multi', 'text-multi'))
+
+ def setup(self, xml=None):
+ if ElementBase.setup(self, xml):
+ self._type = None
+ else:
+ self._type = self['type']
+
+ def set_type(self, value):
+ self._set_attr('type', value)
+ if value:
+ self._type = value
+
+ def add_option(self, label='', value=''):
+ if self._type is None or self._type in self.option_types:
+ opt = FieldOption()
+ opt['label'] = label
+ opt['value'] = value
+ self.append(opt)
+ else:
+ raise ValueError("Cannot add options to " + \
+ "a %s field." % self['type'])
+
+ def del_options(self):
+ optsXML = self.xml.findall('{%s}option' % self.namespace)
+ for optXML in optsXML:
+ self.xml.remove(optXML)
+
+ def del_required(self):
+ reqXML = self.xml.find('{%s}required' % self.namespace)
+ if reqXML is not None:
+ self.xml.remove(reqXML)
+
+ def del_value(self):
+ valsXML = self.xml.findall('{%s}value' % self.namespace)
+ for valXML in valsXML:
+ self.xml.remove(valXML)
+
+ def get_answer(self):
+ return self['value']
+
+ def get_options(self):
+ options = []
+ optsXML = self.xml.findall('{%s}option' % self.namespace)
+ for optXML in optsXML:
+ opt = FieldOption(xml=optXML)
+ options.append({'label': opt['label'], 'value': opt['value']})
+ return options
+
+ def get_required(self):
+ reqXML = self.xml.find('{%s}required' % self.namespace)
+ return reqXML is not None
+
+ def get_value(self, convert=True):
+ valsXML = self.xml.findall('{%s}value' % self.namespace)
+ if len(valsXML) == 0:
+ return None
+ elif self._type == 'boolean':
+ if convert:
+ return valsXML[0].text in self.true_values
+ return valsXML[0].text
+ elif self._type in self.multi_value_types or len(valsXML) > 1:
+ values = []
+ for valXML in valsXML:
+ if valXML.text is None:
+ valXML.text = ''
+ values.append(valXML.text)
+ if self._type == 'text-multi' and convert:
+ values = "\n".join(values)
+ return values
+ else:
+ if valsXML[0].text is None:
+ return ''
+ return valsXML[0].text
+
+ def set_answer(self, answer):
+ self['value'] = answer
+
+ def set_false(self):
+ self['value'] = False
+
+ def set_options(self, options):
+ for value in options:
+ if isinstance(value, dict):
+ self.add_option(**value)
+ else:
+ self.add_option(value=value)
+
+ def set_required(self, required):
+ exists = self['required']
+ if not exists and required:
+ self.xml.append(ET.Element('{%s}required' % self.namespace))
+ elif exists and not required:
+ del self['required']
+
+ def set_true(self):
+ self['value'] = True
+
+ def set_value(self, value):
+ del self['value']
+ valXMLName = '{%s}value' % self.namespace
+
+ if self._type == 'boolean':
+ if value in self.true_values:
+ valXML = ET.Element(valXMLName)
+ valXML.text = '1'
+ self.xml.append(valXML)
+ else:
+ valXML = ET.Element(valXMLName)
+ valXML.text = '0'
+ self.xml.append(valXML)
+ elif self._type in self.multi_value_types or self._type in ('', None):
+ if isinstance(value, bool):
+ value = [value]
+ if not isinstance(value, list):
+ value = value.replace('\r', '')
+ value = value.split('\n')
+ for val in value:
+ if self._type in ('', None) and val in self.true_values:
+ val = '1'
+ valXML = ET.Element(valXMLName)
+ valXML.text = val
+ self.xml.append(valXML)
+ else:
+ if isinstance(value, list):
+ raise ValueError("Cannot add multiple values " + \
+ "to a %s field." % self._type)
+ valXML = ET.Element(valXMLName)
+ valXML.text = value
+ self.xml.append(valXML)
+
+
+class FieldOption(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'option'
+ plugin_attrib = 'option'
+ interfaces = set(('label', 'value'))
+ sub_interfaces = set(('value',))
+ plugin_multi_attrib = 'options'
+
+
+FormField.addOption = FormField.add_option
+FormField.delOptions = FormField.del_options
+FormField.delRequired = FormField.del_required
+FormField.delValue = FormField.del_value
+FormField.getAnswer = FormField.get_answer
+FormField.getOptions = FormField.get_options
+FormField.getRequired = FormField.get_required
+FormField.getValue = FormField.get_value
+FormField.setAnswer = FormField.set_answer
+FormField.setFalse = FormField.set_false
+FormField.setOptions = FormField.set_options
+FormField.setRequired = FormField.set_required
+FormField.setTrue = FormField.set_true
+FormField.setValue = FormField.set_value
diff --git a/slixmpp/plugins/xep_0004/stanza/form.py b/slixmpp/plugins/xep_0004/stanza/form.py
new file mode 100644
index 00000000..151e2ef1
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/stanza/form.py
@@ -0,0 +1,275 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import copy
+import logging
+
+from collections import OrderedDict
+from slixmpp.thirdparty import OrderedSet
+
+from slixmpp.xmlstream import ElementBase, ET
+from slixmpp.plugins.xep_0004.stanza import FormField
+
+
+log = logging.getLogger(__name__)
+
+
+class Form(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'x'
+ plugin_attrib = 'form'
+ interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', ))
+ sub_interfaces = set(('title',))
+ form_types = set(('cancel', 'form', 'result', 'submit'))
+
+ def __init__(self, *args, **kwargs):
+ title = None
+ if 'title' in kwargs:
+ title = kwargs['title']
+ del kwargs['title']
+ ElementBase.__init__(self, *args, **kwargs)
+ if title is not None:
+ self['title'] = title
+
+ def setup(self, xml=None):
+ if ElementBase.setup(self, xml):
+ # If we had to generate xml
+ self['type'] = 'form'
+
+ @property
+ def field(self):
+ return self.get_fields()
+
+ def set_type(self, ftype):
+ self._set_attr('type', ftype)
+ if ftype == 'submit':
+ fields = self.get_fields()
+ for var in fields:
+ field = fields[var]
+ del field['type']
+ del field['label']
+ del field['desc']
+ del field['required']
+ del field['options']
+ elif ftype == 'cancel':
+ del self['fields']
+
+ def add_field(self, var='', ftype=None, label='', desc='',
+ required=False, value=None, options=None, **kwargs):
+ kwtype = kwargs.get('type', None)
+ if kwtype is None:
+ kwtype = ftype
+
+ field = FormField()
+ field['var'] = var
+ field['type'] = kwtype
+ field['value'] = value
+ if self['type'] in ('form', 'result'):
+ field['label'] = label
+ field['desc'] = desc
+ field['required'] = required
+ if options is not None:
+ for option in options:
+ field.add_option(**option)
+ else:
+ del field['type']
+ self.append(field)
+ return field
+
+ def getXML(self, type='submit'):
+ self['type'] = type
+ log.warning("Form.getXML() is deprecated API compatibility " + \
+ "with plugins/old_0004.py")
+ return self.xml
+
+ def fromXML(self, xml):
+ log.warning("Form.fromXML() is deprecated API compatibility " + \
+ "with plugins/old_0004.py")
+ n = Form(xml=xml)
+ return n
+
+ def add_item(self, values):
+ itemXML = ET.Element('{%s}item' % self.namespace)
+ self.xml.append(itemXML)
+ reported_vars = self['reported'].keys()
+ for var in reported_vars:
+ field = FormField()
+ field._type = self['reported'][var]['type']
+ field['var'] = var
+ field['value'] = values.get(var, None)
+ itemXML.append(field.xml)
+
+ def add_reported(self, var, ftype=None, label='', desc='', **kwargs):
+ kwtype = kwargs.get('type', None)
+ if kwtype is None:
+ kwtype = ftype
+ reported = self.xml.find('{%s}reported' % self.namespace)
+ if reported is None:
+ reported = ET.Element('{%s}reported' % self.namespace)
+ self.xml.append(reported)
+ fieldXML = ET.Element('{%s}field' % FormField.namespace)
+ reported.append(fieldXML)
+ field = FormField(xml=fieldXML)
+ field['var'] = var
+ field['type'] = kwtype
+ field['label'] = label
+ field['desc'] = desc
+ return field
+
+ def cancel(self):
+ self['type'] = 'cancel'
+
+ def del_fields(self):
+ fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ self.xml.remove(fieldXML)
+
+ def del_instructions(self):
+ instsXML = self.xml.findall('{%s}instructions')
+ for instXML in instsXML:
+ self.xml.remove(instXML)
+
+ def del_items(self):
+ itemsXML = self.xml.find('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ self.xml.remove(itemXML)
+
+ def del_reported(self):
+ reportedXML = self.xml.find('{%s}reported' % self.namespace)
+ if reportedXML is not None:
+ self.xml.remove(reportedXML)
+
+ def get_fields(self, use_dict=False):
+ fields = OrderedDict()
+ for stanza in self['substanzas']:
+ if isinstance(stanza, FormField):
+ fields[stanza['var']] = stanza
+ return fields
+
+ def get_instructions(self):
+ instsXML = self.xml.findall('{%s}instructions' % self.namespace)
+ return "\n".join([instXML.text for instXML in instsXML])
+
+ def get_items(self):
+ items = []
+ itemsXML = self.xml.findall('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ item = OrderedDict()
+ fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ field = FormField(xml=fieldXML)
+ item[field['var']] = field['value']
+ items.append(item)
+ return items
+
+ def get_reported(self):
+ fields = OrderedDict()
+ xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
+ FormField.namespace))
+ for field in xml:
+ field = FormField(xml=field)
+ fields[field['var']] = field
+ return fields
+
+ def get_values(self):
+ values = OrderedDict()
+ fields = self.get_fields()
+ for var in fields:
+ values[var] = fields[var]['value']
+ return values
+
+ def reply(self):
+ if self['type'] == 'form':
+ self['type'] = 'submit'
+ elif self['type'] == 'submit':
+ self['type'] = 'result'
+
+ def set_fields(self, fields):
+ del self['fields']
+ if not isinstance(fields, list):
+ fields = fields.items()
+ for var, field in fields:
+ field['var'] = var
+ self.add_field(
+ var=field.get('var'),
+ label=field.get('label'),
+ desc=field.get('desc'),
+ required=field.get('required'),
+ value=field.get('value'),
+ options=field.get('options'),
+ type=field.get('type'))
+
+ def set_instructions(self, instructions):
+ del self['instructions']
+ if instructions in [None, '']:
+ return
+ if not isinstance(instructions, list):
+ instructions = instructions.split('\n')
+ for instruction in instructions:
+ inst = ET.Element('{%s}instructions' % self.namespace)
+ inst.text = instruction
+ self.xml.append(inst)
+
+ def set_items(self, items):
+ for item in items:
+ self.add_item(item)
+
+ def set_reported(self, reported):
+ """
+ This either needs a dictionary of dictionaries or a dictionary of form fields.
+ :param reported:
+ :return:
+ """
+ for var in reported:
+ field = reported[var]
+
+ if isinstance(field, dict):
+ self.add_reported(**field)
+ else:
+ reported = self.xml.find('{%s}reported' % self.namespace)
+ if reported is None:
+ reported = ET.Element('{%s}reported' % self.namespace)
+ self.xml.append(reported)
+
+ fieldXML = ET.Element('{%s}field' % FormField.namespace)
+ reported.append(fieldXML)
+ new_field = FormField(xml=fieldXML)
+ new_field.values = field.values
+
+ def set_values(self, values):
+ fields = self.get_fields()
+ for field in values:
+ if field not in self.get_fields():
+ fields[field] = self.add_field(var=field)
+ self.get_fields()[field]['value'] = values[field]
+
+ def merge(self, other):
+ new = copy.copy(self)
+ if type(other) == dict:
+ new['values'] = other
+ return new
+ nfields = new['fields']
+ ofields = other['fields']
+ nfields.update(ofields)
+ new['fields'] = nfields
+ return new
+
+
+Form.addField = Form.add_field
+Form.addReported = Form.add_reported
+Form.delFields = Form.del_fields
+Form.delInstructions = Form.del_instructions
+Form.delReported = Form.del_reported
+Form.getFields = Form.get_fields
+Form.getInstructions = Form.get_instructions
+Form.getReported = Form.get_reported
+Form.getValues = Form.get_values
+Form.setFields = Form.set_fields
+Form.setInstructions = Form.set_instructions
+Form.setReported = Form.set_reported
+Form.setValues = Form.set_values
diff --git a/slixmpp/plugins/xep_0009/__init__.py b/slixmpp/plugins/xep_0009/__init__.py
new file mode 100644
index 00000000..a06306b2
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0009 import stanza
+from slixmpp.plugins.xep_0009.rpc import XEP_0009
+from slixmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
+
+
+register_plugin(XEP_0009)
+
+
+# Retain some backwards compatibility
+xep_0009 = XEP_0009
diff --git a/slixmpp/plugins/xep_0009/binding.py b/slixmpp/plugins/xep_0009/binding.py
new file mode 100644
index 00000000..d922dfc7
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/binding.py
@@ -0,0 +1,169 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET
+import base64
+import logging
+import time
+
+log = logging.getLogger(__name__)
+
+_namespace = 'jabber:iq:rpc'
+
+def fault2xml(fault):
+ value = dict()
+ value['faultCode'] = fault['code']
+ value['faultString'] = fault['string']
+ fault = ET.Element("fault", {'xmlns': _namespace})
+ fault.append(_py2xml((value)))
+ return fault
+
+def xml2fault(params):
+ vals = []
+ for value in params.findall('{%s}value' % _namespace):
+ vals.append(_xml2py(value))
+ fault = dict()
+ fault['code'] = vals[0]['faultCode']
+ fault['string'] = vals[0]['faultString']
+ return fault
+
+def py2xml(*args):
+ params = ET.Element("{%s}params" % _namespace)
+ for x in args:
+ param = ET.Element("{%s}param" % _namespace)
+ param.append(_py2xml(x))
+ params.append(param) #<params><param>...
+ return params
+
+def _py2xml(*args):
+ for x in args:
+ val = ET.Element("{%s}value" % _namespace)
+ if x is None:
+ nil = ET.Element("{%s}nil" % _namespace)
+ val.append(nil)
+ elif type(x) is int:
+ i4 = ET.Element("{%s}i4" % _namespace)
+ i4.text = str(x)
+ val.append(i4)
+ elif type(x) is bool:
+ boolean = ET.Element("{%s}boolean" % _namespace)
+ boolean.text = str(int(x))
+ val.append(boolean)
+ elif type(x) is str:
+ string = ET.Element("{%s}string" % _namespace)
+ string.text = x
+ val.append(string)
+ elif type(x) is float:
+ double = ET.Element("{%s}double" % _namespace)
+ double.text = str(x)
+ val.append(double)
+ elif type(x) is rpcbase64:
+ b64 = ET.Element("{%s}base64" % _namespace)
+ b64.text = x.encoded()
+ val.append(b64)
+ elif type(x) is rpctime:
+ iso = ET.Element("{%s}dateTime.iso8601" % _namespace)
+ iso.text = str(x)
+ val.append(iso)
+ elif type(x) in (list, tuple):
+ array = ET.Element("{%s}array" % _namespace)
+ data = ET.Element("{%s}data" % _namespace)
+ for y in x:
+ data.append(_py2xml(y))
+ array.append(data)
+ val.append(array)
+ elif type(x) is dict:
+ struct = ET.Element("{%s}struct" % _namespace)
+ for y in x.keys():
+ member = ET.Element("{%s}member" % _namespace)
+ name = ET.Element("{%s}name" % _namespace)
+ name.text = y
+ member.append(name)
+ member.append(_py2xml(x[y]))
+ struct.append(member)
+ val.append(struct)
+ return val
+
+def xml2py(params):
+ namespace = 'jabber:iq:rpc'
+ vals = []
+ for param in params.findall('{%s}param' % namespace):
+ vals.append(_xml2py(param.find('{%s}value' % namespace)))
+ return vals
+
+def _xml2py(value):
+ namespace = 'jabber:iq:rpc'
+ if value.find('{%s}nil' % namespace) is not None:
+ return None
+ if value.find('{%s}i4' % namespace) is not None:
+ return int(value.find('{%s}i4' % namespace).text)
+ if value.find('{%s}int' % namespace) is not None:
+ return int(value.find('{%s}int' % namespace).text)
+ if value.find('{%s}boolean' % namespace) is not None:
+ return bool(int(value.find('{%s}boolean' % namespace).text))
+ if value.find('{%s}string' % namespace) is not None:
+ return value.find('{%s}string' % namespace).text
+ if value.find('{%s}double' % namespace) is not None:
+ return float(value.find('{%s}double' % namespace).text)
+ if value.find('{%s}base64' % namespace) is not None:
+ return rpcbase64(value.find('{%s}base64' % namespace).text.encode())
+ if value.find('{%s}Base64' % namespace) is not None:
+ # Older versions of XEP-0009 used Base64
+ return rpcbase64(value.find('{%s}Base64' % namespace).text.encode())
+ if value.find('{%s}dateTime.iso8601' % namespace) is not None:
+ return rpctime(value.find('{%s}dateTime.iso8601' % namespace).text)
+ if value.find('{%s}struct' % namespace) is not None:
+ struct = {}
+ for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace):
+ struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace))
+ return struct
+ if value.find('{%s}array' % namespace) is not None:
+ array = []
+ for val in value.find('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace):
+ array.append(_xml2py(val))
+ return array
+ raise ValueError()
+
+
+
+class rpcbase64(object):
+
+ def __init__(self, data):
+ #base 64 encoded string
+ self.data = data
+
+ def decode(self):
+ return base64.b64decode(self.data)
+
+ def __str__(self):
+ return self.decode().decode()
+
+ def encoded(self):
+ return self.data.decode()
+
+
+
+class rpctime(object):
+
+ def __init__(self,data=None):
+ #assume string data is in iso format YYYYMMDDTHH:MM:SS
+ if type(data) is str:
+ self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
+ elif type(data) is time.struct_time:
+ self.timestamp = data
+ elif data is None:
+ self.timestamp = time.gmtime()
+ else:
+ raise ValueError()
+
+ def iso8601(self):
+ #return a iso8601 string
+ return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
+
+ def __str__(self):
+ return self.iso8601()
diff --git a/slixmpp/plugins/xep_0009/remote.py b/slixmpp/plugins/xep_0009/remote.py
new file mode 100644
index 00000000..9675c88d
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/remote.py
@@ -0,0 +1,772 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml
+from threading import RLock
+import abc
+import inspect
+import logging
+import slixmpp
+import sys
+import threading
+import traceback
+
+log = logging.getLogger(__name__)
+
+def _isstr(obj):
+ return isinstance(obj, str)
+
+
+# Class decorator to declare a metaclass to a class in a way compatible with Python 2 and 3.
+# This decorator is copied from 'six' (https://bitbucket.org/gutworth/six):
+#
+# Copyright (c) 2010-2015 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+def _add_metaclass(metaclass):
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
+def _intercept(method, name, public):
+ def _resolver(instance, *args, **kwargs):
+ log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args)
+ try:
+ value = method(instance, *args, **kwargs)
+ if value == NotImplemented:
+ raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__))
+ return value
+ except InvocationException:
+ raise
+ except Exception as e:
+ raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e)
+ _resolver._rpc = public
+ _resolver._rpc_name = method.__name__ if name is None else name
+ return _resolver
+
+def remote(function_argument, public = True):
+ '''
+ Decorator for methods which are remotely callable. This decorator
+ works in conjunction with classes which extend ABC Endpoint.
+ Example:
+
+ @remote
+ def remote_method(arg1, arg2)
+
+ Arguments:
+ function_argument -- a stand-in for either the actual method
+ OR a new name (string) for the method. In that case the
+ method is considered mapped:
+ Example:
+
+ @remote("new_name")
+ def remote_method(arg1, arg2)
+
+ public -- A flag which indicates if this method should be part
+ of the known dictionary of remote methods. Defaults to True.
+ Example:
+
+ @remote(False)
+ def remote_method(arg1, arg2)
+
+ Note: renaming and revising (public vs. private) can be combined.
+ Example:
+
+ @remote("new_name", False)
+ def remote_method(arg1, arg2)
+ '''
+ if hasattr(function_argument, '__call__'):
+ return _intercept(function_argument, None, public)
+ else:
+ if not _isstr(function_argument):
+ if not isinstance(function_argument, bool):
+ raise Exception('Expected an RPC method name or visibility modifier!')
+ else:
+ def _wrap_revised(function):
+ function = _intercept(function, None, function_argument)
+ return function
+ return _wrap_revised
+ def _wrap_remapped(function):
+ function = _intercept(function, function_argument, public)
+ return function
+ return _wrap_remapped
+
+
+class ACL:
+ '''
+ An Access Control List (ACL) is a list of rules, which are evaluated
+ in order until a match is found. The policy of the matching rule
+ is then applied.
+
+ Rules are 3-tuples, consisting of a policy enumerated type, a JID
+ expression and a RCP resource expression.
+
+ Examples:
+ [ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions
+ [ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions
+ [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'),
+ (ACL.DENY, '*', '*') ] deny everyone everything, except named
+ JID, which is allowed access to endpoint 'test' only.
+
+ The use of wildcards is allowed in expressions, as follows:
+ '*' everyone, or everything (= all endpoints and methods)
+ 'test@xmpp.org/*' every JID regardless of JID resource
+ '*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc'
+ 'frank@*' every 'frank', regardless of domain or JID res
+ 'system.*' all methods of endpoint 'system'
+ '*.reboot' all methods reboot regardless of endpoint
+ '''
+ ALLOW = True
+ DENY = False
+
+ @classmethod
+ def check(cls, rules, jid, resource):
+ if rules is None:
+ return cls.DENY # No rules means no access!
+ jid = str(jid) # Check the string representation of the JID.
+ if not jid:
+ return cls.DENY # Can't check an empty JID.
+ for rule in rules:
+ policy = cls._check(rule, jid, resource)
+ if policy is not None:
+ return policy
+ return cls.DENY # By default if not rule matches, deny access.
+
+ @classmethod
+ def _check(cls, rule, jid, resource):
+ if cls._match(jid, rule[1]) and cls._match(resource, rule[2]):
+ return rule[0]
+ else:
+ return None
+
+ @classmethod
+ def _next_token(cls, expression, index):
+ new_index = expression.find('*', index)
+ if new_index == 0:
+ return ''
+ else:
+ if new_index == -1:
+ return expression[index : ]
+ else:
+ return expression[index : new_index]
+
+ @classmethod
+ def _match(cls, value, expression):
+ #! print "_match [VALUE] %s [EXPR] %s" % (value, expression)
+ index = 0
+ position = 0
+ while index < len(expression):
+ token = cls._next_token(expression, index)
+ #! print "[TOKEN] '%s'" % token
+ size = len(token)
+ if size > 0:
+ token_index = value.find(token, position)
+ if token_index == -1:
+ return False
+ else:
+ #! print "[INDEX-OF] %s" % token_index
+ position = token_index + len(token)
+ pass
+ if size == 0:
+ index += 1
+ else:
+ index += size
+ #! print "index %s position %s" % (index, position)
+ return True
+
+ANY_ALL = [ (ACL.ALLOW, '*', '*') ]
+
+
+class RemoteException(Exception):
+ '''
+ Base exception for RPC. This exception is raised when a problem
+ occurs in the network layer.
+ '''
+
+ def __init__(self, message="", cause=None):
+ '''
+ Initializes a new RemoteException.
+
+ Arguments:
+ message -- The message accompanying this exception.
+ cause -- The underlying cause of this exception.
+ '''
+ self._message = message
+ self._cause = cause
+ pass
+
+ def __str__(self):
+ return repr(self._message)
+
+ def get_message(self):
+ return self._message
+
+ def get_cause(self):
+ return self._cause
+
+
+
+class InvocationException(RemoteException):
+ '''
+ Exception raised when a problem occurs during the remote invocation
+ of a method.
+ '''
+ pass
+
+
+
+class AuthorizationException(RemoteException):
+ '''
+ Exception raised when the caller is not authorized to invoke the
+ remote method.
+ '''
+ pass
+
+
+class TimeoutException(Exception):
+ '''
+ Exception raised when the synchronous execution of a method takes
+ longer than the given threshold because an underlying asynchronous
+ reply did not arrive in time.
+ '''
+ pass
+
+
+@_add_metaclass(abc.ABCMeta)
+class Callback(object):
+ '''
+ A base class for callback handlers.
+ '''
+
+ @abc.abstractproperty
+ def set_value(self, value):
+ return NotImplemented
+
+ @abc.abstractproperty
+ def cancel_with_error(self, exception):
+ return NotImplemented
+
+
+class Future(Callback):
+ '''
+ Represents the result of an asynchronous computation.
+ '''
+
+ def __init__(self):
+ '''
+ Initializes a new Future.
+ '''
+ self._value = None
+ self._exception = None
+ self._event = threading.Event()
+ pass
+
+ def set_value(self, value):
+ '''
+ Sets the value of this Future. Once the value is set, a caller
+ blocked on get_value will be able to continue.
+ '''
+ self._value = value
+ self._event.set()
+
+ def get_value(self, timeout=None):
+ '''
+ Gets the value of this Future. This call will block until
+ the result is available, or until an optional timeout expires.
+ When this Future is cancelled with an error,
+
+ Arguments:
+ timeout -- The maximum waiting time to obtain the value.
+ '''
+ self._event.wait(timeout)
+ if self._exception:
+ raise self._exception
+ if not self._event.is_set():
+ raise TimeoutException
+ return self._value
+
+ def is_done(self):
+ '''
+ Returns true if a value has been returned.
+ '''
+ return self._event.is_set()
+
+ def cancel_with_error(self, exception):
+ '''
+ Cancels the Future because of an error. Once cancelled, a
+ caller blocked on get_value will be able to continue.
+ '''
+ self._exception = exception
+ self._event.set()
+
+
+@_add_metaclass(abc.ABCMeta)
+class Endpoint(object):
+ '''
+ The Endpoint class is an abstract base class for all objects
+ participating in an RPC-enabled XMPP network.
+
+ A user subclassing this class is required to implement the method:
+ FQN(self)
+ where FQN stands for Fully Qualified Name, an unambiguous name
+ which specifies which object an RPC call refers to. It is the
+ first part in a RPC method name '<fqn>.<method>'.
+ '''
+
+ def __init__(self, session, target_jid):
+ '''
+ Initialize a new Endpoint. This constructor should never be
+ invoked by a user, instead it will be called by the factories
+ which instantiate the RPC-enabled objects, of which only
+ the classes are provided by the user.
+
+ Arguments:
+ session -- An RPC session instance.
+ target_jid -- the identity of the remote XMPP entity.
+ '''
+ self.session = session
+ self.target_jid = target_jid
+
+ @abc.abstractproperty
+ def FQN(self):
+ return NotImplemented
+
+ def get_methods(self):
+ '''
+ Returns a dictionary of all RPC method names provided by this
+ class. This method returns the actual method names as found
+ in the class definition which have been decorated with:
+
+ @remote
+ def some_rpc_method(arg1, arg2)
+
+
+ Unless:
+ (1) the name has been remapped, in which case the new
+ name will be returned.
+
+ @remote("new_name")
+ def some_rpc_method(arg1, arg2)
+
+ (2) the method is set to hidden
+
+ @remote(False)
+ def some_hidden_method(arg1, arg2)
+ '''
+ result = dict()
+ for function in dir(self):
+ test_attr = getattr(self, function, None)
+ try:
+ if test_attr._rpc:
+ result[test_attr._rpc_name] = test_attr
+ except Exception:
+ pass
+ return result
+
+
+
+class Proxy(Endpoint):
+ '''
+ Implementation of the Proxy pattern which is intended to wrap
+ around Endpoints in order to intercept calls, marshall them and
+ forward them to the remote object.
+ '''
+
+ def __init__(self, endpoint, callback = None):
+ '''
+ Initializes a new Proxy.
+
+ Arguments:
+ endpoint -- The endpoint which is proxified.
+ '''
+ self._endpoint = endpoint
+ self._callback = callback
+
+ def __getattribute__(self, name, *args):
+ if name in ('__dict__', '_endpoint', 'async', '_callback'):
+ return object.__getattribute__(self, name)
+ else:
+ attribute = self._endpoint.__getattribute__(name)
+ if hasattr(attribute, '__call__'):
+ try:
+ if attribute._rpc:
+ def _remote_call(*args, **kwargs):
+ log.debug("Remotely calling '%s.%s' with arguments %s.", self._endpoint.FQN(), attribute._rpc_name, args)
+ return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs)
+ return _remote_call
+ except:
+ pass # If the attribute doesn't exist, don't care!
+ return attribute
+
+ def async(self, callback):
+ return Proxy(self._endpoint, callback)
+
+ def get_endpoint(self):
+ '''
+ Returns the proxified endpoint.
+ '''
+ return self._endpoint
+
+ def FQN(self):
+ return self._endpoint.FQN()
+
+
+class JabberRPCEntry(object):
+
+
+ def __init__(self, endpoint_FQN, call):
+ self._endpoint_FQN = endpoint_FQN
+ self._call = call
+
+ def call_method(self, args):
+ return_value = self._call(*args)
+ if return_value is None:
+ return return_value
+ else:
+ return self._return(return_value)
+
+ def get_endpoint_FQN(self):
+ return self._endpoint_FQN
+
+ def _return(self, *args):
+ return args
+
+
+class RemoteSession(object):
+ '''
+ A context object for a Jabber-RPC session.
+ '''
+
+
+ def __init__(self, client, session_close_callback):
+ '''
+ Initializes a new RPC session.
+
+ Arguments:
+ client -- The Slixmpp client associated with this session.
+ session_close_callback -- A callback called when the
+ session is closed.
+ '''
+ self._client = client
+ self._session_close_callback = session_close_callback
+ self._event = threading.Event()
+ self._entries = {}
+ self._callbacks = {}
+ self._acls = {}
+ self._lock = RLock()
+
+ def _wait(self):
+ self._event.wait()
+
+ def _notify(self, event):
+ log.debug("RPC Session as %s started.", self._client.boundjid.full)
+ self._client.send_presence()
+ self._event.set()
+ pass
+
+ def _register_call(self, endpoint, method, name=None):
+ '''
+ Registers a method from an endpoint as remotely callable.
+ '''
+ if name is None:
+ name = method.__name__
+ key = "%s.%s" % (endpoint, name)
+ log.debug("Registering call handler for %s (%s).", key, method)
+ with self._lock:
+ if key in self._entries:
+ raise KeyError("A handler for %s has already been regisered!" % endpoint)
+ self._entries[key] = JabberRPCEntry(endpoint, method)
+ return key
+
+ def _register_acl(self, endpoint, acl):
+ log.debug("Registering ACL %s for endpoint %s.", repr(acl), endpoint)
+ with self._lock:
+ self._acls[endpoint] = acl
+
+ def _register_callback(self, pid, callback):
+ with self._lock:
+ self._callbacks[pid] = callback
+
+ def forget_callback(self, callback):
+ with self._lock:
+ pid = self._find_key(self._callbacks, callback)
+ if pid is not None:
+ del self._callback[pid]
+ else:
+ raise ValueError("Unknown callback!")
+ pass
+
+ def _find_key(self, dict, value):
+ """return the key of dictionary dic given the value"""
+ search = [k for k, v in dict.items() if v == value]
+ if len(search) == 0:
+ return None
+ else:
+ return search[0]
+
+ def _unregister_call(self, key):
+ #removes the registered call
+ with self._lock:
+ if self._entries[key]:
+ del self._entries[key]
+ else:
+ raise ValueError()
+
+ def new_proxy(self, target_jid, endpoint_cls):
+ '''
+ Instantiates a new proxy object, which proxies to a remote
+ endpoint. This method uses a class reference without
+ constructor arguments to instantiate the proxy.
+
+ Arguments:
+ target_jid -- the XMPP entity ID hosting the endpoint.
+ endpoint_cls -- The remote (duck) type.
+ '''
+ try:
+ argspec = inspect.getargspec(endpoint_cls.__init__)
+ args = [None] * (len(argspec[0]) - 1)
+ result = endpoint_cls(*args)
+ Endpoint.__init__(result, self, target_jid)
+ return Proxy(result)
+ except:
+ traceback.print_exc(file=sys.stdout)
+
+ def new_handler(self, acl, handler_cls, *args, **kwargs):
+ '''
+ Instantiates a new handler object, which is called remotely
+ by others. The user can control the effect of the call by
+ implementing the remote method in the local endpoint class. The
+ returned reference can be called locally and will behave as a
+ regular instance.
+
+ Arguments:
+ acl -- Access control list (see ACL class)
+ handler_clss -- The local (duck) type.
+ *args -- Constructor arguments for the local type.
+ **kwargs -- Constructor keyworded arguments for the local
+ type.
+ '''
+ argspec = inspect.getargspec(handler_cls.__init__)
+ base_argspec = inspect.getargspec(Endpoint.__init__)
+ if(argspec == base_argspec):
+ result = handler_cls(self, self._client.boundjid.full)
+ else:
+ result = handler_cls(*args, **kwargs)
+ Endpoint.__init__(result, self, self._client.boundjid.full)
+ method_dict = result.get_methods()
+ for method_name, method in method_dict.items():
+ #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name)
+ self._register_call(result.FQN(), method, method_name)
+ self._register_acl(result.FQN(), acl)
+ return result
+
+# def is_available(self, targetCls, pto):
+# return self._client.is_available(pto)
+
+ def _call_remote(self, pto, pmethod, callback, *arguments):
+ iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments))
+ pid = iq['id']
+ if callback is None:
+ future = Future()
+ self._register_callback(pid, future)
+ iq.send()
+ return future.get_value(30)
+ else:
+ log.debug("[RemoteSession] _call_remote %s", callback)
+ self._register_callback(pid, callback)
+ iq.send()
+
+ def close(self, wait=False):
+ '''
+ Closes this session.
+ '''
+ self._client.disconnect(wait=wait)
+ self._session_close_callback()
+
+ def _on_jabber_rpc_method_call(self, iq):
+ iq.enable('rpc_query')
+ params = iq['rpc_query']['method_call']['params']
+ args = xml2py(params)
+ pmethod = iq['rpc_query']['method_call']['method_name']
+ try:
+ with self._lock:
+ entry = self._entries[pmethod]
+ rules = self._acls[entry.get_endpoint_FQN()]
+ if ACL.check(rules, iq['from'], pmethod):
+ return_value = entry.call_method(args)
+ else:
+ raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from']))
+ if return_value is None:
+ return_value = ()
+ response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value))
+ response.send()
+ except InvocationException as ie:
+ fault = dict()
+ fault['code'] = 500
+ fault['string'] = ie.get_message()
+ self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault))
+ except AuthorizationException as ae:
+ log.error(ae.get_message())
+ error = self._client.plugin['xep_0009']._forbidden(iq)
+ error.send()
+ except Exception as e:
+ if isinstance(e, KeyError):
+ log.error("No handler available for %s!", pmethod)
+ error = self._client.plugin['xep_0009']._item_not_found(iq)
+ else:
+ traceback.print_exc(file=sys.stderr)
+ log.error("An unexpected problem occurred invoking method %s!", pmethod)
+ error = self._client.plugin['xep_0009']._undefined_condition(iq)
+ #! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e
+ error.send()
+
+ def _on_jabber_rpc_method_response(self, iq):
+ iq.enable('rpc_query')
+ args = xml2py(iq['rpc_query']['method_response']['params'])
+ pid = iq['id']
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ if(len(args) > 0):
+ callback.set_value(args[0])
+ else:
+ callback.set_value(None)
+ pass
+
+ def _on_jabber_rpc_method_response2(self, iq):
+ iq.enable('rpc_query')
+ if iq['rpc_query']['method_response']['fault'] is not None:
+ self._on_jabber_rpc_method_fault(iq)
+ else:
+ args = xml2py(iq['rpc_query']['method_response']['params'])
+ pid = iq['id']
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ if(len(args) > 0):
+ callback.set_value(args[0])
+ else:
+ callback.set_value(None)
+ pass
+
+ def _on_jabber_rpc_method_fault(self, iq):
+ iq.enable('rpc_query')
+ fault = xml2fault(iq['rpc_query']['method_response']['fault'])
+ pid = iq['id']
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ e = {
+ 500: InvocationException
+ }[fault['code']](fault['string'])
+ callback.cancel_with_error(e)
+
+ def _on_jabber_rpc_error(self, iq):
+ pid = iq['id']
+ pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query'])
+ code = iq['error']['code']
+ type = iq['error']['type']
+ condition = iq['error']['condition']
+ #! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition)
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ e = {
+ 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])),
+ 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])),
+ 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])),
+ }[condition]
+ if e is None:
+ RemoteException("An unexpected exception occurred at %s!" % iq['from'])
+ callback.cancel_with_error(e)
+
+
+class Remote(object):
+ '''
+ Bootstrap class for Jabber-RPC sessions. New sessions are openend
+ with an existing XMPP client, or one is instantiated on demand.
+ '''
+ _instance = None
+ _sessions = dict()
+ _lock = threading.RLock()
+
+ @classmethod
+ def new_session_with_client(cls, client, callback=None):
+ '''
+ Opens a new session with a given client.
+
+ Arguments:
+ client -- An XMPP client.
+ callback -- An optional callback which can be used to track
+ the starting state of the session.
+ '''
+ with Remote._lock:
+ if(client.boundjid.bare in cls._sessions):
+ raise RemoteException("There already is a session associated with these credentials!")
+ else:
+ cls._sessions[client.boundjid.bare] = client
+
+ def _session_close_callback():
+ with Remote._lock:
+ del cls._sessions[client.boundjid.bare]
+ result = RemoteSession(client, _session_close_callback)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call, threaded=True)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response, threaded=True)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault, threaded=True)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error, threaded=True)
+ if callback is None:
+ start_event_handler = result._notify
+ else:
+ start_event_handler = callback
+ client.add_event_handler("session_start", start_event_handler)
+ if client.connect():
+ client.process(threaded=True)
+ else:
+ raise RemoteException("Could not connect to XMPP server!")
+ pass
+ if callback is None:
+ result._wait()
+ return result
+
+ @classmethod
+ def new_session(cls, jid, password, callback=None):
+ '''
+ Opens a new session and instantiates a new XMPP client.
+
+ Arguments:
+ jid -- The XMPP JID for logging in.
+ password -- The password for logging in.
+ callback -- An optional callback which can be used to track
+ the starting state of the session.
+ '''
+ client = slixmpp.ClientXMPP(jid, password)
+ #? Register plug-ins.
+ client.register_plugin('xep_0004') # Data Forms
+ client.register_plugin('xep_0009') # Jabber-RPC
+ client.register_plugin('xep_0030') # Service Discovery
+ client.register_plugin('xep_0060') # PubSub
+ client.register_plugin('xep_0199') # XMPP Ping
+ return cls.new_session_with_client(client, callback)
+
diff --git a/slixmpp/plugins/xep_0009/rpc.py b/slixmpp/plugins/xep_0009/rpc.py
new file mode 100644
index 00000000..3ce156cf
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/rpc.py
@@ -0,0 +1,223 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.xmlstream import ET, register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0009 import stanza
+from slixmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0009(BasePlugin):
+
+ name = 'xep_0009'
+ description = 'XEP-0009: Jabber-RPC'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, RPCQuery)
+ register_stanza_plugin(RPCQuery, MethodCall)
+ register_stanza_plugin(RPCQuery, MethodResponse)
+
+ self.xmpp.register_handler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
+ self._handle_method_call)
+ )
+ self.xmpp.register_handler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
+ self._handle_method_response)
+ )
+ self.xmpp.register_handler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
+ self._handle_error)
+ )
+ self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call)
+ self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response)
+ self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault)
+ self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error)
+ self.xmpp.add_event_handler('error', self._handle_error)
+ #self.activeCalls = []
+
+ self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp['xep_0030'].add_identity('automation','rpc')
+
+ def make_iq_method_call(self, pto, pmethod, params):
+ iq = self.xmpp.make_iq_set()
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_call']['method_name'] = pmethod
+ iq['rpc_query']['method_call']['params'] = params
+ return iq
+
+ def make_iq_method_response(self, pid, pto, params):
+ iq = self.xmpp.make_iq_result(pid)
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_response']['params'] = params
+ return iq
+
+ def make_iq_method_response_fault(self, pid, pto, params):
+ iq = self.xmpp.make_iq_result(pid)
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_response']['params'] = None
+ iq['rpc_query']['method_response']['fault'] = params
+ return iq
+
+# def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition):
+# iq = self.xmpp.make_iq_error(pid)
+# iq.attrib['to'] = pto
+# iq.attrib['from'] = self.xmpp.boundjid.full
+# iq['error']['code'] = code
+# iq['error']['type'] = type
+# iq['error']['condition'] = condition
+# iq['rpc_query']['method_call']['method_name'] = pmethod
+# iq['rpc_query']['method_call']['params'] = params
+# return iq
+
+ def _item_not_found(self, iq):
+ payload = iq.get_payload()
+ iq = iq.reply()
+ iq.error().set_payload(payload)
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'item-not-found'
+ return iq
+
+ def _undefined_condition(self, iq):
+ payload = iq.get_payload()
+ iq = iq.reply()
+ iq.error().set_payload(payload)
+ iq['error']['code'] = '500'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'undefined-condition'
+ return iq
+
+ def _forbidden(self, iq):
+ payload = iq.get_payload()
+ iq = iq.reply()
+ iq.error().set_payload(payload)
+ iq['error']['code'] = '403'
+ iq['error']['type'] = 'auth'
+ iq['error']['condition'] = 'forbidden'
+ return iq
+
+ def _recipient_unvailable(self, iq):
+ payload = iq.get_payload()
+ iq = iq.reply()
+ error().set_payload(payload)
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'wait'
+ iq['error']['condition'] = 'recipient-unavailable'
+ return iq
+
+ def _handle_method_call(self, iq):
+ type = iq['type']
+ if type == 'set':
+ log.debug("Incoming Jabber-RPC call from %s", iq['from'])
+ self.xmpp.event('jabber_rpc_method_call', iq)
+ else:
+ if type == 'error' and ['rpc_query'] is None:
+ self.handle_error(iq)
+ else:
+ log.debug("Incoming Jabber-RPC error from %s", iq['from'])
+ self.xmpp.event('jabber_rpc_error', iq)
+
+ def _handle_method_response(self, iq):
+ if iq['rpc_query']['method_response']['fault'] is not None:
+ log.debug("Incoming Jabber-RPC fault from %s", iq['from'])
+ #self._on_jabber_rpc_method_fault(iq)
+ self.xmpp.event('jabber_rpc_method_fault', iq)
+ else:
+ log.debug("Incoming Jabber-RPC response from %s", iq['from'])
+ self.xmpp.event('jabber_rpc_method_response', iq)
+
+ def _handle_error(self, iq):
+ print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq)
+ print("#######################")
+ print("### NOT IMPLEMENTED ###")
+ print("#######################")
+
+ def _on_jabber_rpc_method_call(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC method call. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
+ return
+ # Reply with error by default
+ error = self.client.plugin['xep_0009']._item_not_found(iq)
+ error.send()
+
+ def _on_jabber_rpc_method_response(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC method response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
+ error.send()
+
+ def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC fault response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
+ error.send()
+
+ def _on_jabber_rpc_error(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC error response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
+ error.send()
+
+ def _send_fault(self, iq, fault_xml): #
+ fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml)
+ fault.send()
+
+ def _send_error(self, iq):
+ print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq)
+ print("#######################")
+ print("### NOT IMPLEMENTED ###")
+ print("#######################")
+
+ def _extract_method(self, stanza):
+ xml = ET.fromstring("%s" % stanza)
+ return xml.find("./methodCall/methodName").text
+
diff --git a/slixmpp/plugins/xep_0009/stanza/RPC.py b/slixmpp/plugins/xep_0009/stanza/RPC.py
new file mode 100644
index 00000000..3abab8fc
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/stanza/RPC.py
@@ -0,0 +1,64 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.stanzabase import ElementBase
+from xml.etree import cElementTree as ET
+
+
+class RPCQuery(ElementBase):
+ name = 'query'
+ namespace = 'jabber:iq:rpc'
+ plugin_attrib = 'rpc_query'
+ interfaces = set(())
+ subinterfaces = set(())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+
+class MethodCall(ElementBase):
+ name = 'methodCall'
+ namespace = 'jabber:iq:rpc'
+ plugin_attrib = 'method_call'
+ interfaces = set(('method_name', 'params'))
+ subinterfaces = set(())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def get_method_name(self):
+ return self._get_sub_text('methodName')
+
+ def set_method_name(self, value):
+ return self._set_sub_text('methodName', value)
+
+ def get_params(self):
+ return self.xml.find('{%s}params' % self.namespace)
+
+ def set_params(self, params):
+ self.append(params)
+
+
+class MethodResponse(ElementBase):
+ name = 'methodResponse'
+ namespace = 'jabber:iq:rpc'
+ plugin_attrib = 'method_response'
+ interfaces = set(('params', 'fault'))
+ subinterfaces = set(())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def get_params(self):
+ return self.xml.find('{%s}params' % self.namespace)
+
+ def set_params(self, params):
+ self.append(params)
+
+ def get_fault(self):
+ return self.xml.find('{%s}fault' % self.namespace)
+
+ def set_fault(self, fault):
+ self.append(fault)
diff --git a/slixmpp/plugins/xep_0009/stanza/__init__.py b/slixmpp/plugins/xep_0009/stanza/__init__.py
new file mode 100644
index 00000000..79a8a3d7
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/stanza/__init__.py
@@ -0,0 +1,9 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
diff --git a/slixmpp/plugins/xep_0012/__init__.py b/slixmpp/plugins/xep_0012/__init__.py
new file mode 100644
index 00000000..3dd39987
--- /dev/null
+++ b/slixmpp/plugins/xep_0012/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0012.stanza import LastActivity
+from slixmpp.plugins.xep_0012.last_activity import XEP_0012
+
+
+register_plugin(XEP_0012)
+
+
+# Retain some backwards compatibility
+xep_0004 = XEP_0012
diff --git a/slixmpp/plugins/xep_0012/last_activity.py b/slixmpp/plugins/xep_0012/last_activity.py
new file mode 100644
index 00000000..6a7773c1
--- /dev/null
+++ b/slixmpp/plugins/xep_0012/last_activity.py
@@ -0,0 +1,156 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from datetime import datetime, timedelta
+
+from slixmpp.plugins import BasePlugin, register_plugin
+from slixmpp import future_wrapper, Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream import JID, register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.xep_0012 import stanza, LastActivity
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0012(BasePlugin):
+
+ """
+ XEP-0012 Last Activity
+ """
+
+ name = 'xep_0012'
+ description = 'XEP-0012: Last Activity'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, LastActivity)
+
+ self._last_activities = {}
+
+ self.xmpp.register_handler(
+ Callback('Last Activity',
+ StanzaPath('iq@type=get/last_activity'),
+ self._handle_get_last_activity))
+
+ self.api.register(self._default_get_last_activity,
+ 'get_last_activity',
+ default=True)
+ self.api.register(self._default_set_last_activity,
+ 'set_last_activity',
+ default=True)
+ self.api.register(self._default_del_last_activity,
+ 'del_last_activity',
+ default=True)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Last Activity')
+ self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('jabber:iq:last')
+
+ def begin_idle(self, jid=None, status=None):
+ self.set_last_activity(jid, 0, status)
+
+ def end_idle(self, jid=None):
+ self.del_last_activity(jid)
+
+ def start_uptime(self, status=None):
+ self.set_last_activity(jid, 0, status)
+
+ def set_last_activity(self, jid=None, seconds=None, status=None):
+ self.api['set_last_activity'](jid, args={
+ 'seconds': seconds,
+ 'status': status})
+
+ def del_last_activity(self, jid):
+ self.api['del_last_activity'](jid)
+
+ @future_wrapper
+ def get_last_activity(self, jid, local=False, ifrom=None, timeout=None,
+ callback=None, timeout_callback=None):
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+
+ if local or jid in (None, ''):
+ log.debug("Looking up local last activity data for %s", jid)
+ return self.api['get_last_activity'](jid, None, ifrom, None)
+
+ iq = self.xmpp.Iq()
+ iq['from'] = ifrom
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq.enable('last_activity')
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def _handle_get_last_activity(self, iq):
+ log.debug("Received last activity query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ reply = self.api['get_last_activity'](iq['to'], None, iq['from'], iq)
+ reply.send()
+
+ # =================================================================
+ # Default in-memory implementations for storing last activity data.
+ # =================================================================
+
+ def _default_set_last_activity(self, jid, node, ifrom, data):
+ seconds = data.get('seconds', None)
+ if seconds is None:
+ seconds = 0
+
+ status = data.get('status', None)
+ if status is None:
+ status = ''
+
+ self._last_activities[jid] = {
+ 'seconds': datetime.now() - timedelta(seconds=seconds),
+ 'status': status}
+
+ def _default_del_last_activity(self, jid, node, ifrom, data):
+ if jid in self._last_activities:
+ del self._last_activities[jid]
+
+ def _default_get_last_activity(self, jid, node, ifrom, iq):
+ if not isinstance(iq, Iq):
+ reply = self.xmpp.Iq()
+ else:
+ reply = iq.reply()
+
+ if jid not in self._last_activities:
+ raise XMPPError('service-unavailable')
+
+ bare = JID(jid).bare
+
+ if bare != self.xmpp.boundjid.bare:
+ if bare in self.xmpp.roster[jid]:
+ sub = self.xmpp.roster[jid][bare]['subscription']
+ if sub not in ('from', 'both'):
+ raise XMPPError('forbidden')
+
+ td = datetime.now() - self._last_activities[jid]['seconds']
+ seconds = td.seconds + td.days * 24 * 3600
+ status = self._last_activities[jid]['status']
+
+ reply['last_activity']['seconds'] = seconds
+ reply['last_activity']['status'] = status
+
+ return reply
diff --git a/slixmpp/plugins/xep_0012/stanza.py b/slixmpp/plugins/xep_0012/stanza.py
new file mode 100644
index 00000000..bd539a2d
--- /dev/null
+++ b/slixmpp/plugins/xep_0012/stanza.py
@@ -0,0 +1,32 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class LastActivity(ElementBase):
+
+ name = 'query'
+ namespace = 'jabber:iq:last'
+ plugin_attrib = 'last_activity'
+ interfaces = set(('seconds', 'status'))
+
+ def get_seconds(self):
+ return int(self._get_attr('seconds'))
+
+ def set_seconds(self, value):
+ self._set_attr('seconds', str(value))
+
+ def get_status(self):
+ return self.xml.text
+
+ def set_status(self, value):
+ self.xml.text = str(value)
+
+ def del_status(self):
+ self.xml.text = ''
diff --git a/slixmpp/plugins/xep_0013/__init__.py b/slixmpp/plugins/xep_0013/__init__.py
new file mode 100644
index 00000000..c02a6c5a
--- /dev/null
+++ b/slixmpp/plugins/xep_0013/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0013.stanza import Offline
+from slixmpp.plugins.xep_0013.offline import XEP_0013
+
+
+register_plugin(XEP_0013)
diff --git a/slixmpp/plugins/xep_0013/offline.py b/slixmpp/plugins/xep_0013/offline.py
new file mode 100644
index 00000000..51840e7b
--- /dev/null
+++ b/slixmpp/plugins/xep_0013/offline.py
@@ -0,0 +1,124 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.stanza import Message, Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream.handler import Collector
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0013 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0013(BasePlugin):
+
+ """
+ XEP-0013 Flexible Offline Message Retrieval
+ """
+
+ name = 'xep_0013'
+ description = 'XEP-0013: Flexible Offline Message Retrieval'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.Offline)
+ register_stanza_plugin(Message, stanza.Offline)
+
+ def get_count(self, **kwargs):
+ return self.xmpp['xep_0030'].get_info(
+ node='http://jabber.org/protocol/offline',
+ local=False,
+ **kwargs)
+
+ def get_headers(self, **kwargs):
+ return self.xmpp['xep_0030'].get_items(
+ node='http://jabber.org/protocol/offline',
+ local=False,
+ **kwargs)
+
+ def view(self, nodes, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ if not isinstance(nodes, (list, set)):
+ nodes = [nodes]
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ offline = iq['offline']
+ for node in nodes:
+ item = stanza.Item()
+ item['node'] = node
+ item['action'] = 'view'
+ offline.append(item)
+
+ collector = Collector(
+ 'Offline_Results_%s' % iq['id'],
+ StanzaPath('message/offline'))
+ self.xmpp.register_handler(collector)
+
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['offline']['results'] = results
+ callback(iq)
+ iq.send(timeout=timeout, callback=wrapped_cb,
+ timeout_callback=timeout_callback)
+
+ def remove(self, nodes, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ if not isinstance(nodes, (list, set)):
+ nodes = [nodes]
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ offline = iq['offline']
+ for node in nodes:
+ item = stanza.Item()
+ item['node'] = node
+ item['action'] = 'remove'
+ offline.append(item)
+
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def fetch(self, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['offline']['fetch'] = True
+
+ collector = Collector(
+ 'Offline_Results_%s' % iq['id'],
+ StanzaPath('message/offline'))
+ self.xmpp.register_handler(collector)
+
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['offline']['results'] = results
+ callback(iq)
+ iq.send(timeout=timeout, callback=wrapped_cb,
+ timeout_callback=timeout_callback)
+
+ def purge(self, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['offline']['purge'] = True
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0013/stanza.py b/slixmpp/plugins/xep_0013/stanza.py
new file mode 100644
index 00000000..cf9c385b
--- /dev/null
+++ b/slixmpp/plugins/xep_0013/stanza.py
@@ -0,0 +1,53 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class Offline(ElementBase):
+ name = 'offline'
+ namespace = 'http://jabber.org/protocol/offline'
+ plugin_attrib = 'offline'
+ interfaces = set(['fetch', 'purge', 'results'])
+ bool_interfaces = interfaces
+
+ def setup(self, xml=None):
+ ElementBase.setup(self, xml)
+ self._results = []
+
+ # The results interface is meant only as an easy
+ # way to access the set of collected message responses
+ # from the query.
+
+ def get_results(self):
+ return self._results
+
+ def set_results(self, values):
+ self._results = values
+
+ def del_results(self):
+ self._results = []
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'http://jabber.org/protocol/offline'
+ plugin_attrib = 'item'
+ interfaces = set(['action', 'node', 'jid'])
+
+ actions = set(['view', 'remove'])
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+
+register_stanza_plugin(Offline, Item, iterable=True)
diff --git a/slixmpp/plugins/xep_0016/__init__.py b/slixmpp/plugins/xep_0016/__init__.py
new file mode 100644
index 00000000..aa95c78d
--- /dev/null
+++ b/slixmpp/plugins/xep_0016/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0016 import stanza
+from slixmpp.plugins.xep_0016.stanza import Privacy
+from slixmpp.plugins.xep_0016.privacy import XEP_0016
+
+
+register_plugin(XEP_0016)
diff --git a/slixmpp/plugins/xep_0016/privacy.py b/slixmpp/plugins/xep_0016/privacy.py
new file mode 100644
index 00000000..38444b2b
--- /dev/null
+++ b/slixmpp/plugins/xep_0016/privacy.py
@@ -0,0 +1,127 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Iq
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0016 import stanza
+from slixmpp.plugins.xep_0016.stanza import Privacy, Item
+
+
+class XEP_0016(BasePlugin):
+
+ name = 'xep_0016'
+ description = 'XEP-0016: Privacy Lists'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Privacy)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Privacy.namespace)
+
+ def get_privacy_lists(self, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('privacy')
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def get_list(self, name, timeout=None, callback=None, timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy']['list']['name'] = name
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def get_active(self, timeout=None, callback=None, timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('active')
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def get_default(self, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('default')
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def activate(self, name, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['active']['name'] = name
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def deactivate(self, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('active')
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def make_default(self, name, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['default']['name'] = name
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def remove_default(self, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('default')
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def edit_list(self, name, rules, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ priv_list = iq['privacy']['list']
+
+ if not rules:
+ rules = []
+
+ for rule in rules:
+ if isinstance(rule, Item):
+ priv_list.append(rule)
+ continue
+
+ priv_list.add_item(
+ rule['value'],
+ rule['action'],
+ rule['order'],
+ itype=rule.get('type', None),
+ iq=rule.get('iq', None),
+ message=rule.get('message', None),
+ presence_in=rule.get('presence_in',
+ rule.get('presence-in', None)),
+ presence_out=rule.get('presence_out',
+ rule.get('presence-out', None)))
+
+ def remove_list(self, name, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0016/stanza.py b/slixmpp/plugins/xep_0016/stanza.py
new file mode 100644
index 00000000..58c9fdb3
--- /dev/null
+++ b/slixmpp/plugins/xep_0016/stanza.py
@@ -0,0 +1,103 @@
+from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Privacy(ElementBase):
+ name = 'query'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = 'privacy'
+ interfaces = set()
+
+ def add_list(self, name):
+ priv_list = List()
+ priv_list['name'] = name
+ self.append(priv_list)
+ return priv_list
+
+
+class Active(ElementBase):
+ name = 'active'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class Default(ElementBase):
+ name = 'default'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class List(ElementBase):
+ name = 'list'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ plugin_multi_attrib = 'lists'
+ interfaces = set(['name'])
+
+ def add_item(self, value, action, order, itype=None, iq=False,
+ message=False, presence_in=False, presence_out=False):
+ item = Item()
+ item.values = {'type': itype,
+ 'value': value,
+ 'action': action,
+ 'order': order,
+ 'message': message,
+ 'iq': iq,
+ 'presence_in': presence_in,
+ 'presence_out': presence_out}
+ self.append(item)
+ return item
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ plugin_multi_attrib = 'items'
+ interfaces = set(['type', 'value', 'action', 'order', 'iq',
+ 'message', 'presence_in', 'presence_out'])
+ bool_interfaces = set(['message', 'iq', 'presence_in', 'presence_out'])
+
+ type_values = ('', 'jid', 'group', 'subscription')
+ action_values = ('allow', 'deny')
+
+ def set_type(self, value):
+ if value and value not in self.type_values:
+ raise ValueError('Unknown type value: %s' % value)
+ else:
+ self._set_attr('type', value)
+
+ def set_action(self, value):
+ if value not in self.action_values:
+ raise ValueError('Unknown action value: %s' % value)
+ else:
+ self._set_attr('action', value)
+
+ def set_presence_in(self, value):
+ keep = True if value else False
+ self._set_sub_text('presence-in', '', keep=keep)
+
+ def get_presence_in(self):
+ pres = self.xml.find('{%s}presence-in' % self.namespace)
+ return pres is not None
+
+ def del_presence_in(self):
+ self._del_sub('{%s}presence-in' % self.namespace)
+
+ def set_presence_out(self, value):
+ keep = True if value else False
+ self._set_sub_text('presence-in', '', keep=keep)
+
+ def get_presence_out(self):
+ pres = self.xml.find('{%s}presence-in' % self.namespace)
+ return pres is not None
+
+ def del_presence_out(self):
+ self._del_sub('{%s}presence-in' % self.namespace)
+
+
+register_stanza_plugin(Privacy, Active)
+register_stanza_plugin(Privacy, Default)
+register_stanza_plugin(Privacy, List, iterable=True)
+register_stanza_plugin(List, Item, iterable=True)
diff --git a/slixmpp/plugins/xep_0020/__init__.py b/slixmpp/plugins/xep_0020/__init__.py
new file mode 100644
index 00000000..53d96fe4
--- /dev/null
+++ b/slixmpp/plugins/xep_0020/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0020 import stanza
+from slixmpp.plugins.xep_0020.stanza import FeatureNegotiation
+from slixmpp.plugins.xep_0020.feature_negotiation import XEP_0020
+
+
+register_plugin(XEP_0020)
diff --git a/slixmpp/plugins/xep_0020/feature_negotiation.py b/slixmpp/plugins/xep_0020/feature_negotiation.py
new file mode 100644
index 00000000..2f4e8c15
--- /dev/null
+++ b/slixmpp/plugins/xep_0020/feature_negotiation.py
@@ -0,0 +1,36 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq, Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins.xep_0020 import stanza, FeatureNegotiation
+from slixmpp.plugins.xep_0004 import Form
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0020(BasePlugin):
+
+ name = 'xep_0020'
+ description = 'XEP-0020: Feature Negotiation'
+ dependencies = set(['xep_0004', 'xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp['xep_0030'].add_feature(FeatureNegotiation.namespace)
+
+ register_stanza_plugin(FeatureNegotiation, Form)
+
+ register_stanza_plugin(Iq, FeatureNegotiation)
+ register_stanza_plugin(Message, FeatureNegotiation)
diff --git a/slixmpp/plugins/xep_0020/stanza.py b/slixmpp/plugins/xep_0020/stanza.py
new file mode 100644
index 00000000..d9cea8f4
--- /dev/null
+++ b/slixmpp/plugins/xep_0020/stanza.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class FeatureNegotiation(ElementBase):
+
+ name = 'feature'
+ namespace = 'http://jabber.org/protocol/feature-neg'
+ plugin_attrib = 'feature_neg'
+ interfaces = set()
diff --git a/slixmpp/plugins/xep_0027/__init__.py b/slixmpp/plugins/xep_0027/__init__.py
new file mode 100644
index 00000000..1f510cb2
--- /dev/null
+++ b/slixmpp/plugins/xep_0027/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0027.stanza import Signed, Encrypted
+from slixmpp.plugins.xep_0027.gpg import XEP_0027
+
+
+register_plugin(XEP_0027)
diff --git a/slixmpp/plugins/xep_0027/gpg.py b/slixmpp/plugins/xep_0027/gpg.py
new file mode 100644
index 00000000..a0b1df48
--- /dev/null
+++ b/slixmpp/plugins/xep_0027/gpg.py
@@ -0,0 +1,169 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.thirdparty import GPG
+
+from slixmpp.stanza import Presence, Message
+from slixmpp.plugins.base import BasePlugin, register_plugin
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.xep_0027 import stanza, Signed, Encrypted
+
+
+def _extract_data(data, kind):
+ stripped = []
+ begin_headers = False
+ begin_data = False
+ for line in data.split('\n'):
+ if not begin_headers and 'BEGIN PGP %s' % kind in line:
+ begin_headers = True
+ continue
+ if begin_headers and line.strip() == '':
+ begin_data = True
+ continue
+ if 'END PGP %s' % kind in line:
+ return '\n'.join(stripped)
+ if begin_data:
+ stripped.append(line)
+ return ''
+
+
+class XEP_0027(BasePlugin):
+
+ name = 'xep_0027'
+ description = 'XEP-0027: Current Jabber OpenPGP Usage'
+ dependencies = set()
+ stanza = stanza
+ default_config = {
+ 'gpg_binary': 'gpg',
+ 'gpg_home': '',
+ 'use_agent': True,
+ 'keyring': None,
+ 'key_server': 'pgp.mit.edu'
+ }
+
+ def plugin_init(self):
+ self.gpg = GPG(gnupghome=self.gpg_home,
+ gpgbinary=self.gpg_binary,
+ use_agent=self.use_agent,
+ keyring=self.keyring)
+
+ self.xmpp.add_filter('out', self._sign_presence)
+
+ self._keyids = {}
+
+ self.api.register(self._set_keyid, 'set_keyid', default=True)
+ self.api.register(self._get_keyid, 'get_keyid', default=True)
+ self.api.register(self._del_keyid, 'del_keyid', default=True)
+ self.api.register(self._get_keyids, 'get_keyids', default=True)
+
+ register_stanza_plugin(Presence, Signed)
+ register_stanza_plugin(Message, Encrypted)
+
+ self.xmpp.add_event_handler('unverified_signed_presence',
+ self._handle_unverified_signed_presence)
+
+ self.xmpp.register_handler(
+ Callback('Signed Presence',
+ StanzaPath('presence/signed'),
+ self._handle_signed_presence))
+
+ self.xmpp.register_handler(
+ Callback('Encrypted Message',
+ StanzaPath('message/encrypted'),
+ self._handle_encrypted_message))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Encrypted Message')
+ self.xmpp.remove_handler('Signed Presence')
+ self.xmpp.del_filter('out', self._sign_presence)
+ self.xmpp.del_event_handler('unverified_signed_presence',
+ self._handle_unverified_signed_presence)
+
+ def _sign_presence(self, stanza):
+ if isinstance(stanza, Presence):
+ if stanza['type'] == 'available' or \
+ stanza['type'] in Presence.showtypes:
+ stanza['signed'] = stanza['status']
+ return stanza
+
+ def sign(self, data, jid=None):
+ keyid = self.get_keyid(jid)
+ if keyid:
+ signed = self.gpg.sign(data, keyid=keyid)
+ return _extract_data(signed.data, 'SIGNATURE')
+
+ def encrypt(self, data, jid=None):
+ keyid = self.get_keyid(jid)
+ if keyid:
+ enc = self.gpg.encrypt(data, keyid)
+ return _extract_data(enc.data, 'MESSAGE')
+
+ def decrypt(self, data, jid=None):
+ template = '-----BEGIN PGP MESSAGE-----\n' + \
+ '\n' + \
+ '%s\n' + \
+ '-----END PGP MESSAGE-----\n'
+ dec = self.gpg.decrypt(template % data)
+ return dec.data
+
+ def verify(self, data, sig, jid=None):
+ template = '-----BEGIN PGP SIGNED MESSAGE-----\n' + \
+ 'Hash: SHA1\n' + \
+ '\n' + \
+ '%s\n' + \
+ '-----BEGIN PGP SIGNATURE-----\n' + \
+ '\n' + \
+ '%s\n' + \
+ '-----END PGP SIGNATURE-----\n'
+ v = self.gpg.verify(template % (data, sig))
+ return v
+
+ def set_keyid(self, jid=None, keyid=None):
+ self.api['set_keyid'](jid, args=keyid)
+
+ def get_keyid(self, jid=None):
+ return self.api['get_keyid'](jid)
+
+ def del_keyid(self, jid=None):
+ self.api['del_keyid'](jid)
+
+ def get_keyids(self):
+ return self.api['get_keyids']()
+
+ def _handle_signed_presence(self, pres):
+ self.xmpp.event('unverified_signed_presence', pres)
+
+ def _handle_unverified_signed_presence(self, pres):
+ verified = self.verify(pres['status'], pres['signed'])
+ if verified.key_id:
+ if not self.get_keyid(pres['from']):
+ known_keyids = [e['keyid'] for e in self.gpg.list_keys()]
+ if verified.key_id not in known_keyids:
+ self.gpg.recv_keys(self.key_server, verified.key_id)
+ self.set_keyid(jid=pres['from'], keyid=verified.key_id)
+ self.xmpp.event('signed_presence', pres)
+
+ def _handle_encrypted_message(self, msg):
+ self.xmpp.event('encrypted_message', msg)
+
+ # =================================================================
+
+ def _set_keyid(self, jid, node, ifrom, keyid):
+ self._keyids[jid] = keyid
+
+ def _get_keyid(self, jid, node, ifrom, keyid):
+ return self._keyids.get(jid, None)
+
+ def _del_keyid(self, jid, node, ifrom, keyid):
+ if jid in self._keyids:
+ del self._keyids[jid]
+
+ def _get_keyids(self, jid, node, ifrom, data):
+ return self._keyids
diff --git a/slixmpp/plugins/xep_0027/stanza.py b/slixmpp/plugins/xep_0027/stanza.py
new file mode 100644
index 00000000..fd41cace
--- /dev/null
+++ b/slixmpp/plugins/xep_0027/stanza.py
@@ -0,0 +1,53 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Signed(ElementBase):
+ name = 'x'
+ namespace = 'jabber:x:signed'
+ plugin_attrib = 'signed'
+ interfaces = set(['signed'])
+ is_extension = True
+
+ def set_signed(self, value):
+ parent = self.parent()
+ xmpp = parent.stream
+ data = xmpp['xep_0027'].sign(value, parent['from'])
+ if data:
+ self.xml.text = data
+ else:
+ del parent['signed']
+
+ def get_signed(self):
+ return self.xml.text
+
+
+class Encrypted(ElementBase):
+ name = 'x'
+ namespace = 'jabber:x:encrypted'
+ plugin_attrib = 'encrypted'
+ interfaces = set(['encrypted'])
+ is_extension = True
+
+ def set_encrypted(self, value):
+ parent = self.parent()
+ xmpp = parent.stream
+ data = xmpp['xep_0027'].encrypt(value, parent['to'])
+ if data:
+ self.xml.text = data
+ else:
+ del parent['encrypted']
+
+ def get_encrypted(self):
+ parent = self.parent()
+ xmpp = parent.stream
+ if self.xml.text:
+ return xmpp['xep_0027'].decrypt(self.xml.text, parent['to'])
+ return None
diff --git a/slixmpp/plugins/xep_0030/__init__.py b/slixmpp/plugins/xep_0030/__init__.py
new file mode 100644
index 00000000..1fc71069
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/__init__.py
@@ -0,0 +1,22 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0030 import stanza
+from slixmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
+from slixmpp.plugins.xep_0030.static import StaticDisco
+from slixmpp.plugins.xep_0030.disco import XEP_0030
+
+
+register_plugin(XEP_0030)
+
+# Retain some backwards compatibility
+xep_0030 = XEP_0030
+XEP_0030.getInfo = XEP_0030.get_info
+XEP_0030.make_static = XEP_0030.restore_defaults
diff --git a/slixmpp/plugins/xep_0030/disco.py b/slixmpp/plugins/xep_0030/disco.py
new file mode 100644
index 00000000..e6286b92
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/disco.py
@@ -0,0 +1,748 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp import future_wrapper
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
+from slixmpp.plugins.xep_0030 import StaticDisco
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0030(BasePlugin):
+
+ """
+ XEP-0030: Service Discovery
+
+ Service discovery in XMPP allows entities to discover information about
+ other agents in the network, such as the feature sets supported by a
+ client, or signposts to other, related entities.
+
+ Also see <http://www.xmpp.org/extensions/xep-0030.html>.
+
+ The XEP-0030 plugin works using a hierarchy of dynamic
+ node handlers, ranging from global handlers to specific
+ JID+node handlers. The default set of handlers operate
+ in a static manner, storing disco information in memory.
+ However, custom handlers may use any available backend
+ storage mechanism desired, such as SQLite or Redis.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Stream Handlers:
+ Disco Info -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#info.
+ Disco Items -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#items.
+
+ Events:
+ disco_info -- Received a disco#info Iq query result.
+ disco_items -- Received a disco#items Iq query result.
+ disco_info_query -- Received a disco#info Iq query request.
+ disco_items_query -- Received a disco#items Iq query request.
+
+ Attributes:
+ stanza -- A reference to the module containing the
+ stanza classes provided by this plugin.
+ static -- Object containing the default set of
+ static node handlers.
+ default_handlers -- A dictionary mapping operations to the default
+ global handler (by default, the static handlers).
+ xmpp -- The main Slixmpp object.
+
+ Methods:
+ set_node_handler -- Assign a handler to a JID/node combination.
+ del_node_handler -- Remove a handler from a JID/node combination.
+ get_info -- Retrieve disco#info data, locally or remote.
+ get_items -- Retrieve disco#items data, locally or remote.
+ set_identities --
+ set_features --
+ set_items --
+ del_items --
+ del_identity --
+ del_feature --
+ del_item --
+ add_identity --
+ add_feature --
+ add_item --
+ """
+
+ name = 'xep_0030'
+ description = 'XEP-0030: Service Discovery'
+ dependencies = set()
+ stanza = stanza
+ default_config = {
+ 'use_cache': True,
+ 'wrap_results': False
+ }
+
+ def plugin_init(self):
+ """
+ Start the XEP-0030 plugin.
+ """
+ self.xmpp.register_handler(
+ Callback('Disco Info',
+ StanzaPath('iq/disco_info'),
+ self._handle_disco_info))
+
+ self.xmpp.register_handler(
+ Callback('Disco Items',
+ StanzaPath('iq/disco_items'),
+ self._handle_disco_items))
+
+ register_stanza_plugin(Iq, DiscoInfo)
+ register_stanza_plugin(Iq, DiscoItems)
+
+ self.static = StaticDisco(self.xmpp, self)
+
+ self._disco_ops = [
+ 'get_info', 'set_info', 'set_identities', 'set_features',
+ 'get_items', 'set_items', 'del_items', 'add_identity',
+ 'del_identity', 'add_feature', 'del_feature', 'add_item',
+ 'del_item', 'del_identities', 'del_features', 'cache_info',
+ 'get_cached_info', 'supports', 'has_identity']
+
+ for op in self._disco_ops:
+ self.api.register(getattr(self.static, op), op, default=True)
+
+ def session_bind(self, jid):
+ self.add_feature('http://jabber.org/protocol/disco#info')
+
+ def plugin_end(self):
+ self.del_feature('http://jabber.org/protocol/disco#info')
+
+ def _add_disco_op(self, op, default_handler):
+ self.api.register(default_handler, op)
+ self.api.register_default(default_handler, op)
+
+ def set_node_handler(self, htype, jid=None, node=None, handler=None):
+ """
+ Add a node handler for the given hierarchy level and
+ handler type.
+
+ Node handlers are ordered in a hierarchy where the
+ most specific handler is executed. Thus, a fallback,
+ global handler can be used for the majority of cases
+ with a few node specific handler that override the
+ global behavior.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Handler types:
+ get_info
+ get_items
+ set_identities
+ set_features
+ set_items
+ del_items
+ del_identities
+ del_identity
+ del_feature
+ del_features
+ del_item
+ add_identity
+ add_feature
+ add_item
+
+ Arguments:
+ htype -- The operation provided by the handler.
+ jid -- The JID the handler applies to. May be narrowed
+ further if a node is given.
+ node -- The particular node the handler is for. If no JID
+ is given, then the self.xmpp.boundjid.full is
+ assumed.
+ handler -- The handler function to use.
+ """
+ self.api.register(handler, htype, jid, node)
+
+ def del_node_handler(self, htype, jid, node):
+ """
+ Remove a handler type for a JID and node combination.
+
+ The next handler in the hierarchy will be used if one
+ exists. If removing the global handler, make sure that
+ other handlers exist to process existing nodes.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Arguments:
+ htype -- The type of handler to remove.
+ jid -- The JID from which to remove the handler.
+ node -- The node from which to remove the handler.
+ """
+ self.api.unregister(htype, jid, node)
+
+ def restore_defaults(self, jid=None, node=None, handlers=None):
+ """
+ Change all or some of a node's handlers to the default
+ handlers. Useful for manually overriding the contents
+ of a node that would otherwise be handled by a JID level
+ or global level dynamic handler.
+
+ The default is to use the built-in static handlers, but that
+ may be changed by modifying self.default_handlers.
+
+ Arguments:
+ jid -- The JID owning the node to modify.
+ node -- The node to change to using static handlers.
+ handlers -- Optional list of handlers to change to the
+ default version. If provided, only these
+ handlers will be changed. Otherwise, all
+ handlers will use the default version.
+ """
+ if handlers is None:
+ handlers = self._disco_ops
+ for op in handlers:
+ self.api.restore_default(op, jid, node)
+
+ def supports(self, jid=None, node=None, feature=None, local=False,
+ cached=True, ifrom=None):
+ """
+ Check if a JID supports a given feature.
+
+ Return values:
+ True -- The feature is supported
+ False -- The feature is not listed as supported
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ feature -- The name of the feature to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'feature': feature,
+ 'local': local,
+ 'cached': cached}
+ return self.api['supports'](jid, node, ifrom, data)
+
+ def has_identity(self, jid=None, node=None, category=None, itype=None,
+ lang=None, local=False, cached=True, ifrom=None):
+ """
+ Check if a JID provides a given identity.
+
+ Return values:
+ True -- The identity is provided
+ False -- The identity is not listed
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'category': category,
+ 'itype': itype,
+ 'lang': lang,
+ 'local': local,
+ 'cached': cached}
+ return self.api['has_identity'](jid, node, ifrom, data)
+
+ @future_wrapper
+ def get_info(self, jid=None, node=None, local=None,
+ cached=None, **kwargs):
+ """
+ Retrieve the disco#info results from a given JID/node combination.
+
+ Info may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the information should be gathered
+ by executing the local node handlers, or if a disco#info stanza
+ must be generated and sent.
+
+ If requesting items from a local JID/node, then only a DiscoInfo
+ stanza will be returned. Otherwise, an Iq stanza will be returned.
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ timeout -- The time in seconds to wait for reply, before
+ calling timeout_callback
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ timeout_callback -- Optional callback to execute when no result
+ has been received in timeout seconds.
+ """
+ if local is None:
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+ elif jid in (None, ''):
+ local = True
+
+ if local:
+ log.debug("Looking up local disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self.api['get_info'](jid, node,
+ kwargs.get('ifrom', None),
+ kwargs)
+ info = self._fix_default_info(info)
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ if cached:
+ log.debug("Looking up cached disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self.api['get_cached_info'](jid, node,
+ kwargs.get('ifrom', None),
+ kwargs)
+ if info is not None:
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ iq = self.xmpp.Iq()
+ # Check dfrom parameter for backwards compatibility
+ iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq['disco_info']['node'] = node if node else ''
+ return iq.send(timeout=kwargs.get('timeout', None),
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None))
+
+ def set_info(self, jid=None, node=None, info=None):
+ """
+ Set the disco#info data for a JID/node based on an existing
+ disco#info stanza.
+ """
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ self.api['set_info'](jid, node, None, info)
+
+ @future_wrapper
+ def get_items(self, jid=None, node=None, local=False, **kwargs):
+ """
+ Retrieve the disco#items results from a given JID/node combination.
+
+ Items may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the items should be gathered by
+ executing the local node handlers, or if a disco#items stanza must
+ be generated and sent.
+
+ If requesting items from a local JID/node, then only a DiscoItems
+ stanza will be returned. Otherwise, an Iq stanza will be returned.
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the items.
+ ifrom -- Specifiy the sender's JID.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ iterator -- If True, return a result set iterator using
+ the XEP-0059 plugin, if the plugin is loaded.
+ Otherwise the parameter is ignored.
+ timeout_callback -- Optional callback to execute when no result
+ has been received in timeout seconds.
+ """
+ if local or local is None and jid is None:
+ items = self.api['get_items'](jid, node,
+ kwargs.get('ifrom', None),
+ kwargs)
+ return self._wrap(kwargs.get('ifrom', None), jid, items)
+
+ iq = self.xmpp.Iq()
+ # Check dfrom parameter for backwards compatibility
+ iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq['disco_items']['node'] = node if node else ''
+ if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
+ raise NotImplementedError("XEP 0059 has not yet been fixed")
+ return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
+ else:
+ return iq.send(timeout=kwargs.get('timeout', None),
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None))
+
+ def set_items(self, jid=None, node=None, **kwargs):
+ """
+ Set or replace all items for the specified JID/node combination.
+
+ The given items must be in a list or set where each item is a
+ tuple of the form: (jid, node, name).
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ items -- A series of items in tuple format.
+ """
+ self.api['set_items'](jid, node, None, kwargs)
+
+ def del_items(self, jid=None, node=None, **kwargs):
+ """
+ Remove all items from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ """
+ self.api['del_items'](jid, node, None, kwargs)
+
+ def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
+ """
+ Add a new item element to the given JID/node combination.
+
+ Each item is required to have a JID, but may also specify
+ a node value to reference non-addressable entities.
+
+ Arguments:
+ jid -- The JID for the item.
+ name -- Optional name for the item.
+ node -- The node to modify.
+ subnode -- Optional node for the item.
+ ijid -- The JID to modify.
+ """
+ if not jid:
+ jid = self.xmpp.boundjid.full
+ kwargs = {'ijid': jid,
+ 'name': name,
+ 'inode': subnode}
+ self.api['add_item'](ijid, node, None, kwargs)
+
+ def del_item(self, jid=None, node=None, **kwargs):
+ """
+ Remove a single item from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ ijid -- The item's JID.
+ inode -- The item's node.
+ """
+ self.api['del_item'](jid, node, None, kwargs)
+
+ def add_identity(self, category='', itype='', name='',
+ node=None, jid=None, lang=None):
+ """
+ Add a new identity to the given JID/node combination.
+
+ Each identity must be unique in terms of all four identity
+ components: category, type, name, and language.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the
+ names are different. A category and type is always required.
+
+ Arguments:
+ category -- The identity's category.
+ itype -- The identity's type.
+ name -- Optional name for the identity.
+ lang -- Optional two-letter language code.
+ node -- The node to modify.
+ jid -- The JID to modify.
+ """
+ kwargs = {'category': category,
+ 'itype': itype,
+ 'name': name,
+ 'lang': lang}
+ self.api['add_identity'](jid, node, None, kwargs)
+
+ def add_feature(self, feature, node=None, jid=None):
+ """
+ Add a feature to a JID/node combination.
+
+ Arguments:
+ feature -- The namespace of the supported feature.
+ node -- The node to modify.
+ jid -- The JID to modify.
+ """
+ kwargs = {'feature': feature}
+ self.api['add_feature'](jid, node, None, kwargs)
+
+ def del_identity(self, jid=None, node=None, **kwargs):
+ """
+ Remove an identity from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ category -- The identity's category.
+ itype -- The identity's type value.
+ name -- Optional, human readable name for the identity.
+ lang -- Optional, the identity's xml:lang value.
+ """
+ self.api['del_identity'](jid, node, None, kwargs)
+
+ def del_feature(self, jid=None, node=None, **kwargs):
+ """
+ Remove a feature from a given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ feature -- The feature's namespace.
+ """
+ self.api['del_feature'](jid, node, None, kwargs)
+
+ def set_identities(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace all identities for the given JID/node combination.
+
+ The identities must be in a set where each identity is a tuple
+ of the form: (category, type, lang, name)
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ identities -- A set of identities in tuple form.
+ lang -- Optional, xml:lang value.
+ """
+ self.api['set_identities'](jid, node, None, kwargs)
+
+ def del_identities(self, jid=None, node=None, **kwargs):
+ """
+ Remove all identities for a JID/node combination.
+
+ If a language is specified, only identities using that
+ language will be removed.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ lang -- Optional. If given, only remove identities
+ using this xml:lang value.
+ """
+ self.api['del_identities'](jid, node, None, kwargs)
+
+ def set_features(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace the set of supported features
+ for a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ features -- The new set of supported features.
+ """
+ self.api['set_features'](jid, node, None, kwargs)
+
+ def del_features(self, jid=None, node=None, **kwargs):
+ """
+ Remove all features from a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ """
+ self.api['del_features'](jid, node, None, kwargs)
+
+ def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None):
+ """
+ Execute the most specific node handler for the given
+ JID/node combination.
+
+ Arguments:
+ htype -- The handler type to execute.
+ jid -- The JID requested.
+ node -- The node requested.
+ data -- Optional, custom data to pass to the handler.
+ """
+ if not data:
+ data = {}
+
+ return self.api[htype](jid, node, ifrom, data)
+
+ def _handle_disco_info(self, iq):
+ """
+ Process an incoming disco#info stanza. If it is a get
+ request, find and return the appropriate identities
+ and features. If it is an info result, fire the
+ disco_info event.
+
+ Arguments:
+ iq -- The incoming disco#items stanza.
+ """
+ if iq['type'] == 'get':
+ log.debug("Received disco info query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ info = self.api['get_info'](iq['to'],
+ iq['disco_info']['node'],
+ iq['from'],
+ iq)
+ if isinstance(info, Iq):
+ info['id'] = iq['id']
+ info.send()
+ else:
+ iq = iq.reply()
+ if info:
+ info = self._fix_default_info(info)
+ iq.set_payload(info.xml)
+ iq.send()
+ elif iq['type'] == 'result':
+ log.debug("Received disco info result from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.use_cache:
+ log.debug("Caching disco info result from " \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ ito = iq['to'].full
+ else:
+ ito = None
+ self.api['cache_info'](iq['from'],
+ iq['disco_info']['node'],
+ ito,
+ iq)
+ self.xmpp.event('disco_info', iq)
+
+ def _handle_disco_items(self, iq):
+ """
+ Process an incoming disco#items stanza. If it is a get
+ request, find and return the appropriate items. If it
+ is an items result, fire the disco_items event.
+
+ Arguments:
+ iq -- The incoming disco#items stanza.
+ """
+ if iq['type'] == 'get':
+ log.debug("Received disco items query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ items = self.api['get_items'](iq['to'],
+ iq['disco_items']['node'],
+ iq['from'],
+ iq)
+ if isinstance(items, Iq):
+ items.send()
+ else:
+ iq = iq.reply()
+ if items:
+ iq.set_payload(items.xml)
+ iq.send()
+ elif iq['type'] == 'result':
+ log.debug("Received disco items result from " + \
+ "%s to %s.", iq['from'], iq['to'])
+ self.xmpp.event('disco_items', iq)
+
+ def _fix_default_info(self, info):
+ """
+ Disco#info results for a JID are required to include at least
+ one identity and feature. As a default, if no other identity is
+ provided, Slixmpp will use either the generic component or the
+ bot client identity. A the standard disco#info feature will also be
+ added if no features are provided.
+
+ Arguments:
+ info -- The disco#info quest (not the full Iq stanza) to modify.
+ """
+ result = info
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ if not info['node']:
+ if not info['identities']:
+ if self.xmpp.is_component:
+ log.debug("No identity found for this entity. " + \
+ "Using default component identity.")
+ info.add_identity('component', 'generic')
+ else:
+ log.debug("No identity found for this entity. " + \
+ "Using default client identity.")
+ info.add_identity('client', 'bot')
+ if not info['features']:
+ log.debug("No features found for this entity. " + \
+ "Using default disco#info feature.")
+ info.add_feature(info.namespace)
+ return result
+
+ def _wrap(self, ito, ifrom, payload, force=False):
+ """
+ Ensure that results are wrapped in an Iq stanza
+ if self.wrap_results has been set to True.
+
+ Arguments:
+ ito -- The JID to use as the 'to' value
+ ifrom -- The JID to use as the 'from' value
+ payload -- The disco data to wrap
+ force -- Force wrapping, regardless of self.wrap_results
+ """
+ if (force or self.wrap_results) and not isinstance(payload, Iq):
+ iq = self.xmpp.Iq()
+ # Since we're simulating a result, we have to treat
+ # the 'from' and 'to' values opposite the normal way.
+ iq['to'] = self.xmpp.boundjid if ito is None else ito
+ iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
+ iq['type'] = 'result'
+ iq.append(payload)
+ return iq
+ return payload
diff --git a/slixmpp/plugins/xep_0030/stanza/__init__.py b/slixmpp/plugins/xep_0030/stanza/__init__.py
new file mode 100644
index 00000000..bdac9bf2
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/stanza/__init__.py
@@ -0,0 +1,10 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo
+from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
diff --git a/slixmpp/plugins/xep_0030/stanza/info.py b/slixmpp/plugins/xep_0030/stanza/info.py
new file mode 100644
index 00000000..39ee83d5
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/stanza/info.py
@@ -0,0 +1,276 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class DiscoInfo(ElementBase):
+
+ """
+ XMPP allows for users and agents to find the identities and features
+ supported by other entities in the XMPP network through service discovery,
+ or "disco". In particular, the "disco#info" query type for <iq> stanzas is
+ used to request the list of identities and features offered by a JID.
+
+ An identity is a combination of a category and type, such as the 'client'
+ category with a type of 'pc' to indicate the agent is a human operated
+ client with a GUI, or a category of 'gateway' with a type of 'aim' to
+ identify the agent as a gateway for the legacy AIM protocol. See
+ <http://xmpp.org/registrar/disco-categories.html> for a full list of
+ accepted category and type combinations.
+
+ Features are simply a set of the namespaces that identify the supported
+ features. For example, a client that supports service discovery will
+ include the feature 'http://jabber.org/protocol/disco#info'.
+
+ Since clients and components may operate in several roles at once, identity
+ and feature information may be grouped into "nodes". If one were to write
+ all of the identities and features used by a client, then node names would
+ be like section headings.
+
+ Example disco#info stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" name="Slixmpp Bot" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ <feature var="jabber:x:data" />
+ <feature var="urn:xmpp:ping" />
+ </query>
+ </iq>
+
+ Stanza Interface:
+ node -- The name of the node to either
+ query or return info from.
+ identities -- A set of 4-tuples, where each tuple contains
+ the category, type, xml:lang, and name
+ of an identity.
+ features -- A set of namespaces for features.
+
+ Methods:
+ add_identity -- Add a new, single identity.
+ del_identity -- Remove a single identity.
+ get_identities -- Return all identities in tuple form.
+ set_identities -- Use multiple identities, each given in tuple form.
+ del_identities -- Remove all identities.
+ add_feature -- Add a single feature.
+ del_feature -- Remove a single feature.
+ get_features -- Return a list of all features.
+ set_features -- Use a given list of features.
+ del_features -- Remove all features.
+ """
+
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/disco#info'
+ plugin_attrib = 'disco_info'
+ interfaces = set(('node', 'features', 'identities'))
+ lang_interfaces = set(('identities',))
+
+ # Cache identities and features
+ _identities = set()
+ _features = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches identity and feature information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+
+ self._identities = set([id[0:3] for id in self['identities']])
+ self._features = self['features']
+
+ def add_identity(self, category, itype, name=None, lang=None):
+ """
+ Add a new identity element. Each identity must be unique
+ in terms of all four identity components.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the names
+ are different. In any case, a category and type are required.
+
+ Arguments:
+ category -- The general category to which the agent belongs.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional standard xml:lang value.
+ """
+ identity = (category, itype, lang)
+ if identity not in self._identities:
+ self._identities.add(identity)
+ id_xml = ET.Element('{%s}identity' % self.namespace)
+ id_xml.attrib['category'] = category
+ id_xml.attrib['type'] = itype
+ if lang:
+ id_xml.attrib['{%s}lang' % self.xml_ns] = lang
+ if name:
+ id_xml.attrib['name'] = name
+ self.xml.insert(0, id_xml)
+ return True
+ return False
+
+ def del_identity(self, category, itype, name=None, lang=None):
+ """
+ Remove a given identity.
+
+ Arguments:
+ category -- The general category to which the agent belonged.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional, standard xml:lang value.
+ """
+ identity = (category, itype, lang)
+ if identity in self._identities:
+ self._identities.remove(identity)
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None))
+ if id == identity:
+ self.xml.remove(id_xml)
+ return True
+ return False
+
+ def get_identities(self, lang=None, dedupe=True):
+ """
+ Return a set of all identities in tuple form as so:
+ (category, type, lang, name)
+
+ If a language was specified, only return identities using
+ that language.
+
+ Arguments:
+ lang -- Optional, standard xml:lang value.
+ dedupe -- If True, de-duplicate identities, otherwise
+ return a list of all identities.
+ """
+ if dedupe:
+ identities = set()
+ else:
+ identities = []
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
+ if lang is None or xml_lang == lang:
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
+ id_xml.attrib.get('name', None))
+ if dedupe:
+ identities.add(id)
+ else:
+ identities.append(id)
+ return identities
+
+ def set_identities(self, identities, lang=None):
+ """
+ Add or replace all identities. The identities must be a in set
+ where each identity is a tuple of the form:
+ (category, type, lang, name)
+
+ If a language is specifified, any identities using that language
+ will be removed to be replaced with the given identities.
+
+ NOTE: An identity's language will not be changed regardless of
+ the value of lang.
+
+ Arguments:
+ identities -- A set of identities in tuple form.
+ lang -- Optional, standard xml:lang value.
+ """
+ self.del_identities(lang)
+ for identity in identities:
+ category, itype, lang, name = identity
+ self.add_identity(category, itype, name, lang)
+
+ def del_identities(self, lang=None):
+ """
+ Remove all identities. If a language was specified, only
+ remove identities using that language.
+
+ Arguments:
+ lang -- Optional, standard xml:lang value.
+ """
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ if lang is None:
+ self.xml.remove(id_xml)
+ elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang:
+ self._identities.remove((
+ id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None)))
+ self.xml.remove(id_xml)
+
+ def add_feature(self, feature):
+ """
+ Add a single, new feature.
+
+ Arguments:
+ feature -- The namespace of the supported feature.
+ """
+ if feature not in self._features:
+ self._features.add(feature)
+ feature_xml = ET.Element('{%s}feature' % self.namespace)
+ feature_xml.attrib['var'] = feature
+ self.xml.append(feature_xml)
+ return True
+ return False
+
+ def del_feature(self, feature):
+ """
+ Remove a single feature.
+
+ Arguments:
+ feature -- The namespace of the removed feature.
+ """
+ if feature in self._features:
+ self._features.remove(feature)
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ if feature_xml.attrib['var'] == feature:
+ self.xml.remove(feature_xml)
+ return True
+ return False
+
+ def get_features(self, dedupe=True):
+ """Return the set of all supported features."""
+ if dedupe:
+ features = set()
+ else:
+ features = []
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ if dedupe:
+ features.add(feature_xml.attrib['var'])
+ else:
+ features.append(feature_xml.attrib['var'])
+ return features
+
+ def set_features(self, features):
+ """
+ Add or replace the set of supported features.
+
+ Arguments:
+ features -- The new set of supported features.
+ """
+ self.del_features()
+ for feature in features:
+ self.add_feature(feature)
+
+ def del_features(self):
+ """Remove all features."""
+ self._features = set()
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ self.xml.remove(feature_xml)
diff --git a/slixmpp/plugins/xep_0030/stanza/items.py b/slixmpp/plugins/xep_0030/stanza/items.py
new file mode 100644
index 00000000..0e238492
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/stanza/items.py
@@ -0,0 +1,152 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class DiscoItems(ElementBase):
+
+ """
+ Example disco#items stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="chat.example.com"
+ node="xmppdev"
+ name="XMPP Dev" />
+ <item jid="chat.example.com"
+ node="slixdev"
+ name="Slixmpp Dev" />
+ </query>
+ </iq>
+
+ Stanza Interface:
+ node -- The name of the node to either
+ query or return info from.
+ items -- A list of 3-tuples, where each tuple contains
+ the JID, node, and name of an item.
+
+ Methods:
+ add_item -- Add a single new item.
+ del_item -- Remove a single item.
+ get_items -- Return all items.
+ set_items -- Set or replace all items.
+ del_items -- Remove all items.
+ """
+
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/disco#items'
+ plugin_attrib = 'disco_items'
+ interfaces = set(('node', 'items'))
+
+ # Cache items
+ _items = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._items = set([item[0:2] for item in self['items']])
+
+ def add_item(self, jid, node=None, name=None):
+ """
+ Add a new item element. Each item is required to have a
+ JID, but may also specify a node value to reference
+ non-addressable entitities.
+
+ Arguments:
+ jid -- The JID for the item.
+ node -- Optional additional information to reference
+ non-addressable items.
+ name -- Optional human readable name for the item.
+ """
+ if (jid, node) not in self._items:
+ self._items.add((jid, node))
+ item = DiscoItem(parent=self)
+ item['jid'] = jid
+ item['node'] = node
+ item['name'] = name
+ self.iterables.append(item)
+ return True
+ return False
+
+ def del_item(self, jid, node=None):
+ """
+ Remove a single item.
+
+ Arguments:
+ jid -- JID of the item to remove.
+ node -- Optional extra identifying information.
+ """
+ if (jid, node) in self._items:
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ item = (item_xml.attrib['jid'],
+ item_xml.attrib.get('node', None))
+ if item == (jid, node):
+ self.xml.remove(item_xml)
+ return True
+ return False
+
+ def get_items(self):
+ """Return all items."""
+ items = set()
+ for item in self['substanzas']:
+ if isinstance(item, DiscoItem):
+ items.add((item['jid'], item['node'], item['name']))
+ return items
+
+ def set_items(self, items):
+ """
+ Set or replace all items. The given items must be in a
+ list or set where each item is a tuple of the form:
+ (jid, node, name)
+
+ Arguments:
+ items -- A series of items in tuple format.
+ """
+ self.del_items()
+ for item in items:
+ jid, node, name = item
+ self.add_item(jid, node, name)
+
+ def del_items(self):
+ """Remove all items."""
+ self._items = set()
+ items = [i for i in self.iterables if isinstance(i, DiscoItem)]
+ for item in items:
+ self.xml.remove(item.xml)
+ self.iterables.remove(item)
+
+
+class DiscoItem(ElementBase):
+ name = 'item'
+ namespace = 'http://jabber.org/protocol/disco#items'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'name'))
+
+ def get_node(self):
+ """Return the item's node name or ``None``."""
+ return self._get_attr('node', None)
+
+ def get_name(self):
+ """Return the item's human readable name, or ``None``."""
+ return self._get_attr('name', None)
+
+
+register_stanza_plugin(DiscoItems, DiscoItem, iterable=True)
diff --git a/slixmpp/plugins/xep_0030/static.py b/slixmpp/plugins/xep_0030/static.py
new file mode 100644
index 00000000..d9a7c80a
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/static.py
@@ -0,0 +1,430 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+
+from slixmpp import Iq
+from slixmpp.exceptions import XMPPError, IqError, IqTimeout
+from slixmpp.xmlstream import JID
+from slixmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticDisco(object):
+
+ """
+ While components will likely require fully dynamic handling
+ of service discovery information, most clients and simple bots
+ only need to manage a few disco nodes that will remain mostly
+ static.
+
+ StaticDisco provides a set of node handlers that will store
+ static sets of disco info and items in memory.
+
+ Attributes:
+ nodes -- A dictionary mapping (JID, node) tuples to a dict
+ containing a disco#info and a disco#items stanza.
+ xmpp -- The main Slixmpp object.
+ """
+
+ def __init__(self, xmpp, disco):
+ """
+ Create a static disco interface. Sets of disco#info and
+ disco#items are maintained for every given JID and node
+ combination. These stanzas are used to store disco
+ information in memory without any additional processing.
+
+ Arguments:
+ xmpp -- The main Slixmpp object.
+ """
+ self.nodes = {}
+ self.xmpp = xmpp
+ self.disco = disco
+ self.lock = threading.RLock()
+
+ def add_node(self, jid=None, node=None, ifrom=None):
+ """
+ Create a new set of stanzas for the provided
+ JID and node combination.
+
+ Arguments:
+ jid -- The JID that will own the new stanzas.
+ node -- The node that will own the new stanzas.
+ """
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
+ 'items': DiscoItems()}
+ self.nodes[(jid, node, ifrom)]['info']['node'] = node
+ self.nodes[(jid, node, ifrom)]['items']['node'] = node
+
+ def get_node(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.add_node(jid, node, ifrom)
+ return self.nodes[(jid, node, ifrom)]
+
+ def node_exists(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ return False
+ return True
+
+ # =================================================================
+ # Node Handlers
+ #
+ # Each handler accepts four arguments: jid, node, ifrom, and data.
+ # The jid and node parameters together determine the set of info
+ # and items stanzas that will be retrieved or added. Additionally,
+ # the ifrom value allows for cached results when results vary based
+ # on the requester's JID. The data parameter is a dictionary with
+ # additional parameters that will be passed to other calls.
+ #
+ # This implementation does not allow different responses based on
+ # the requester's JID, except for cached results. To do that,
+ # register a custom node handler.
+
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ features = info['disco_info']['features']
+ return feature in features
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ trunc = lambda i: (i[0], i[1], i[2])
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def get_info(self, jid, node, ifrom, data):
+ """
+ Return the stored info data for the requested JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
+ else:
+ return self.get_node(jid, node)['info']
+
+ def set_info(self, jid, node, ifrom, data):
+ """
+ Set the entire info stanza for a JID/node at once.
+
+ The data parameter is a disco#info substanza.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'] = data
+
+ def del_info(self, jid, node, ifrom, data):
+ """
+ Reset the info stanza for a given JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'] = DiscoInfo()
+
+ def get_items(self, jid, node, ifrom, data):
+ """
+ Return the stored items data for the requested JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoItems()
+ else:
+ raise XMPPError(condition='item-not-found')
+ else:
+ return self.get_node(jid, node)['items']
+
+ def set_items(self, jid, node, ifrom, data):
+ """
+ Replace the stored items data for a JID/node combination.
+
+ The data parameter may provide:
+ items -- A set of items in tuple format.
+ """
+ with self.lock:
+ items = data.get('items', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items']['items'] = items
+
+ def del_items(self, jid, node, ifrom, data):
+ """
+ Reset the items stanza for a given JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'] = DiscoItems()
+
+ def add_identity(self, jid, node, ifrom, data):
+ """
+ Add a new identity to te JID/node combination.
+
+ The data parameter may provide:
+ category -- The general category to which the agent belongs.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional standard xml:lang value.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
+
+ def set_identities(self, jid, node, ifrom, data):
+ """
+ Add or replace all identities for a JID/node combination.
+
+ The data parameter should include:
+ identities -- A list of identities in tuple form:
+ (category, type, name, lang)
+ """
+ with self.lock:
+ identities = data.get('identities', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['identities'] = identities
+
+ def del_identity(self, jid, node, ifrom, data):
+ """
+ Remove an identity from a JID/node combination.
+
+ The data parameter may provide:
+ category -- The general category to which the agent belonged.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional, standard xml:lang value.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
+
+ def del_identities(self, jid, node, ifrom, data):
+ """
+ Remove all identities from a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ del self.get_node(jid, node)['info']['identities']
+
+ def add_feature(self, jid, node, ifrom, data):
+ """
+ Add a feature to a JID/node combination.
+
+ The data parameter should include:
+ feature -- The namespace of the supported feature.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_feature(
+ data.get('feature', ''))
+
+ def set_features(self, jid, node, ifrom, data):
+ """
+ Add or replace all features for a JID/node combination.
+
+ The data parameter should include:
+ features -- The new set of supported features.
+ """
+ with self.lock:
+ features = data.get('features', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['features'] = features
+
+ def del_feature(self, jid, node, ifrom, data):
+ """
+ Remove a feature from a JID/node combination.
+
+ The data parameter should include:
+ feature -- The namespace of the removed feature.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_feature(
+ data.get('feature', ''))
+
+ def del_features(self, jid, node, ifrom, data):
+ """
+ Remove all features from a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ return
+ del self.get_node(jid, node)['info']['features']
+
+ def add_item(self, jid, node, ifrom, data):
+ """
+ Add an item to a JID/node combination.
+
+ The data parameter may include:
+ ijid -- The JID for the item.
+ inode -- Optional additional information to reference
+ non-addressable items.
+ name -- Optional human readable name for the item.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items'].add_item(
+ data.get('ijid', ''),
+ node=data.get('inode', ''),
+ name=data.get('name', ''))
+
+ def del_item(self, jid, node, ifrom, data):
+ """
+ Remove an item from a JID/node combination.
+
+ The data parameter may include:
+ ijid -- JID of the item to remove.
+ inode -- Optional extra identifying information.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'].del_item(
+ data.get('ijid', ''),
+ node=data.get('inode', None))
+
+ def cache_info(self, jid, node, ifrom, data):
+ """
+ Cache disco information for an external JID.
+
+ The data parameter is the Iq result stanza
+ containing the disco info to cache, or
+ the disco#info substanza itself.
+ """
+ with self.lock:
+ if isinstance(data, Iq):
+ data = data['disco_info']
+
+ self.add_node(jid, node, ifrom)
+ self.get_node(jid, node, ifrom)['info'] = data
+
+ def get_cached_info(self, jid, node, ifrom, data):
+ """
+ Retrieve cached disco info data.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node, ifrom):
+ return None
+ else:
+ return self.get_node(jid, node, ifrom)['info']
diff --git a/slixmpp/plugins/xep_0033/__init__.py b/slixmpp/plugins/xep_0033/__init__.py
new file mode 100644
index 00000000..e6953ce7
--- /dev/null
+++ b/slixmpp/plugins/xep_0033/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0033 import stanza
+from slixmpp.plugins.xep_0033.stanza import Addresses, Address
+from slixmpp.plugins.xep_0033.addresses import XEP_0033
+
+
+register_plugin(XEP_0033)
+
+# Retain some backwards compatibility
+xep_0033 = XEP_0033
+Addresses.addAddress = Addresses.add_address
diff --git a/slixmpp/plugins/xep_0033/addresses.py b/slixmpp/plugins/xep_0033/addresses.py
new file mode 100644
index 00000000..7b3c2d17
--- /dev/null
+++ b/slixmpp/plugins/xep_0033/addresses.py
@@ -0,0 +1,37 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Message, Presence
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0033 import stanza, Addresses
+
+
+class XEP_0033(BasePlugin):
+
+ """
+ XEP-0033: Extended Stanza Addressing
+ """
+
+ name = 'xep_0033'
+ description = 'XEP-0033: Extended Stanza Addressing'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, Addresses)
+ register_stanza_plugin(Presence, Addresses)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Addresses.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Addresses.namespace)
+
diff --git a/slixmpp/plugins/xep_0033/stanza.py b/slixmpp/plugins/xep_0033/stanza.py
new file mode 100644
index 00000000..3f89f373
--- /dev/null
+++ b/slixmpp/plugins/xep_0033/stanza.py
@@ -0,0 +1,131 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import JID, ElementBase, ET, register_stanza_plugin
+
+
+class Addresses(ElementBase):
+
+ name = 'addresses'
+ namespace = 'http://jabber.org/protocol/address'
+ plugin_attrib = 'addresses'
+ interfaces = set()
+
+ def add_address(self, atype='to', jid='', node='', uri='',
+ desc='', delivered=False):
+ addr = Address(parent=self)
+ addr['type'] = atype
+ addr['jid'] = jid
+ addr['node'] = node
+ addr['uri'] = uri
+ addr['desc'] = desc
+ addr['delivered'] = delivered
+
+ return addr
+
+ # Additional methods for manipulating sets of addresses
+ # based on type are generated below.
+
+
+class Address(ElementBase):
+
+ name = 'address'
+ namespace = 'http://jabber.org/protocol/address'
+ plugin_attrib = 'address'
+ interfaces = set(['type', 'jid', 'node', 'uri', 'desc', 'delivered'])
+
+ address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_delivered(self):
+ value = self._get_attr('delivered', False)
+ return value and value.lower() in ('true', '1')
+
+ def set_delivered(self, delivered):
+ if delivered:
+ self._set_attr('delivered', 'true')
+ else:
+ del self['delivered']
+
+ def set_uri(self, uri):
+ if uri:
+ del self['jid']
+ del self['node']
+ self._set_attr('uri', uri)
+ else:
+ self._del_attr('uri')
+
+
+# =====================================================================
+# Auto-generate address type filters for the Addresses class.
+
+def _addr_filter(atype):
+ def _type_filter(addr):
+ if isinstance(addr, Address):
+ if atype == 'all' or addr['type'] == atype:
+ return True
+ return False
+ return _type_filter
+
+
+def _build_methods(atype):
+
+ def get_multi(self):
+ return list(filter(_addr_filter(atype), self))
+
+ def set_multi(self, value):
+ del self[atype]
+ for addr in value:
+
+ # Support assigning dictionary versions of addresses
+ # instead of full Address objects.
+ if not isinstance(addr, Address):
+ if atype != 'all':
+ addr['type'] = atype
+ elif 'atype' in addr and 'type' not in addr:
+ addr['type'] = addr['atype']
+ addrObj = Address()
+ addrObj.values = addr
+ addr = addrObj
+
+ self.append(addr)
+
+ def del_multi(self):
+ res = list(filter(_addr_filter(atype), self))
+ for addr in res:
+ self.iterables.remove(addr)
+ self.xml.remove(addr.xml)
+
+ return get_multi, set_multi, del_multi
+
+
+for atype in ('all', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'):
+ get_multi, set_multi, del_multi = _build_methods(atype)
+
+ Addresses.interfaces.add(atype)
+ setattr(Addresses, "get_%s" % atype, get_multi)
+ setattr(Addresses, "set_%s" % atype, set_multi)
+ setattr(Addresses, "del_%s" % atype, del_multi)
+
+ # To retain backwards compatibility:
+ setattr(Addresses, "get%s" % atype.title(), get_multi)
+ setattr(Addresses, "set%s" % atype.title(), set_multi)
+ setattr(Addresses, "del%s" % atype.title(), del_multi)
+ if atype == 'all':
+ Addresses.interfaces.add('addresses')
+ setattr(Addresses, "getAddresses", get_multi)
+ setattr(Addresses, "setAddresses", set_multi)
+ setattr(Addresses, "delAddresses", del_multi)
+
+
+register_stanza_plugin(Addresses, Address, iterable=True)
diff --git a/slixmpp/plugins/xep_0045.py b/slixmpp/plugins/xep_0045.py
new file mode 100644
index 00000000..f6f48891
--- /dev/null
+++ b/slixmpp/plugins/xep_0045.py
@@ -0,0 +1,418 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+from __future__ import with_statement
+
+import logging
+
+from slixmpp import Presence
+from slixmpp.plugins import BasePlugin, register_plugin
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
+from slixmpp.xmlstream.handler.callback import Callback
+from slixmpp.xmlstream.matcher.xpath import MatchXPath
+from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
+from slixmpp.exceptions import IqError, IqTimeout
+
+
+log = logging.getLogger(__name__)
+
+
+class MUCPresence(ElementBase):
+ name = 'x'
+ namespace = 'http://jabber.org/protocol/muc#user'
+ plugin_attrib = 'muc'
+ interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
+ affiliations = set(('', ))
+ roles = set(('', ))
+
+ def getXMLItem(self):
+ item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
+ if item is None:
+ item = ET.Element('{http://jabber.org/protocol/muc#user}item')
+ self.xml.append(item)
+ return item
+
+ def getAffiliation(self):
+ #TODO if no affilation, set it to the default and return default
+ item = self.getXMLItem()
+ return item.get('affiliation', '')
+
+ def setAffiliation(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid affiliation
+ item.attrib['affiliation'] = value
+ return self
+
+ def delAffiliation(self):
+ item = self.getXMLItem()
+ #TODO set default affiliation
+ if 'affiliation' in item.attrib: del item.attrib['affiliation']
+ return self
+
+ def getJid(self):
+ item = self.getXMLItem()
+ return JID(item.get('jid', ''))
+
+ def setJid(self, value):
+ item = self.getXMLItem()
+ if not isinstance(value, str):
+ value = str(value)
+ item.attrib['jid'] = value
+ return self
+
+ def delJid(self):
+ item = self.getXMLItem()
+ if 'jid' in item.attrib: del item.attrib['jid']
+ return self
+
+ def getRole(self):
+ item = self.getXMLItem()
+ #TODO get default role, set default role if none
+ return item.get('role', '')
+
+ def setRole(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid role
+ item.attrib['role'] = value
+ return self
+
+ def delRole(self):
+ item = self.getXMLItem()
+ #TODO set default role
+ if 'role' in item.attrib: del item.attrib['role']
+ return self
+
+ def getNick(self):
+ return self.parent()['from'].resource
+
+ def getRoom(self):
+ return self.parent()['from'].bare
+
+ def setNick(self, value):
+ log.warning("Cannot set nick through mucpresence plugin.")
+ return self
+
+ def setRoom(self, value):
+ log.warning("Cannot set room through mucpresence plugin.")
+ return self
+
+ def delNick(self):
+ log.warning("Cannot delete nick through mucpresence plugin.")
+ return self
+
+ def delRoom(self):
+ log.warning("Cannot delete room through mucpresence plugin.")
+ return self
+
+
+class XEP_0045(BasePlugin):
+
+ """
+ Implements XEP-0045 Multi-User Chat
+ """
+
+ name = 'xep_0045'
+ description = 'XEP-0045: Multi-User Chat'
+ dependencies = set(['xep_0030', 'xep_0004'])
+
+ def plugin_init(self):
+ self.rooms = {}
+ self.ourNicks = {}
+ self.xep = '0045'
+ # load MUC support in presence stanzas
+ register_stanza_plugin(Presence, MUCPresence)
+ self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
+ self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message))
+ self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
+ self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
+ self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
+ self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
+ self.xmpp.default_ns,
+ 'http://jabber.org/protocol/muc#user',
+ 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
+
+ def plugin_end(self):
+ self.xmpp.plugin['xep_0030'].del_feature(feature='http://jabber.org/protocol/muc')
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/muc')
+
+ def handle_groupchat_invite(self, inv):
+ """ Handle an invite into a muc.
+ """
+ logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
+ if inv['from'] not in self.rooms.keys():
+ self.xmpp.event("groupchat_invite", inv)
+
+ def handle_config_change(self, msg):
+ """Handle a MUC configuration change (with status code)."""
+ self.xmpp.event('groupchat_config_status', msg)
+ self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
+
+ def handle_groupchat_presence(self, pr):
+ """ Handle a presence in a muc.
+ """
+ got_offline = False
+ got_online = False
+ if pr['muc']['room'] not in self.rooms.keys():
+ return
+ entry = pr['muc'].get_stanza_values()
+ entry['show'] = pr['show']
+ entry['status'] = pr['status']
+ entry['alt_nick'] = pr['nick']
+ if pr['type'] == 'unavailable':
+ if entry['nick'] in self.rooms[entry['room']]:
+ del self.rooms[entry['room']][entry['nick']]
+ got_offline = True
+ else:
+ if entry['nick'] not in self.rooms[entry['room']]:
+ got_online = True
+ self.rooms[entry['room']][entry['nick']] = entry
+ log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
+ self.xmpp.event("groupchat_presence", pr)
+ self.xmpp.event("muc::%s::presence" % entry['room'], pr)
+ if got_offline:
+ self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
+ if got_online:
+ self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
+
+ def handle_groupchat_message(self, msg):
+ """ Handle a message event in a muc.
+ """
+ self.xmpp.event('groupchat_message', msg)
+ self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
+
+ def handle_groupchat_error_message(self, msg):
+ """ Handle a message error event in a muc.
+ """
+ self.xmpp.event('groupchat_message_error', msg)
+ self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
+
+
+
+ def handle_groupchat_subject(self, msg):
+ """ Handle a message coming from a muc indicating
+ a change of subject (or announcing it when joining the room)
+ """
+ self.xmpp.event('groupchat_subject', msg)
+
+ def jidInRoom(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return True
+ return False
+
+ def getNick(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return nick
+
+ def configureRoom(self, room, form=None, ifrom=None):
+ if form is None:
+ form = self.getRoomConfig(room, ifrom=ifrom)
+ iq = self.xmpp.make_iq_set()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ form = form.getXML('submit')
+ query.append(form)
+ iq.append(query)
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ return True
+
+ def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
+ """ Join the specified room, requesting 'maxhistory' lines of history.
+ """
+ stanza = self.xmpp.make_presence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
+ x = ET.Element('{http://jabber.org/protocol/muc}x')
+ if password:
+ passelement = ET.Element('{http://jabber.org/protocol/muc}password')
+ passelement.text = password
+ x.append(passelement)
+ if maxhistory:
+ history = ET.Element('{http://jabber.org/protocol/muc}history')
+ if maxhistory == "0":
+ history.attrib['maxchars'] = maxhistory
+ else:
+ history.attrib['maxstanzas'] = maxhistory
+ x.append(history)
+ stanza.append(x)
+ if not wait:
+ self.xmpp.send(stanza)
+ else:
+ #wait for our own room presence back
+ expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
+ self.xmpp.send(stanza, expect)
+ self.rooms[room] = {}
+ self.ourNicks[room] = nick
+
+ def destroy(self, room, reason='', altroom = '', ifrom=None):
+ iq = self.xmpp.make_iq_set()
+ if ifrom is not None:
+ iq['from'] = ifrom
+ iq['to'] = room
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
+ if altroom:
+ destroy.attrib['jid'] = altroom
+ xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
+ xreason.text = reason
+ destroy.append(xreason)
+ query.append(destroy)
+ iq.append(query)
+ # For now, swallow errors to preserve existing API
+ try:
+ r = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ return True
+
+ def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
+ """ Change room affiliation."""
+ if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
+ raise TypeError
+ query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
+ if nick is not None:
+ item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
+ else:
+ item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
+ query.append(item)
+ iq = self.xmpp.make_iq_set(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ return True
+
+ def setRole(self, room, nick, role):
+ """ Change role property of a nick in a room.
+ Typically, roles are temporary (they last only as long as you are in the
+ room), whereas affiliations are permanent (they last across groupchat
+ sessions).
+ """
+ if role not in ('moderator', 'participant', 'visitor', 'none'):
+ raise TypeError
+ query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
+ item = ET.Element('item', {'role':role, 'nick':nick})
+ query.append(item)
+ iq = self.xmpp.make_iq_set(query)
+ iq['to'] = room
+ result = iq.send()
+ if result is False or result['type'] != 'result':
+ raise ValueError
+ return True
+
+ def invite(self, room, jid, reason='', mfrom=''):
+ """ Invite a jid to a room."""
+ msg = self.xmpp.make_message(room)
+ msg['from'] = mfrom
+ x = ET.Element('{http://jabber.org/protocol/muc#user}x')
+ invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
+ if reason:
+ rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
+ rxml.text = reason
+ invite.append(rxml)
+ x.append(invite)
+ msg.append(x)
+ self.xmpp.send(msg)
+
+ def leaveMUC(self, room, nick, msg='', pfrom=None):
+ """ Leave the specified room.
+ """
+ if msg:
+ self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
+ else:
+ self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
+ del self.rooms[room]
+
+ def getRoomConfig(self, room, ifrom=''):
+ iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner')
+ iq['to'] = room
+ iq['from'] = ifrom
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ raise ValueError
+ except IqTimeout:
+ raise ValueError
+ form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if form is None:
+ raise ValueError
+ return self.xmpp.plugin['xep_0004'].buildForm(form)
+
+ def cancelConfig(self, room, ifrom=None):
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ x = ET.Element('{jabber:x:data}x', type='cancel')
+ query.append(x)
+ iq = self.xmpp.make_iq_set(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ iq.send()
+
+ def setRoomConfig(self, room, config, ifrom=''):
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ x = config.getXML('submit')
+ query.append(x)
+ iq = self.xmpp.make_iq_set(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ iq.send()
+
+ def getJoinedRooms(self):
+ return self.rooms.keys()
+
+ def getOurJidInRoom(self, roomJid):
+ """ Return the jid we're using in a room.
+ """
+ return "%s/%s" % (roomJid, self.ourNicks[roomJid])
+
+ def getJidProperty(self, room, nick, jidProperty):
+ """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
+ If not found, return None.
+ """
+ if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
+ return self.rooms[room][nick][jidProperty]
+ else:
+ return None
+
+ def getRoster(self, room):
+ """ Get the list of nicks in a room.
+ """
+ if room not in self.rooms.keys():
+ return None
+ return self.rooms[room].keys()
+
+ def getUsersByAffiliation(cls, room, affiliation='member', ifrom=None):
+ if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
+ raise TypeError
+ query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
+ item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation})
+ query.append(item)
+ iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get')
+ iq.append(query)
+ return iq.send()
+
+
+xep_0045 = XEP_0045
+register_plugin(XEP_0045)
diff --git a/slixmpp/plugins/xep_0047/__init__.py b/slixmpp/plugins/xep_0047/__init__.py
new file mode 100644
index 00000000..5bb9e7cc
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/__init__.py
@@ -0,0 +1,21 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0047 import stanza
+from slixmpp.plugins.xep_0047.stanza import Open, Close, Data
+from slixmpp.plugins.xep_0047.stream import IBBytestream
+from slixmpp.plugins.xep_0047.ibb import XEP_0047
+
+
+register_plugin(XEP_0047)
+
+
+# Retain some backwards compatibility
+xep_0047 = XEP_0047
diff --git a/slixmpp/plugins/xep_0047/ibb.py b/slixmpp/plugins/xep_0047/ibb.py
new file mode 100644
index 00000000..52d7fbe5
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/ibb.py
@@ -0,0 +1,183 @@
+import asyncio
+import uuid
+import logging
+
+from slixmpp import Message, Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0047(BasePlugin):
+
+ name = 'xep_0047'
+ description = 'XEP-0047: In-band Bytestreams'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ default_config = {
+ 'block_size': 4096,
+ 'max_block_size': 8192,
+ 'auto_accept': False,
+ }
+
+ def plugin_init(self):
+ self._streams = {}
+ self._preauthed_sids = {}
+
+ register_stanza_plugin(Iq, Open)
+ register_stanza_plugin(Iq, Close)
+ register_stanza_plugin(Iq, Data)
+ register_stanza_plugin(Message, Data)
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Open',
+ StanzaPath('iq@type=set/ibb_open'),
+ self._handle_open_request))
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Close',
+ StanzaPath('iq@type=set/ibb_close'),
+ self._handle_close))
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Data',
+ StanzaPath('iq@type=set/ibb_data'),
+ self._handle_data))
+
+ self.xmpp.register_handler(Callback(
+ 'IBB Message Data',
+ StanzaPath('message/ibb_data'),
+ self._handle_data))
+
+ self.api.register(self._authorized, 'authorized', default=True)
+ self.api.register(self._authorized_sid, 'authorized_sid', default=True)
+ self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True)
+ self.api.register(self._get_stream, 'get_stream', default=True)
+ self.api.register(self._set_stream, 'set_stream', default=True)
+ self.api.register(self._del_stream, 'del_stream', default=True)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('IBB Open')
+ self.xmpp.remove_handler('IBB Close')
+ self.xmpp.remove_handler('IBB Data')
+ self.xmpp.remove_handler('IBB Message Data')
+ self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb')
+
+ def _get_stream(self, jid, sid, peer_jid, data):
+ return self._streams.get((jid, sid, peer_jid), None)
+
+ def _set_stream(self, jid, sid, peer_jid, stream):
+ self._streams[(jid, sid, peer_jid)] = stream
+
+ def _del_stream(self, jid, sid, peer_jid, data):
+ if (jid, sid, peer_jid) in self._streams:
+ del self._streams[(jid, sid, peer_jid)]
+
+ def _accept_stream(self, iq):
+ receiver = iq['to']
+ sender = iq['from']
+ sid = iq['ibb_open']['sid']
+
+ if self.api['authorized_sid'](receiver, sid, sender, iq):
+ return True
+ return self.api['authorized'](receiver, sid, sender, iq)
+
+ def _authorized(self, jid, sid, ifrom, iq):
+ if self.auto_accept:
+ return True
+ return False
+
+ def _authorized_sid(self, jid, sid, ifrom, iq):
+ if (jid, sid, ifrom) in self._preauthed_sids:
+ del self._preauthed_sids[(jid, sid, ifrom)]
+ return True
+ return False
+
+ def _preauthorize_sid(self, jid, sid, ifrom, data):
+ self._preauthed_sids[(jid, sid, ifrom)] = True
+
+ def open_stream(self, jid, block_size=None, sid=None, use_messages=False,
+ ifrom=None, timeout=None, callback=None):
+ if sid is None:
+ sid = str(uuid.uuid4())
+ if block_size is None:
+ block_size = self.block_size
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['ibb_open']['block_size'] = block_size
+ iq['ibb_open']['sid'] = sid
+ iq['ibb_open']['stanza'] = 'message' if use_messages else 'iq'
+
+ stream = IBBytestream(self.xmpp, sid, block_size,
+ iq['from'], iq['to'], use_messages)
+
+ stream_future = asyncio.Future()
+
+ def _handle_opened_stream(iq):
+ log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from'])
+ stream.self_jid = iq['to']
+ stream.peer_jid = iq['from']
+ stream.stream_started = True
+ self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
+ stream_future.set_result(stream)
+ if callback is not None:
+ callback(stream)
+ self.xmpp.event('ibb_stream_start', stream)
+ self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream)
+
+ iq.send(timeout=timeout, callback=_handle_opened_stream)
+
+ return stream_future
+
+ def _handle_open_request(self, iq):
+ sid = iq['ibb_open']['sid']
+ size = iq['ibb_open']['block_size'] or self.block_size
+
+ log.debug('Received IBB stream request from %s', iq['from'])
+
+ if not sid:
+ raise XMPPError(etype='modify', condition='bad-request')
+
+ if not self._accept_stream(iq):
+ raise XMPPError(etype='cancel', condition='not-acceptable')
+
+ if size > self.max_block_size:
+ raise XMPPError('resource-constraint')
+
+ stream = IBBytestream(self.xmpp, sid, size,
+ iq['to'], iq['from'])
+ stream.stream_started = True
+ self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
+ iq.reply().send()
+
+ self.xmpp.event('ibb_stream_start', stream)
+ self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream)
+
+ def _handle_data(self, stanza):
+ sid = stanza['ibb_data']['sid']
+ stream = self.api['get_stream'](stanza['to'], sid, stanza['from'])
+ if stream is not None and stanza['from'] == stream.peer_jid:
+ stream._recv_data(stanza)
+ else:
+ raise XMPPError('item-not-found')
+
+ def _handle_close(self, iq):
+ sid = iq['ibb_close']['sid']
+ stream = self.api['get_stream'](iq['to'], sid, iq['from'])
+ if stream is not None and iq['from'] == stream.peer_jid:
+ stream._closed(iq)
+ self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid)
+ else:
+ raise XMPPError('item-not-found')
diff --git a/slixmpp/plugins/xep_0047/stanza.py b/slixmpp/plugins/xep_0047/stanza.py
new file mode 100644
index 00000000..7f8ff0ba
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/stanza.py
@@ -0,0 +1,70 @@
+import re
+import base64
+
+from slixmpp.util import bytes
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream import ElementBase
+
+
+VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
+
+
+def to_b64(data):
+ return bytes(base64.b64encode(bytes(data))).decode('utf-8')
+
+
+def from_b64(data):
+ return bytes(base64.b64decode(bytes(data)))
+
+
+class Open(ElementBase):
+ name = 'open'
+ namespace = 'http://jabber.org/protocol/ibb'
+ plugin_attrib = 'ibb_open'
+ interfaces = set(('block_size', 'sid', 'stanza'))
+
+ def get_block_size(self):
+ return int(self._get_attr('block-size', '0'))
+
+ def set_block_size(self, value):
+ self._set_attr('block-size', str(value))
+
+ def del_block_size(self):
+ self._del_attr('block-size')
+
+
+class Data(ElementBase):
+ name = 'data'
+ namespace = 'http://jabber.org/protocol/ibb'
+ plugin_attrib = 'ibb_data'
+ interfaces = set(('seq', 'sid', 'data'))
+ sub_interfaces = set(['data'])
+
+ def get_seq(self):
+ return int(self._get_attr('seq', '0'))
+
+ def set_seq(self, value):
+ self._set_attr('seq', str(value))
+
+ def get_data(self):
+ text = self.xml.text
+ if not text:
+ raise XMPPError('not-acceptable', 'IBB data element is empty.')
+ b64_data = text.strip()
+ if VALID_B64.match(b64_data).group() == b64_data:
+ return from_b64(b64_data)
+ else:
+ raise XMPPError('not-acceptable')
+
+ def set_data(self, value):
+ self.xml.text = to_b64(value)
+
+ def del_data(self):
+ self.xml.text = ''
+
+
+class Close(ElementBase):
+ name = 'close'
+ namespace = 'http://jabber.org/protocol/ibb'
+ plugin_attrib = 'ibb_close'
+ interfaces = set(['sid'])
diff --git a/slixmpp/plugins/xep_0047/stream.py b/slixmpp/plugins/xep_0047/stream.py
new file mode 100644
index 00000000..3be894eb
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/stream.py
@@ -0,0 +1,128 @@
+import asyncio
+import socket
+import logging
+
+from slixmpp.stanza import Iq
+from slixmpp.exceptions import XMPPError
+
+
+log = logging.getLogger(__name__)
+
+
+class IBBytestream(object):
+
+ def __init__(self, xmpp, sid, block_size, jid, peer, use_messages=False):
+ self.xmpp = xmpp
+ self.sid = sid
+ self.block_size = block_size
+ self.use_messages = use_messages
+
+ if jid is None:
+ jid = xmpp.boundjid
+ self.self_jid = jid
+ self.peer_jid = peer
+
+ self.send_seq = -1
+ self.recv_seq = -1
+
+ self.stream_started = False
+ self.stream_in_closed = False
+ self.stream_out_closed = False
+
+ self.recv_queue = asyncio.Queue()
+
+ @asyncio.coroutine
+ def send(self, data, timeout=None):
+ if not self.stream_started or self.stream_out_closed:
+ raise socket.error
+ if len(data) > self.block_size:
+ data = data[:self.block_size]
+ self.send_seq = (self.send_seq + 1) % 65535
+ seq = self.send_seq
+ if self.use_messages:
+ msg = self.xmpp.Message()
+ msg['to'] = self.peer_jid
+ msg['from'] = self.self_jid
+ msg['id'] = self.xmpp.new_id()
+ msg['ibb_data']['sid'] = self.sid
+ msg['ibb_data']['seq'] = seq
+ msg['ibb_data']['data'] = data
+ msg.send()
+ else:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = self.peer_jid
+ iq['from'] = self.self_jid
+ iq['ibb_data']['sid'] = self.sid
+ iq['ibb_data']['seq'] = seq
+ iq['ibb_data']['data'] = data
+ yield from iq.send(timeout=timeout)
+ return len(data)
+
+ @asyncio.coroutine
+ def sendall(self, data, timeout=None):
+ sent_len = 0
+ while sent_len < len(data):
+ sent_len += yield from self.send(data[sent_len:self.block_size], timeout=timeout)
+
+ @asyncio.coroutine
+ def sendfile(self, file, timeout=None):
+ while True:
+ data = file.read(self.block_size)
+ if not data:
+ break
+ yield from self.send(data, timeout=timeout)
+
+ def _recv_data(self, stanza):
+ new_seq = stanza['ibb_data']['seq']
+ if new_seq != (self.recv_seq + 1) % 65535:
+ self.close()
+ raise XMPPError('unexpected-request')
+ self.recv_seq = new_seq
+
+ data = stanza['ibb_data']['data']
+ if len(data) > self.block_size:
+ self.close()
+ raise XMPPError('not-acceptable')
+
+ self.recv_queue.put_nowait(data)
+ self.xmpp.event('ibb_stream_data', self)
+
+ if isinstance(stanza, Iq):
+ stanza.reply().send()
+
+ def recv(self, *args, **kwargs):
+ return self.read()
+
+ def read(self):
+ if not self.stream_started or self.stream_in_closed:
+ raise socket.error
+ return self.recv_queue.get_nowait()
+
+ def close(self, timeout=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = self.peer_jid
+ iq['from'] = self.self_jid
+ iq['ibb_close']['sid'] = self.sid
+ self.stream_out_closed = True
+ def _close_stream(_):
+ self.stream_in_closed = True
+ future = iq.send(timeout=timeout, callback=_close_stream)
+ self.xmpp.event('ibb_stream_end', self)
+ return future
+
+ def _closed(self, iq):
+ self.stream_in_closed = True
+ self.stream_out_closed = True
+ iq.reply().send()
+ self.xmpp.event('ibb_stream_end', self)
+
+ def makefile(self, *args, **kwargs):
+ return self
+
+ def connect(*args, **kwargs):
+ return None
+
+ def shutdown(self, *args, **kwargs):
+ return None
diff --git a/slixmpp/plugins/xep_0048/__init__.py b/slixmpp/plugins/xep_0048/__init__.py
new file mode 100644
index 00000000..d19f12a3
--- /dev/null
+++ b/slixmpp/plugins/xep_0048/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0048.stanza import Bookmarks, Conference, URL
+from slixmpp.plugins.xep_0048.bookmarks import XEP_0048
+
+
+register_plugin(XEP_0048)
diff --git a/slixmpp/plugins/xep_0048/bookmarks.py b/slixmpp/plugins/xep_0048/bookmarks.py
new file mode 100644
index 00000000..fa76df39
--- /dev/null
+++ b/slixmpp/plugins/xep_0048/bookmarks.py
@@ -0,0 +1,76 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0048 import stanza, Bookmarks, Conference, URL
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0048(BasePlugin):
+
+ name = 'xep_0048'
+ description = 'XEP-0048: Bookmarks'
+ dependencies = set(['xep_0045', 'xep_0049', 'xep_0060', 'xep_0163', 'xep_0223'])
+ stanza = stanza
+ default_config = {
+ 'auto_join': False,
+ 'storage_method': 'xep_0049'
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, Bookmarks)
+
+ self.xmpp['xep_0049'].register(Bookmarks)
+ self.xmpp['xep_0163'].register_pep('bookmarks', Bookmarks)
+
+ self.xmpp.add_event_handler('session_start', self._autojoin)
+
+ def plugin_end(self):
+ self.xmpp.del_event_handler('session_start', self._autojoin)
+
+ def _autojoin(self, __):
+ if not self.auto_join:
+ return
+
+ try:
+ result = self.get_bookmarks(method=self.storage_method)
+ except XMPPError:
+ return
+
+ if self.storage_method == 'xep_0223':
+ bookmarks = result['pubsub']['items']['item']['bookmarks']
+ else:
+ bookmarks = result['private']['bookmarks']
+
+ for conf in bookmarks['conferences']:
+ if conf['autojoin']:
+ log.debug('Auto joining %s as %s', conf['jid'], conf['nick'])
+ self.xmpp['xep_0045'].joinMUC(conf['jid'], conf['nick'],
+ password=conf['password'])
+
+ def set_bookmarks(self, bookmarks, method=None, **iqargs):
+ if not method:
+ method = self.storage_method
+ return self.xmpp[method].store(bookmarks, **iqargs)
+
+ def get_bookmarks(self, method=None, **iqargs):
+ if not method:
+ method = self.storage_method
+
+ loc = 'storage:bookmarks' if method == 'xep_0223' else 'bookmarks'
+
+ return self.xmpp[method].retrieve(loc, **iqargs)
diff --git a/slixmpp/plugins/xep_0048/stanza.py b/slixmpp/plugins/xep_0048/stanza.py
new file mode 100644
index 00000000..c158535a
--- /dev/null
+++ b/slixmpp/plugins/xep_0048/stanza.py
@@ -0,0 +1,65 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Bookmarks(ElementBase):
+ name = 'storage'
+ namespace = 'storage:bookmarks'
+ plugin_attrib = 'bookmarks'
+ interfaces = set()
+
+ def add_conference(self, jid, nick, name=None, autojoin=None, password=None):
+ conf = Conference()
+ conf['jid'] = jid
+ conf['nick'] = nick
+ if name is None:
+ name = jid
+ conf['name'] = name
+ conf['autojoin'] = autojoin
+ conf['password'] = password
+ self.append(conf)
+
+ def add_url(self, url, name=None):
+ saved_url = URL()
+ saved_url['url'] = url
+ if name is None:
+ name = url
+ saved_url['name'] = name
+ self.append(saved_url)
+
+
+class Conference(ElementBase):
+ name = 'conference'
+ namespace = 'storage:bookmarks'
+ plugin_attrib = 'conference'
+ plugin_multi_attrib = 'conferences'
+ interfaces = set(['nick', 'password', 'autojoin', 'jid', 'name'])
+ sub_interfaces = set(['nick', 'password'])
+
+ def get_autojoin(self):
+ value = self._get_attr('autojoin')
+ return value in ('1', 'true')
+
+ def set_autojoin(self, value):
+ del self['autojoin']
+ if value in ('1', 'true', True):
+ self._set_attr('autojoin', 'true')
+
+
+class URL(ElementBase):
+ name = 'url'
+ namespace = 'storage:bookmarks'
+ plugin_attrib = 'url'
+ plugin_multi_attrib = 'urls'
+ interfaces = set(['url', 'name'])
+
+
+register_stanza_plugin(Bookmarks, Conference, iterable=True)
+register_stanza_plugin(Bookmarks, URL, iterable=True)
diff --git a/slixmpp/plugins/xep_0049/__init__.py b/slixmpp/plugins/xep_0049/__init__.py
new file mode 100644
index 00000000..396be75d
--- /dev/null
+++ b/slixmpp/plugins/xep_0049/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0049.stanza import PrivateXML
+from slixmpp.plugins.xep_0049.private_storage import XEP_0049
+
+
+register_plugin(XEP_0049)
diff --git a/slixmpp/plugins/xep_0049/private_storage.py b/slixmpp/plugins/xep_0049/private_storage.py
new file mode 100644
index 00000000..a66c05d1
--- /dev/null
+++ b/slixmpp/plugins/xep_0049/private_storage.py
@@ -0,0 +1,57 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0049 import stanza, PrivateXML
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0049(BasePlugin):
+
+ name = 'xep_0049'
+ description = 'XEP-0049: Private XML Storage'
+ dependencies = set([])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, PrivateXML)
+
+ def register(self, stanza):
+ register_stanza_plugin(PrivateXML, stanza, iterable=True)
+
+ def store(self, data, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+
+ if not isinstance(data, list):
+ data = [data]
+
+ for elem in data:
+ iq['private'].append(elem)
+
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def retrieve(self, name, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq['private'].enable(name)
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0049/stanza.py b/slixmpp/plugins/xep_0049/stanza.py
new file mode 100644
index 00000000..a8d425ba
--- /dev/null
+++ b/slixmpp/plugins/xep_0049/stanza.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET, ElementBase
+
+
+class PrivateXML(ElementBase):
+
+ name = 'query'
+ namespace = 'jabber:iq:private'
+ plugin_attrib = 'private'
+ interfaces = set()
diff --git a/slixmpp/plugins/xep_0050/__init__.py b/slixmpp/plugins/xep_0050/__init__.py
new file mode 100644
index 00000000..5a2d4805
--- /dev/null
+++ b/slixmpp/plugins/xep_0050/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0050.stanza import Command
+from slixmpp.plugins.xep_0050.adhoc import XEP_0050
+
+
+register_plugin(XEP_0050)
+
+
+# Retain some backwards compatibility
+xep_0050 = XEP_0050
diff --git a/slixmpp/plugins/xep_0050/adhoc.py b/slixmpp/plugins/xep_0050/adhoc.py
new file mode 100644
index 00000000..fa6017d5
--- /dev/null
+++ b/slixmpp/plugins/xep_0050/adhoc.py
@@ -0,0 +1,665 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import time
+
+from slixmpp import Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0050 import stanza
+from slixmpp.plugins.xep_0050 import Command
+from slixmpp.plugins.xep_0004 import Form
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0050(BasePlugin):
+
+ """
+ XEP-0050: Ad-Hoc Commands
+
+ XMPP's Adhoc Commands provides a generic workflow mechanism for
+ interacting with applications. The result is similar to menu selections
+ and multi-step dialogs in normal desktop applications. Clients do not
+ need to know in advance what commands are provided by any particular
+ application or agent. While adhoc commands provide similar functionality
+ to Jabber-RPC, adhoc commands are used primarily for human interaction.
+
+ Also see <http://xmpp.org/extensions/xep-0050.html>
+
+ Events:
+ command_execute -- Received a command with action="execute"
+ command_next -- Received a command with action="next"
+ command_complete -- Received a command with action="complete"
+ command_cancel -- Received a command with action="cancel"
+
+ Attributes:
+ commands -- A dictionary mapping JID/node pairs to command
+ names and handlers.
+ sessions -- A dictionary or equivalent backend mapping
+ session IDs to dictionaries containing data
+ relevant to a command's session.
+
+ Methods:
+ plugin_init -- Overrides BasePlugin.plugin_init
+ post_init -- Overrides BasePlugin.post_init
+ new_session -- Return a new session ID.
+ prep_handlers -- Placeholder. May call with a list of handlers
+ to prepare them for use with the session storage
+ backend, if needed.
+ set_backend -- Replace the default session storage with some
+ external storage mechanism, such as a database.
+ The provided backend wrapper must be able to
+ act using the same syntax as a dictionary.
+ add_command -- Add a command for use by external entitites.
+ get_commands -- Retrieve a list of commands provided by a
+ remote agent.
+ send_command -- Send a command request to a remote agent.
+ start_command -- Command user API: initiate a command session
+ continue_command -- Command user API: proceed to the next step
+ cancel_command -- Command user API: cancel a command
+ complete_command -- Command user API: finish a command
+ terminate_command -- Command user API: delete a command's session
+ """
+
+ name = 'xep_0050'
+ description = 'XEP-0050: Ad-Hoc Commands'
+ dependencies = set(['xep_0030', 'xep_0004'])
+ stanza = stanza
+ default_config = {
+ 'session_db': None
+ }
+
+ def plugin_init(self):
+ """Start the XEP-0050 plugin."""
+ self.sessions = self.session_db
+ if self.sessions is None:
+ self.sessions = {}
+
+ self.commands = {}
+
+ self.xmpp.register_handler(
+ Callback("Ad-Hoc Execute",
+ StanzaPath('iq@type=set/command'),
+ self._handle_command))
+
+ register_stanza_plugin(Iq, Command)
+ register_stanza_plugin(Command, Form, iterable=True)
+
+ self.xmpp.add_event_handler('command_execute',
+ self._handle_command_start)
+ self.xmpp.add_event_handler('command_next',
+ self._handle_command_next)
+ self.xmpp.add_event_handler('command_cancel',
+ self._handle_command_cancel)
+ self.xmpp.add_event_handler('command_complete',
+ self._handle_command_complete)
+
+ def plugin_end(self):
+ self.xmpp.del_event_handler('command_execute',
+ self._handle_command_start)
+ self.xmpp.del_event_handler('command_next',
+ self._handle_command_next)
+ self.xmpp.del_event_handler('command_cancel',
+ self._handle_command_cancel)
+ self.xmpp.del_event_handler('command_complete',
+ self._handle_command_complete)
+ self.xmpp.remove_handler('Ad-Hoc Execute')
+ self.xmpp['xep_0030'].del_feature(feature=Command.namespace)
+ self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Command.namespace)
+ self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
+
+ def set_backend(self, db):
+ """
+ Replace the default session storage dictionary with
+ a generic, external data storage mechanism.
+
+ The replacement backend must be able to interact through
+ the same syntax and interfaces as a normal dictionary.
+
+ Arguments:
+ db -- The new session storage mechanism.
+ """
+ self.sessions = db
+
+ def prep_handlers(self, handlers, **kwargs):
+ """
+ Prepare a list of functions for use by the backend service.
+
+ Intended to be replaced by the backend service as needed.
+
+ Arguments:
+ handlers -- A list of function pointers
+ **kwargs -- Any additional parameters required by the backend.
+ """
+ pass
+
+ # =================================================================
+ # Server side (command provider) API
+
+ def add_command(self, jid=None, node=None, name='', handler=None):
+ """
+ Make a new command available to external entities.
+
+ Access control may be implemented in the provided handler.
+
+ Command workflow is done across a sequence of command handlers. The
+ first handler is given the initial Iq stanza of the request in order
+ to support access control. Subsequent handlers are given only the
+ payload items of the command. All handlers will receive the command's
+ session data.
+
+ Arguments:
+ jid -- The JID that will expose the command.
+ node -- The node associated with the command.
+ name -- A human readable name for the command.
+ handler -- A function that will generate the response to the
+ initial command request, as well as enforcing any
+ access control policies.
+ """
+ if jid is None:
+ jid = self.xmpp.boundjid
+ elif not isinstance(jid, JID):
+ jid = JID(jid)
+ item_jid = jid.full
+
+ self.xmpp['xep_0030'].add_identity(category='automation',
+ itype='command-list',
+ name='Ad-Hoc commands',
+ node=Command.namespace,
+ jid=jid)
+ self.xmpp['xep_0030'].add_item(jid=item_jid,
+ name=name,
+ node=Command.namespace,
+ subnode=node,
+ ijid=jid)
+ self.xmpp['xep_0030'].add_identity(category='automation',
+ itype='command-node',
+ name=name,
+ node=node,
+ jid=jid)
+ self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid)
+
+ self.commands[(item_jid, node)] = (name, handler)
+
+ def new_session(self):
+ """Return a new session ID."""
+ return str(time.time()) + '-' + self.xmpp.new_id()
+
+ def _handle_command(self, iq):
+ """Raise command events based on the command action."""
+ self.xmpp.event('command_%s' % iq['command']['action'], iq)
+
+ def _handle_command_start(self, iq):
+ """
+ Process an initial request to execute a command.
+
+ Arguments:
+ iq -- The command execution request.
+ """
+ sessionid = self.new_session()
+ node = iq['command']['node']
+ key = (iq['to'].full, node)
+ name, handler = self.commands.get(key, ('Not found', None))
+ if not handler:
+ log.debug('Command not found: %s, %s', key, self.commands)
+ raise XMPPError('item-not-found')
+
+ payload = []
+ for stanza in iq['command']['substanzas']:
+ payload.append(stanza)
+
+ if len(payload) == 1:
+ payload = payload[0]
+
+ interfaces = set([item.plugin_attrib for item in payload])
+ payload_classes = set([item.__class__ for item in payload])
+
+ initial_session = {'id': sessionid,
+ 'from': iq['from'],
+ 'to': iq['to'],
+ 'node': node,
+ 'payload': payload,
+ 'interfaces': interfaces,
+ 'payload_classes': payload_classes,
+ 'notes': None,
+ 'has_next': False,
+ 'allow_complete': False,
+ 'allow_prev': False,
+ 'past': [],
+ 'next': None,
+ 'prev': None,
+ 'cancel': None}
+
+ session = handler(iq, initial_session)
+
+ self._process_command_response(iq, session)
+
+ def _handle_command_next(self, iq):
+ """
+ Process a request for the next step in the workflow
+ for a command with multiple steps.
+
+ Arguments:
+ iq -- The command continuation request.
+ """
+ sessionid = iq['command']['sessionid']
+ session = self.sessions.get(sessionid)
+
+ if session:
+ handler = session['next']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
+
+ session = handler(results, session)
+
+ self._process_command_response(iq, session)
+ else:
+ raise XMPPError('item-not-found')
+
+ def _handle_command_prev(self, iq):
+ """
+ Process a request for the prev step in the workflow
+ for a command with multiple steps.
+
+ Arguments:
+ iq -- The command continuation request.
+ """
+ sessionid = iq['command']['sessionid']
+ session = self.sessions.get(sessionid)
+
+ if session:
+ handler = session['prev']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
+
+ session = handler(results, session)
+
+ self._process_command_response(iq, session)
+ else:
+ raise XMPPError('item-not-found')
+
+ def _process_command_response(self, iq, session):
+ """
+ Generate a command reply stanza based on the
+ provided session data.
+
+ Arguments:
+ iq -- The command request stanza.
+ session -- A dictionary of relevant session data.
+ """
+ sessionid = session['id']
+
+ payload = session['payload']
+ if payload is None:
+ payload = []
+ if not isinstance(payload, list):
+ payload = [payload]
+
+ interfaces = session.get('interfaces', set())
+ payload_classes = session.get('payload_classes', set())
+
+ interfaces.update(set([item.plugin_attrib for item in payload]))
+ payload_classes.update(set([item.__class__ for item in payload]))
+
+ session['interfaces'] = interfaces
+ session['payload_classes'] = payload_classes
+
+ self.sessions[sessionid] = session
+
+ for item in payload:
+ register_stanza_plugin(Command, item.__class__, iterable=True)
+
+ iq = iq.reply()
+ iq['command']['node'] = session['node']
+ iq['command']['sessionid'] = session['id']
+
+ if session['next'] is None:
+ iq['command']['actions'] = []
+ iq['command']['status'] = 'completed'
+ elif session['has_next']:
+ actions = ['next']
+ if session['allow_complete']:
+ actions.append('complete')
+ if session['allow_prev']:
+ actions.append('prev')
+ iq['command']['actions'] = actions
+ iq['command']['status'] = 'executing'
+ else:
+ iq['command']['actions'] = ['complete']
+ iq['command']['status'] = 'executing'
+
+ iq['command']['notes'] = session['notes']
+
+ for item in payload:
+ iq['command'].append(item)
+
+ iq.send()
+
+ def _handle_command_cancel(self, iq):
+ """
+ Process a request to cancel a command's execution.
+
+ Arguments:
+ iq -- The command cancellation request.
+ """
+ node = iq['command']['node']
+ sessionid = iq['command']['sessionid']
+
+ session = self.sessions.get(sessionid)
+
+ if session:
+ handler = session['cancel']
+ if handler:
+ handler(iq, session)
+ del self.sessions[sessionid]
+ iq = iq.reply()
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['status'] = 'canceled'
+ iq['command']['notes'] = session['notes']
+ iq.send()
+ else:
+ raise XMPPError('item-not-found')
+
+
+ def _handle_command_complete(self, iq):
+ """
+ Process a request to finish the execution of command
+ and terminate the workflow.
+
+ All data related to the command session will be removed.
+
+ Arguments:
+ iq -- The command completion request.
+ """
+ node = iq['command']['node']
+ sessionid = iq['command']['sessionid']
+ session = self.sessions.get(sessionid)
+
+ if session:
+ handler = session['next']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
+
+ if handler:
+ handler(results, session)
+
+ del self.sessions[sessionid]
+
+ payload = session['payload']
+ if payload is None:
+ payload = []
+ if not isinstance(payload, list):
+ payload = [payload]
+
+ for item in payload:
+ register_stanza_plugin(Command, item.__class__, iterable=True)
+
+ iq = iq.reply()
+
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['actions'] = []
+ iq['command']['status'] = 'completed'
+ iq['command']['notes'] = session['notes']
+
+ for item in payload:
+ iq['command'].append(item)
+
+ iq.send()
+ else:
+ raise XMPPError('item-not-found')
+
+ # =================================================================
+ # Client side (command user) API
+
+ def get_commands(self, jid, **kwargs):
+ """
+ Return a list of commands provided by a given JID.
+
+ Arguments:
+ jid -- The JID to query for commands.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the items.
+ ifrom -- Specifiy the sender's JID.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ iterator -- If True, return a result set iterator using
+ the XEP-0059 plugin, if the plugin is loaded.
+ Otherwise the parameter is ignored.
+ """
+ return self.xmpp['xep_0030'].get_items(jid=jid,
+ node=Command.namespace,
+ **kwargs)
+
+ def send_command(self, jid, node, ifrom=None, action='execute',
+ payload=None, sessionid=None, flow=False, **kwargs):
+ """
+ Create and send a command stanza, without using the provided
+ workflow management APIs.
+
+ Arguments:
+ jid -- The JID to send the command request or result.
+ node -- The node for the command.
+ ifrom -- Specify the sender's JID.
+ action -- May be one of: execute, cancel, complete,
+ or cancel.
+ payload -- Either a list of payload items, or a single
+ payload item such as a data form.
+ sessionid -- The current session's ID value.
+ flow -- If True, process the Iq result using the
+ command workflow methods contained in the
+ session instead of returning the response
+ stanza itself. Defaults to False.
+ timeout -- The length of time (in seconds) to wait for a
+ response before exiting the send call
+ if blocking is used. Defaults to
+ slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler
+ function. Will be executed when a reply
+ stanza is received if flow=False.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['command']['node'] = node
+ iq['command']['action'] = action
+ if sessionid is not None:
+ iq['command']['sessionid'] = sessionid
+ if payload is not None:
+ if not isinstance(payload, list):
+ payload = [payload]
+ for item in payload:
+ iq['command'].append(item)
+ if not flow:
+ return iq.send(**kwargs)
+ else:
+ iq.send(callback=self._handle_command_result)
+
+ def start_command(self, jid, node, session, ifrom=None):
+ """
+ Initiate executing a command provided by a remote agent.
+
+ The provided session dictionary should contain:
+ next -- A handler for processing the command result.
+ error -- A handler for processing any error stanzas
+ generated by the request.
+
+ Arguments:
+ jid -- The JID to send the command request.
+ node -- The node for the desired command.
+ session -- A dictionary of relevant session data.
+ ifrom -- Optionally specify the sender's JID.
+ """
+ session['jid'] = jid
+ session['node'] = node
+ session['timestamp'] = time.time()
+ if 'payload' not in session:
+ session['payload'] = None
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ session['from'] = ifrom
+ iq['command']['node'] = node
+ iq['command']['action'] = 'execute'
+ if session['payload'] is not None:
+ payload = session['payload']
+ if not isinstance(payload, list):
+ payload = list(payload)
+ for stanza in payload:
+ iq['command'].append(stanza)
+ sessionid = 'client:pending_' + iq['id']
+ session['id'] = sessionid
+ self.sessions[sessionid] = session
+ iq.send(callback=self._handle_command_result)
+
+ def continue_command(self, session, direction='next'):
+ """
+ Execute the next action of the command.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ self.sessions[sessionid] = session
+
+ self.send_command(session['jid'],
+ session['node'],
+ ifrom=session.get('from', None),
+ action=direction,
+ payload=session.get('payload', None),
+ sessionid=session['id'],
+ flow=True)
+
+ def cancel_command(self, session):
+ """
+ Cancel the execution of a command.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ self.sessions[sessionid] = session
+
+ self.send_command(session['jid'],
+ session['node'],
+ ifrom=session.get('from', None),
+ action='cancel',
+ payload=session.get('payload', None),
+ sessionid=session['id'],
+ flow=True)
+
+ def complete_command(self, session):
+ """
+ Finish the execution of a command workflow.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ self.sessions[sessionid] = session
+
+ self.send_command(session['jid'],
+ session['node'],
+ ifrom=session.get('from', None),
+ action='complete',
+ payload=session.get('payload', None),
+ sessionid=session['id'],
+ flow=True)
+
+ def terminate_command(self, session):
+ """
+ Delete a command's session after a command has completed
+ or an error has occured.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ try:
+ del self.sessions[sessionid]
+ except Exception as e:
+ log.error("Error deleting adhoc command session: %s" % e.message)
+
+ def _handle_command_result(self, iq):
+ """
+ Process the results of a command request.
+
+ Will execute the 'next' handler stored in the session
+ data, or the 'error' handler depending on the Iq's type.
+
+ Arguments:
+ iq -- The command response.
+ """
+ sessionid = 'client:' + iq['command']['sessionid']
+ pending = False
+
+ if sessionid not in self.sessions:
+ pending = True
+ pendingid = 'client:pending_' + iq['id']
+ if pendingid not in self.sessions:
+ return
+ sessionid = pendingid
+
+ session = self.sessions[sessionid]
+ sessionid = 'client:' + iq['command']['sessionid']
+ session['id'] = iq['command']['sessionid']
+
+ self.sessions[sessionid] = session
+
+ if pending:
+ del self.sessions[pendingid]
+
+ handler_type = 'next'
+ if iq['type'] == 'error':
+ handler_type = 'error'
+ handler = session.get(handler_type, None)
+ if handler:
+ handler(iq, session)
+ elif iq['type'] == 'error':
+ self.terminate_command(session)
+
+ if iq['command']['status'] == 'completed':
+ self.terminate_command(session)
diff --git a/slixmpp/plugins/xep_0050/stanza.py b/slixmpp/plugins/xep_0050/stanza.py
new file mode 100644
index 00000000..9bae7d15
--- /dev/null
+++ b/slixmpp/plugins/xep_0050/stanza.py
@@ -0,0 +1,185 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Command(ElementBase):
+
+ """
+ XMPP's Adhoc Commands provides a generic workflow mechanism for
+ interacting with applications. The result is similar to menu selections
+ and multi-step dialogs in normal desktop applications. Clients do not
+ need to know in advance what commands are provided by any particular
+ application or agent. While adhoc commands provide similar functionality
+ to Jabber-RPC, adhoc commands are used primarily for human interaction.
+
+ Also see <http://xmpp.org/extensions/xep-0050.html>
+
+ Example command stanzas:
+ <iq type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="run_foo"
+ action="execute" />
+ </iq>
+
+ <iq type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="run_foo"
+ sessionid="12345"
+ status="executing">
+ <actions>
+ <complete />
+ </actions>
+ <note type="info">Information!</note>
+ <x xmlns="jabber:x:data">
+ <field var="greeting"
+ type="text-single"
+ label="Greeting" />
+ </x>
+ </command>
+ </iq>
+
+ Stanza Interface:
+ action -- The action to perform.
+ actions -- The set of allowable next actions.
+ node -- The node associated with the command.
+ notes -- A list of tuples for informative notes.
+ sessionid -- A unique identifier for a command session.
+ status -- May be one of: canceled, completed, or executing.
+
+ Attributes:
+ actions -- A set of allowed action values.
+ statuses -- A set of allowed status values.
+ next_actions -- A set of allowed next action names.
+
+ Methods:
+ get_action -- Return the requested action.
+ get_actions -- Return the allowable next actions.
+ set_actions -- Set the allowable next actions.
+ del_actions -- Remove the current set of next actions.
+ get_notes -- Return a list of informative note data.
+ set_notes -- Set informative notes.
+ del_notes -- Remove any note data.
+ add_note -- Add a single note.
+ """
+
+ name = 'command'
+ namespace = 'http://jabber.org/protocol/commands'
+ plugin_attrib = 'command'
+ interfaces = set(('action', 'sessionid', 'node',
+ 'status', 'actions', 'notes'))
+ actions = set(('cancel', 'complete', 'execute', 'next', 'prev'))
+ statuses = set(('canceled', 'completed', 'executing'))
+ next_actions = set(('prev', 'next', 'complete'))
+
+ def get_action(self):
+ """
+ Return the value of the action attribute.
+
+ If the Iq stanza's type is "set" then use a default
+ value of "execute".
+ """
+ if self.parent()['type'] == 'set':
+ return self._get_attr('action', default='execute')
+ return self._get_attr('action')
+
+ def set_actions(self, values):
+ """
+ Assign the set of allowable next actions.
+
+ Arguments:
+ values -- A list containing any combination of:
+ 'prev', 'next', and 'complete'
+ """
+ self.del_actions()
+ if values:
+ self._set_sub_text('{%s}actions' % self.namespace, '', True)
+ actions = self.find('{%s}actions' % self.namespace)
+ for val in values:
+ if val in self.next_actions:
+ action = ET.Element('{%s}%s' % (self.namespace, val))
+ actions.append(action)
+
+ def get_actions(self):
+ """
+ Return the set of allowable next actions.
+ """
+ actions = set()
+ actions_xml = self.find('{%s}actions' % self.namespace)
+ if actions_xml is not None:
+ for action in self.next_actions:
+ action_xml = actions_xml.find('{%s}%s' % (self.namespace,
+ action))
+ if action_xml is not None:
+ actions.add(action)
+ return actions
+
+ def del_actions(self):
+ """
+ Remove all allowable next actions.
+ """
+ self._del_sub('{%s}actions' % self.namespace)
+
+ def get_notes(self):
+ """
+ Return a list of note information.
+
+ Example:
+ [('info', 'Some informative data'),
+ ('warning', 'Use caution'),
+ ('error', 'The command ran, but had errors')]
+ """
+ notes = []
+ notes_xml = self.findall('{%s}note' % self.namespace)
+ for note in notes_xml:
+ notes.append((note.attrib.get('type', 'info'),
+ note.text))
+ return notes
+
+ def set_notes(self, notes):
+ """
+ Add multiple notes to the command result.
+
+ Each note is a tuple, with the first item being one of:
+ 'info', 'warning', or 'error', and the second item being
+ any human readable message.
+
+ Example:
+ [('info', 'Some informative data'),
+ ('warning', 'Use caution'),
+ ('error', 'The command ran, but had errors')]
+
+
+ Arguments:
+ notes -- A list of tuples of note information.
+ """
+ self.del_notes()
+ for note in notes:
+ self.add_note(note[1], note[0])
+
+ def del_notes(self):
+ """
+ Remove all notes associated with the command result.
+ """
+ notes_xml = self.findall('{%s}note' % self.namespace)
+ for note in notes_xml:
+ self.xml.remove(note)
+
+ def add_note(self, msg='', ntype='info'):
+ """
+ Add a single note annotation to the command.
+
+ Arguments:
+ msg -- A human readable message.
+ ntype -- One of: 'info', 'warning', 'error'
+ """
+ xml = ET.Element('{%s}note' % self.namespace)
+ xml.attrib['type'] = ntype
+ xml.text = msg
+ self.xml.append(xml)
diff --git a/slixmpp/plugins/xep_0054/__init__.py b/slixmpp/plugins/xep_0054/__init__.py
new file mode 100644
index 00000000..2029b41f
--- /dev/null
+++ b/slixmpp/plugins/xep_0054/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0054.stanza import VCardTemp
+from slixmpp.plugins.xep_0054.vcard_temp import XEP_0054
+
+
+register_plugin(XEP_0054)
diff --git a/slixmpp/plugins/xep_0054/stanza.py b/slixmpp/plugins/xep_0054/stanza.py
new file mode 100644
index 00000000..48a41432
--- /dev/null
+++ b/slixmpp/plugins/xep_0054/stanza.py
@@ -0,0 +1,571 @@
+import base64
+import datetime as dt
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
+from slixmpp.plugins import xep_0082
+
+
+class VCardTemp(ElementBase):
+ name = 'vCard'
+ namespace = 'vcard-temp'
+ plugin_attrib = 'vcard_temp'
+ interfaces = set(['FN', 'VERSION'])
+ sub_interfaces = set(['FN', 'VERSION'])
+
+
+class Name(ElementBase):
+ name = 'N'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ interfaces = set(['FAMILY', 'GIVEN', 'MIDDLE', 'PREFIX', 'SUFFIX'])
+ sub_interfaces = interfaces
+
+ def _set_component(self, name, value):
+ if isinstance(value, list):
+ value = ','.join(value)
+ if value is not None:
+ self._set_sub_text(name, value, keep=True)
+ else:
+ self._del_sub(name)
+
+ def _get_component(self, name):
+ value = self._get_sub_text(name, '')
+ if ',' in value:
+ value = [v.strip() for v in value.split(',')]
+ return value
+
+ def set_family(self, value):
+ self._set_component('FAMILY', value)
+
+ def get_family(self):
+ return self._get_component('FAMILY')
+
+ def set_given(self, value):
+ self._set_component('GIVEN', value)
+
+ def get_given(self):
+ return self._get_component('GIVEN')
+
+ def set_middle(self, value):
+ print(value)
+ self._set_component('MIDDLE', value)
+
+ def get_middle(self):
+ return self._get_component('MIDDLE')
+
+ def set_prefix(self, value):
+ self._set_component('PREFIX', value)
+
+ def get_prefix(self):
+ return self._get_component('PREFIX')
+
+ def set_suffix(self, value):
+ self._set_component('SUFFIX', value)
+
+ def get_suffix(self):
+ return self._get_component('SUFFIX')
+
+
+class Nickname(ElementBase):
+ name = 'NICKNAME'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'nicknames'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_nickname(self, value):
+ if not value:
+ self.xml.text = ''
+ return
+
+ if not isinstance(value, list):
+ value = [value]
+
+ self.xml.text = ','.join(value)
+
+ def get_nickname(self):
+ if self.xml.text:
+ return self.xml.text.split(',')
+
+
+class Email(ElementBase):
+ name = 'EMAIL'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'emails'
+ interfaces = set(['HOME', 'WORK', 'INTERNET', 'PREF', 'X400', 'USERID'])
+ sub_interfaces = set(['USERID'])
+ bool_interfaces = set(['HOME', 'WORK', 'INTERNET', 'PREF', 'X400'])
+
+
+class Address(ElementBase):
+ name = 'ADR'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'addresses'
+ interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INTL',
+ 'PREF', 'POBOX', 'EXTADD', 'STREET', 'LOCALITY',
+ 'REGION', 'PCODE', 'CTRY'])
+ sub_interfaces = set(['POBOX', 'EXTADD', 'STREET', 'LOCALITY',
+ 'REGION', 'PCODE', 'CTRY'])
+ bool_interfaces = set(['HOME', 'WORK', 'DOM', 'INTL', 'PREF'])
+
+
+class Telephone(ElementBase):
+ name = 'TEL'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'telephone_numbers'
+ interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG',
+ 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS',
+ 'PREF', 'NUMBER'])
+ sub_interfaces = set(['NUMBER'])
+ bool_interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER',
+ 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM',
+ 'ISDN', 'PCS', 'PREF'])
+
+ def setup(self, xml=None):
+ super(Telephone, self).setup(xml=xml)
+ ## this blanks out numbers received from server
+ ##self._set_sub_text('NUMBER', '', keep=True)
+
+ def set_number(self, value):
+ self._set_sub_text('NUMBER', value, keep=True)
+
+ def del_number(self):
+ self._set_sub_text('NUMBER', '', keep=True)
+
+
+class Label(ElementBase):
+ name = 'LABEL'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'labels'
+ interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT',
+ 'PREF', 'lines'])
+ bool_interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM',
+ 'INT', 'PREF'])
+
+ def add_line(self, value):
+ line = ET.Element('{%s}LINE' % self.namespace)
+ line.text = value
+ self.xml.append(line)
+
+ def get_lines(self):
+ lines = self.xml.find('{%s}LINE' % self.namespace)
+ if lines is None:
+ return []
+ return [line.text for line in lines]
+
+ def set_lines(self, values):
+ self.del_lines()
+ for line in values:
+ self.add_line(line)
+
+ def del_lines(self):
+ lines = self.xml.find('{%s}LINE' % self.namespace)
+ if lines is None:
+ return
+ for line in lines:
+ self.xml.remove(line)
+
+
+class Geo(ElementBase):
+ name = 'GEO'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'geolocations'
+ interfaces = set(['LAT', 'LON'])
+ sub_interfaces = interfaces
+
+
+class Org(ElementBase):
+ name = 'ORG'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'organizations'
+ interfaces = set(['ORGNAME', 'ORGUNIT', 'orgunits'])
+ sub_interfaces = set(['ORGNAME', 'ORGUNIT'])
+
+ def add_orgunit(self, value):
+ orgunit = ET.Element('{%s}ORGUNIT' % self.namespace)
+ orgunit.text = value
+ self.xml.append(orgunit)
+
+ def get_orgunits(self):
+ orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace)
+ if orgunits is None:
+ return []
+ return [orgunit.text for orgunit in orgunits]
+
+ def set_orgunits(self, values):
+ self.del_orgunits()
+ for orgunit in values:
+ self.add_orgunit(orgunit)
+
+ def del_orgunits(self):
+ orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace)
+ if orgunits is None:
+ return
+ for orgunit in orgunits:
+ self.xml.remove(orgunit)
+
+
+class Photo(ElementBase):
+ name = 'PHOTO'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'photos'
+ interfaces = set(['TYPE', 'EXTVAL'])
+ sub_interfaces = interfaces
+
+
+class Logo(ElementBase):
+ name = 'LOGO'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'logos'
+ interfaces = set(['TYPE', 'EXTVAL'])
+ sub_interfaces = interfaces
+
+
+class Sound(ElementBase):
+ name = 'SOUND'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'sounds'
+ interfaces = set(['PHONETC', 'EXTVAL'])
+ sub_interfaces = interfaces
+
+
+class BinVal(ElementBase):
+ name = 'BINVAL'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ interfaces = set(['BINVAL'])
+ is_extension = True
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def set_binval(self, value):
+ self.del_binval()
+ parent = self.parent()
+ if value:
+ xml = ET.Element('{%s}BINVAL' % self.namespace)
+ xml.text = bytes(base64.b64encode(value)).decode('utf-8')
+ parent.append(xml)
+
+ def get_binval(self):
+ parent = self.parent()
+ xml = parent.find('{%s}BINVAL' % self.namespace)
+ if xml is not None:
+ return base64.b64decode(bytes(xml.text))
+ return b''
+
+ def del_binval(self):
+ self.parent()._del_sub('{%s}BINVAL' % self.namespace)
+
+
+class Classification(ElementBase):
+ name = 'CLASS'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'classifications'
+ interfaces = set(['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'])
+ bool_interfaces = interfaces
+
+
+class Categories(ElementBase):
+ name = 'CATEGORIES'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'categories'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_categories(self, values):
+ self.del_categories()
+ for keyword in values:
+ item = ET.Element('{%s}KEYWORD' % self.namespace)
+ item.text = keyword
+ self.xml.append(item)
+
+ def get_categories(self):
+ items = self.xml.findall('{%s}KEYWORD' % self.namespace)
+ if items is None:
+ return []
+ keywords = []
+ for item in items:
+ keywords.append(item.text)
+ return keywords
+
+ def del_categories(self):
+ items = self.xml.findall('{%s}KEYWORD' % self.namespace)
+ for item in items:
+ self.xml.remove(item)
+
+
+class Birthday(ElementBase):
+ name = 'BDAY'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'birthdays'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_bday(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self.xml.text = value
+
+ def get_bday(self):
+ if not self.xml.text:
+ return None
+ try:
+ return xep_0082.parse(self.xml.text)
+ except ValueError:
+ return self.xml.text
+
+
+class Rev(ElementBase):
+ name = 'REV'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'revision_dates'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_rev(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self.xml.text = value
+
+ def get_rev(self):
+ if not self.xml.text:
+ return None
+ try:
+ return xep_0082.parse(self.xml.text)
+ except ValueError:
+ return self.xml.text
+
+
+class Title(ElementBase):
+ name = 'TITLE'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'titles'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_title(self, value):
+ self.xml.text = value
+
+ def get_title(self):
+ return self.xml.text
+
+
+class Role(ElementBase):
+ name = 'ROLE'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'roles'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_role(self, value):
+ self.xml.text = value
+
+ def get_role(self):
+ return self.xml.text
+
+
+class Note(ElementBase):
+ name = 'NOTE'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'notes'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_note(self, value):
+ self.xml.text = value
+
+ def get_note(self):
+ return self.xml.text
+
+
+class Desc(ElementBase):
+ name = 'DESC'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'descriptions'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_desc(self, value):
+ self.xml.text = value
+
+ def get_desc(self):
+ return self.xml.text
+
+
+class URL(ElementBase):
+ name = 'URL'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'urls'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_url(self, value):
+ self.xml.text = value
+
+ def get_url(self):
+ return self.xml.text
+
+
+class UID(ElementBase):
+ name = 'UID'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'uids'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_uid(self, value):
+ self.xml.text = value
+
+ def get_uid(self):
+ return self.xml.text
+
+
+class ProdID(ElementBase):
+ name = 'PRODID'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'product_ids'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_prodid(self, value):
+ self.xml.text = value
+
+ def get_prodid(self):
+ return self.xml.text
+
+
+class Mailer(ElementBase):
+ name = 'MAILER'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'mailers'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_mailer(self, value):
+ self.xml.text = value
+
+ def get_mailer(self):
+ return self.xml.text
+
+
+class SortString(ElementBase):
+ name = 'SORT-STRING'
+ namespace = 'vcard-temp'
+ plugin_attrib = 'SORT_STRING'
+ plugin_multi_attrib = 'sort_strings'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_sort_string(self, value):
+ self.xml.text = value
+
+ def get_sort_string(self):
+ return self.xml.text
+
+
+class Agent(ElementBase):
+ name = 'AGENT'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'agents'
+ interfaces = set(['EXTVAL'])
+ sub_interfaces = interfaces
+
+
+class JabberID(ElementBase):
+ name = 'JABBERID'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'jids'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_jabberid(self, value):
+ self.xml.text = JID(value).bare
+
+ def get_jabberid(self):
+ return JID(self.xml.text)
+
+
+class TimeZone(ElementBase):
+ name = 'TZ'
+ namespace = 'vcard-temp'
+ plugin_attrib = name
+ plugin_multi_attrib = 'timezones'
+ interfaces = set([name])
+ is_extension = True
+
+ def set_tz(self, value):
+ time = xep_0082.time(offset=value)
+ if time[-1] == 'Z':
+ self.xml.text = 'Z'
+ else:
+ self.xml.text = time[-6:]
+
+ def get_tz(self):
+ if not self.xml.text:
+ return xep_0082.tzutc()
+ try:
+ time = xep_0082.parse('00:00:00%s' % self.xml.text)
+ return time.tzinfo
+ except ValueError:
+ return self.xml.text
+
+
+register_stanza_plugin(VCardTemp, Name)
+register_stanza_plugin(VCardTemp, Address, iterable=True)
+register_stanza_plugin(VCardTemp, Agent, iterable=True)
+register_stanza_plugin(VCardTemp, Birthday, iterable=True)
+register_stanza_plugin(VCardTemp, Categories, iterable=True)
+register_stanza_plugin(VCardTemp, Desc, iterable=True)
+register_stanza_plugin(VCardTemp, Email, iterable=True)
+register_stanza_plugin(VCardTemp, Geo, iterable=True)
+register_stanza_plugin(VCardTemp, JabberID, iterable=True)
+register_stanza_plugin(VCardTemp, Label, iterable=True)
+register_stanza_plugin(VCardTemp, Logo, iterable=True)
+register_stanza_plugin(VCardTemp, Mailer, iterable=True)
+register_stanza_plugin(VCardTemp, Note, iterable=True)
+register_stanza_plugin(VCardTemp, Nickname, iterable=True)
+register_stanza_plugin(VCardTemp, Org, iterable=True)
+register_stanza_plugin(VCardTemp, Photo, iterable=True)
+register_stanza_plugin(VCardTemp, ProdID, iterable=True)
+register_stanza_plugin(VCardTemp, Rev, iterable=True)
+register_stanza_plugin(VCardTemp, Role, iterable=True)
+register_stanza_plugin(VCardTemp, SortString, iterable=True)
+register_stanza_plugin(VCardTemp, Sound, iterable=True)
+register_stanza_plugin(VCardTemp, Telephone, iterable=True)
+register_stanza_plugin(VCardTemp, Title, iterable=True)
+register_stanza_plugin(VCardTemp, TimeZone, iterable=True)
+register_stanza_plugin(VCardTemp, UID, iterable=True)
+register_stanza_plugin(VCardTemp, URL, iterable=True)
+
+register_stanza_plugin(Photo, BinVal)
+register_stanza_plugin(Logo, BinVal)
+register_stanza_plugin(Sound, BinVal)
+
+register_stanza_plugin(Agent, VCardTemp)
diff --git a/slixmpp/plugins/xep_0054/vcard_temp.py b/slixmpp/plugins/xep_0054/vcard_temp.py
new file mode 100644
index 00000000..f0173386
--- /dev/null
+++ b/slixmpp/plugins/xep_0054/vcard_temp.py
@@ -0,0 +1,147 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import JID, Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0054 import VCardTemp, stanza
+from slixmpp import future_wrapper
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0054(BasePlugin):
+
+ """
+ XEP-0054: vcard-temp
+ """
+
+ name = 'xep_0054'
+ description = 'XEP-0054: vcard-temp'
+ dependencies = set(['xep_0030', 'xep_0082'])
+ stanza = stanza
+
+ def plugin_init(self):
+ """
+ Start the XEP-0054 plugin.
+ """
+ register_stanza_plugin(Iq, VCardTemp)
+
+
+ self.api.register(self._set_vcard, 'set_vcard', default=True)
+ self.api.register(self._get_vcard, 'get_vcard', default=True)
+ self.api.register(self._del_vcard, 'del_vcard', default=True)
+
+ self._vcard_cache = {}
+
+ self.xmpp.register_handler(
+ Callback('VCardTemp',
+ StanzaPath('iq/vcard_temp'),
+ self._handle_get_vcard))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('VCardTemp')
+ self.xmpp['xep_0030'].del_feature(feature='vcard-temp')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('vcard-temp')
+
+ def make_vcard(self):
+ return VCardTemp()
+
+ @future_wrapper
+ def get_vcard(self, jid=None, ifrom=None, local=None, cached=False,
+ callback=None, timeout=None, timeout_callback=None):
+ if local is None:
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+ elif jid in (None, ''):
+ local = True
+
+ if local:
+ vcard = self.api['get_vcard'](jid, None, ifrom)
+ if not isinstance(vcard, Iq):
+ iq = self.xmpp.Iq()
+ if vcard is None:
+ vcard = VCardTemp()
+ iq.append(vcard)
+ return iq
+ return vcard
+
+ if cached:
+ vcard = self.api['get_vcard'](jid, None, ifrom)
+ if vcard is not None:
+ if not isinstance(vcard, Iq):
+ iq = self.xmpp.Iq()
+ iq.append(vcard)
+ return iq
+ return vcard
+
+ iq = self.xmpp.Iq()
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq.enable('vcard_temp')
+
+ return iq.send(callback=callback, timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ @future_wrapper
+ def publish_vcard(self, vcard=None, jid=None, ifrom=None,
+ callback=None, timeout=None, timeout_callback=None):
+ self.api['set_vcard'](jid, None, ifrom, vcard)
+ if self.xmpp.is_component:
+ return
+
+ iq = self.xmpp.Iq()
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'set'
+ iq.append(vcard)
+ return iq.send(callback=callback, timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def _handle_get_vcard(self, iq):
+ if iq['type'] == 'result':
+ self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp'])
+ return
+ elif iq['type'] == 'get':
+ vcard = self.api['get_vcard'](iq['from'].bare)
+ if isinstance(vcard, Iq):
+ vcard.send()
+ else:
+ iq = iq.reply()
+ iq.append(vcard)
+ iq.send()
+ elif iq['type'] == 'set':
+ raise XMPPError('service-unavailable')
+
+ # =================================================================
+
+ def _set_vcard(self, jid, node, ifrom, vcard):
+ self._vcard_cache[jid.bare] = vcard
+
+ def _get_vcard(self, jid, node, ifrom, vcard):
+ return self._vcard_cache.get(jid.bare, None)
+
+ def _del_vcard(self, jid, node, ifrom, vcard):
+ if jid.bare in self._vcard_cache:
+ del self._vcard_cache[jid.bare]
diff --git a/slixmpp/plugins/xep_0059/__init__.py b/slixmpp/plugins/xep_0059/__init__.py
new file mode 100644
index 00000000..21a0e5c9
--- /dev/null
+++ b/slixmpp/plugins/xep_0059/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0059.stanza import Set
+from slixmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059
+
+
+register_plugin(XEP_0059)
+
+# Retain some backwards compatibility
+xep_0059 = XEP_0059
diff --git a/slixmpp/plugins/xep_0059/rsm.py b/slixmpp/plugins/xep_0059/rsm.py
new file mode 100644
index 00000000..5876a9aa
--- /dev/null
+++ b/slixmpp/plugins/xep_0059/rsm.py
@@ -0,0 +1,145 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin, register_plugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0059 import stanza, Set
+from slixmpp.exceptions import XMPPError
+
+
+log = logging.getLogger(__name__)
+
+
+class ResultIterator():
+
+ """
+ An iterator for Result Set Managment
+ """
+
+ def __init__(self, query, interface, results='substanzas', amount=10,
+ start=None, reverse=False):
+ """
+ Arguments:
+ query -- The template query
+ interface -- The substanza of the query, for example disco_items
+ results -- The query stanza's interface which provides a
+ countable list of query results.
+ amount -- The max amounts of items to request per iteration
+ start -- From which item id to start
+ reverse -- If True, page backwards through the results
+
+ Example:
+ q = Iq()
+ q['to'] = 'pubsub.example.com'
+ q['disco_items']['node'] = 'blog'
+ for i in ResultIterator(q, 'disco_items', '10'):
+ print i['disco_items']['items']
+
+ """
+ self.query = query
+ self.amount = amount
+ self.start = start
+ self.interface = interface
+ self.results = results
+ self.reverse = reverse
+ self._stop = False
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ return self.next()
+
+ def next(self):
+ """
+ Return the next page of results from a query.
+
+ Note: If using backwards paging, then the next page of
+ results will be the items before the current page
+ of items.
+ """
+ if self._stop:
+ raise StopIteration
+ self.query[self.interface]['rsm']['before'] = self.reverse
+ self.query['id'] = self.query.stream.new_id()
+ self.query[self.interface]['rsm']['max'] = str(self.amount)
+
+ if self.start and self.reverse:
+ self.query[self.interface]['rsm']['before'] = self.start
+ elif self.start:
+ self.query[self.interface]['rsm']['after'] = self.start
+
+ try:
+ r = self.query.send(block=True)
+
+ if not r[self.interface]['rsm']['first'] and \
+ not r[self.interface]['rsm']['last']:
+ raise StopIteration
+
+ if r[self.interface]['rsm']['count'] and \
+ r[self.interface]['rsm']['first_index']:
+ count = int(r[self.interface]['rsm']['count'])
+ first = int(r[self.interface]['rsm']['first_index'])
+ num_items = len(r[self.interface][self.results])
+ if first + num_items == count:
+ self._stop = True
+
+ if self.reverse:
+ self.start = r[self.interface]['rsm']['first']
+ else:
+ self.start = r[self.interface]['rsm']['last']
+
+ return r
+ except XMPPError:
+ raise StopIteration
+
+
+class XEP_0059(BasePlugin):
+
+ """
+ XEP-0050: Result Set Management
+ """
+
+ name = 'xep_0059'
+ description = 'XEP-0059: Result Set Management'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ """
+ Start the XEP-0059 plugin.
+ """
+ register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems,
+ self.stanza.Set)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Set.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Set.namespace)
+
+ def iterate(self, stanza, interface, results='substanzas'):
+ """
+ Create a new result set iterator for a given stanza query.
+
+ Arguments:
+ stanza -- A stanza object to serve as a template for
+ queries made each iteration. For example, a
+ basic disco#items query.
+ interface -- The name of the substanza to which the
+ result set management stanza should be
+ appended. For example, for disco#items queries
+ the interface 'disco_items' should be used.
+ results -- The name of the interface containing the
+ query results (typically just 'substanzas').
+ """
+ return ResultIterator(stanza, interface, results)
diff --git a/slixmpp/plugins/xep_0059/stanza.py b/slixmpp/plugins/xep_0059/stanza.py
new file mode 100644
index 00000000..e2701af4
--- /dev/null
+++ b/slixmpp/plugins/xep_0059/stanza.py
@@ -0,0 +1,108 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
+
+
+class Set(ElementBase):
+
+ """
+ XEP-0059 (Result Set Managment) can be used to manage the
+ results of queries. For example, limiting the number of items
+ per response or starting at certain positions.
+
+ Example set stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>2</max>
+ </set>
+ </query>
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="conference.example.com" />
+ <item jid="pubsub.example.com" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first>conference.example.com</first>
+ <last>pubsub.example.com</last>
+ </set>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ first_index -- The index attribute of <first>
+ after -- The id defining from which item to start
+ before -- The id defining from which item to
+ start when browsing backwards
+ max -- Max amount per response
+ first -- Id for the first item in the response
+ last -- Id for the last item in the response
+ index -- Used to set an index to start from
+ count -- The number of remote items available
+
+ Methods:
+ set_first_index -- Sets the index attribute for <first> and
+ creates the element if it doesn't exist
+ get_first_index -- Returns the value of the index
+ attribute for <first>
+ del_first_index -- Removes the index attribute for <first>
+ but keeps the element
+ set_before -- Sets the value of <before>, if the value is True
+ then the element will be created without a value
+ get_before -- Returns the value of <before>, if it is
+ empty it will return True
+
+ """
+ namespace = 'http://jabber.org/protocol/rsm'
+ name = 'set'
+ plugin_attrib = 'rsm'
+ sub_interfaces = set(('first', 'after', 'before', 'count',
+ 'index', 'last', 'max'))
+ interfaces = set(('first_index', 'first', 'after', 'before',
+ 'count', 'index', 'last', 'max'))
+
+ def set_first_index(self, val):
+ fi = self.find("{%s}first" % (self.namespace))
+ if fi is not None:
+ if val:
+ fi.attrib['index'] = val
+ elif 'index' in fi.attrib:
+ del fi.attrib['index']
+ elif val:
+ fi = ET.Element("{%s}first" % (self.namespace))
+ fi.attrib['index'] = val
+ self.xml.append(fi)
+
+ def get_first_index(self):
+ fi = self.find("{%s}first" % (self.namespace))
+ if fi is not None:
+ return fi.attrib.get('index', '')
+
+ def del_first_index(self):
+ fi = self.xml.find("{%s}first" % (self.namespace))
+ if fi is not None:
+ del fi.attrib['index']
+
+ def set_before(self, val):
+ b = self.xml.find("{%s}before" % (self.namespace))
+ if b is None and val is True:
+ self._set_sub_text('{%s}before' % self.namespace, '', True)
+ else:
+ self._set_sub_text('{%s}before' % self.namespace, val)
+
+ def get_before(self):
+ b = self.xml.find("{%s}before" % (self.namespace))
+ if b is not None and not b.text:
+ return True
+ elif b is not None:
+ return b.text
+ else:
+ return None
diff --git a/slixmpp/plugins/xep_0060/__init__.py b/slixmpp/plugins/xep_0060/__init__.py
new file mode 100644
index 00000000..6c4d8428
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0060.pubsub import XEP_0060
+from slixmpp.plugins.xep_0060 import stanza
+
+
+register_plugin(XEP_0060)
+
+
+# Retain some backwards compatibility
+xep_0060 = XEP_0060
diff --git a/slixmpp/plugins/xep_0060/pubsub.py b/slixmpp/plugins/xep_0060/pubsub.py
new file mode 100644
index 00000000..8e12ae92
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/pubsub.py
@@ -0,0 +1,566 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.xmlstream import JID
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0060 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0060(BasePlugin):
+
+ """
+ XEP-0060 Publish Subscribe
+ """
+
+ name = 'xep_0060'
+ description = 'XEP-0060: Publish-Subscribe'
+ dependencies = set(['xep_0030', 'xep_0004', 'xep_0082', 'xep_0131'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.node_event_map = {}
+
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Items',
+ StanzaPath('message/pubsub_event/items'),
+ self._handle_event_items))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Purge',
+ StanzaPath('message/pubsub_event/purge'),
+ self._handle_event_purge))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Delete',
+ StanzaPath('message/pubsub_event/delete'),
+ self._handle_event_delete))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Configuration',
+ StanzaPath('message/pubsub_event/configuration'),
+ self._handle_event_configuration))
+ self.xmpp.register_handler(
+ Callback('Pubsub Event: Subscription',
+ StanzaPath('message/pubsub_event/subscription'),
+ self._handle_event_subscription))
+
+ self.xmpp['xep_0131'].supported_headers.add('SubID')
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Pubsub Event: Items')
+ self.xmpp.remove_handler('Pubsub Event: Purge')
+ self.xmpp.remove_handler('Pubsub Event: Delete')
+ self.xmpp.remove_handler('Pubsub Event: Configuration')
+ self.xmpp.remove_handler('Pubsub Event: Subscription')
+
+ def _handle_event_items(self, msg):
+ """Raise events for publish and retraction notifications."""
+ node = msg['pubsub_event']['items']['node']
+
+ multi = len(msg['pubsub_event']['items']) > 1
+ values = {}
+ if multi:
+ values = msg.values
+ del values['pubsub_event']
+
+ for item in msg['pubsub_event']['items']:
+ event_name = self.node_event_map.get(node, None)
+ event_type = 'publish'
+ if item.name == 'retract':
+ event_type = 'retract'
+
+ if multi:
+ condensed = self.xmpp.Message()
+ condensed.values = values
+ condensed['pubsub_event']['items']['node'] = node
+ condensed['pubsub_event']['items'].append(item)
+ self.xmpp.event('pubsub_%s' % event_type, msg)
+ if event_name:
+ self.xmpp.event('%s_%s' % (event_name, event_type),
+ condensed)
+ else:
+ self.xmpp.event('pubsub_%s' % event_type, msg)
+ if event_name:
+ self.xmpp.event('%s_%s' % (event_name, event_type), msg)
+
+ def _handle_event_purge(self, msg):
+ """Raise events for node purge notifications."""
+ node = msg['pubsub_event']['purge']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_purge', msg)
+ if event_name:
+ self.xmpp.event('%s_purge' % event_name, msg)
+
+ def _handle_event_delete(self, msg):
+ """Raise events for node deletion notifications."""
+ node = msg['pubsub_event']['delete']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_delete', msg)
+ if event_name:
+ self.xmpp.event('%s_delete' % event_name, msg)
+
+ def _handle_event_configuration(self, msg):
+ """Raise events for node configuration notifications."""
+ node = msg['pubsub_event']['configuration']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_config', msg)
+ if event_name:
+ self.xmpp.event('%s_config' % event_name, msg)
+
+ def _handle_event_subscription(self, msg):
+ """Raise events for node subscription notifications."""
+ node = msg['pubsub_event']['subscription']['node']
+ event_name = self.node_event_map.get(node, None)
+
+ self.xmpp.event('pubsub_subscription', msg)
+ if event_name:
+ self.xmpp.event('%s_subscription' % event_name, msg)
+
+ def map_node_event(self, node, event_name):
+ """
+ Map node names to events.
+
+ When a pubsub event is received for the given node,
+ raise the provided event.
+
+ For example::
+
+ map_node_event('http://jabber.org/protocol/tune',
+ 'user_tune')
+
+ will produce the events 'user_tune_publish' and 'user_tune_retract'
+ when the respective notifications are received from the node
+ 'http://jabber.org/protocol/tune', among other events.
+
+ Arguments:
+ node -- The node name to map to an event.
+ event_name -- The name of the event to raise when a
+ notification from the given node is received.
+ """
+ self.node_event_map[node] = event_name
+
+ def create_node(self, jid, node, config=None, ntype=None, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ """
+ Create and configure a new pubsub node.
+
+ A server MAY use a different name for the node than the one provided,
+ so be sure to check the result stanza for a server assigned name.
+
+ If no configuration form is provided, the node will be created using
+ the server's default configuration. To get the default configuration
+ use get_node_config().
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- Optional name of the node to create. If no name is
+ provided, the server MAY generate a node ID for you.
+ The server can also assign a different name than the
+ one you provide; check the result stanza to see if
+ the server assigned a name.
+ config -- Optional XEP-0004 data form of configuration settings.
+ ntype -- The type of node to create. Servers typically default
+ to using 'leaf' if no type is provided.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['create']['node'] = node
+
+ if config is not None:
+ form_type = 'http://jabber.org/protocol/pubsub#node_config'
+ if 'FORM_TYPE' in config['fields']:
+ config.field['FORM_TYPE']['value'] = form_type
+ else:
+ config.add_field(var='FORM_TYPE',
+ ftype='hidden',
+ value=form_type)
+ if ntype:
+ if 'pubsub#node_type' in config['fields']:
+ config.field['pubsub#node_type']['value'] = ntype
+ else:
+ config.add_field(var='pubsub#node_type', value=ntype)
+ iq['pubsub']['configure'].append(config)
+
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def subscribe(self, jid, node, bare=True, subscribee=None, options=None,
+ ifrom=None, timeout_callback=None, callback=None,
+ timeout=None):
+ """
+ Subscribe to updates from a pubsub node.
+
+ The rules for determining the JID that is subscribing to the node are:
+ 1. If subscribee is given, use that as provided.
+ 2. If ifrom was given, use the bare or full version based on bare.
+ 3. Otherwise, use self.xmpp.boundjid based on bare.
+
+ Arguments:
+ jid -- The pubsub service JID.
+ node -- The node to subscribe to.
+ bare -- Indicates if the subscribee is a bare or full JID.
+ Defaults to True for a bare JID.
+ subscribee -- The JID that is subscribing to the node.
+ options --
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a
+ response before exiting the send call if blocking
+ is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['subscribe']['node'] = node
+
+ if subscribee is None:
+ if ifrom:
+ if bare:
+ subscribee = JID(ifrom).bare
+ else:
+ subscribee = ifrom
+ else:
+ if bare:
+ subscribee = self.xmpp.boundjid.bare
+ else:
+ subscribee = self.xmpp.boundjid
+
+ iq['pubsub']['subscribe']['jid'] = subscribee
+ if options is not None:
+ iq['pubsub']['options'].append(options)
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None,
+ ifrom=None, timeout_callback=None, callback=None,
+ timeout=None):
+ """
+ Unubscribe from updates from a pubsub node.
+
+ The rules for determining the JID that is unsubscribing
+ from the node are:
+ 1. If subscribee is given, use that as provided.
+ 2. If ifrom was given, use the bare or full version based on bare.
+ 3. Otherwise, use self.xmpp.boundjid based on bare.
+
+ Arguments:
+ jid -- The pubsub service JID.
+ node -- The node to unsubscribe from.
+ subid -- The specific subscription, if multiple subscriptions
+ exist for this JID/node combination.
+ bare -- Indicates if the subscribee is a bare or full JID.
+ Defaults to True for a bare JID.
+ subscribee -- The JID that is unsubscribing from the node.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a
+ response before exiting the send call if blocking
+ is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['unsubscribe']['node'] = node
+
+ if subscribee is None:
+ if ifrom:
+ if bare:
+ subscribee = JID(ifrom).bare
+ else:
+ subscribee = ifrom
+ else:
+ if bare:
+ subscribee = self.xmpp.boundjid.bare
+ else:
+ subscribee = self.xmpp.boundjid
+
+ iq['pubsub']['unsubscribe']['jid'] = subscribee
+ iq['pubsub']['unsubscribe']['subid'] = subid
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_subscriptions(self, jid, node=None, ifrom=None,
+ timeout_callback=None, callback=None,
+ timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['subscriptions']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_affiliations(self, jid, node=None, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['affiliations']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_subscription_options(self, jid, node=None, user_jid=None,
+ ifrom=None, timeout_callback=None,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ if user_jid is None:
+ iq['pubsub']['default']['node'] = node
+ else:
+ iq['pubsub']['options']['node'] = node
+ iq['pubsub']['options']['jid'] = user_jid
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def set_subscription_options(self, jid, node, user_jid, options,
+ ifrom=None, timeout_callback=None,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['options']['node'] = node
+ iq['pubsub']['options']['jid'] = user_jid
+ iq['pubsub']['options'].append(options)
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_node_config(self, jid, node=None, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ """
+ Retrieve the configuration for a node, or the pubsub service's
+ default configuration for new nodes.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to retrieve the configuration for. If None,
+ the default configuration for new nodes will be
+ requested. Defaults to None.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ if node is None:
+ iq['pubsub_owner']['default']
+ else:
+ iq['pubsub_owner']['configure']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_node_subscriptions(self, jid, node, ifrom=None,
+ timeout_callback=None, callback=None,
+ timeout=None):
+ """
+ Retrieve the subscriptions associated with a given node.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to retrieve subscriptions from.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub_owner']['subscriptions']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_node_affiliations(self, jid, node, ifrom=None, timeout_callback=None,
+ callback=None, timeout=None):
+ """
+ Retrieve the affiliations associated with a given node.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to retrieve affiliations from.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub_owner']['affiliations']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def delete_node(self, jid, node, ifrom=None, timeout_callback=None, callback=None,
+ timeout=None):
+ """
+ Delete a a pubsub node.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to delete.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['delete']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def set_node_config(self, jid, node, config, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['configure']['node'] = node
+ iq['pubsub_owner']['configure'].append(config)
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def publish(self, jid, node, id=None, payload=None, options=None,
+ ifrom=None, timeout_callback=None, callback=None,
+ timeout=None):
+ """
+ Add a new item to a node, or edit an existing item.
+
+ For services that support it, you can use the publish command
+ as an event signal by not including an ID or payload.
+
+ When including a payload and you do not provide an ID then
+ the service will generally create an ID for you.
+
+ Publish options may be specified, and how those options
+ are processed is left to the service, such as treating
+ the options as preconditions that the node's settings
+ must match.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to publish the item to.
+ id -- Optionally specify the ID of the item.
+ payload -- The item content to publish.
+ options -- A form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['publish']['node'] = node
+ if id is not None:
+ iq['pubsub']['publish']['item']['id'] = id
+ if payload is not None:
+ iq['pubsub']['publish']['item']['payload'] = payload
+ iq['pubsub']['publish_options'] = options
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def retract(self, jid, node, id, notify=None, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ """
+ Delete a single item from a node.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+
+ iq['pubsub']['retract']['node'] = node
+ iq['pubsub']['retract']['notify'] = notify
+ iq['pubsub']['retract']['item']['id'] = id
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def purge(self, jid, node, ifrom=None, timeout_callback=None, callback=None,
+ timeout=None):
+ """
+ Remove all items from a node.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['purge']['node'] = node
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_nodes(self, *args, **kwargs):
+ """
+ Discover the nodes provided by a Pubsub service, using disco.
+ """
+ return self.xmpp['xep_0030'].get_items(*args, **kwargs)
+
+ def get_item(self, jid, node, item_id, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ """
+ Retrieve the content of an individual item.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ item = stanza.Item()
+ item['id'] = item_id
+ iq['pubsub']['items']['node'] = node
+ iq['pubsub']['items'].append(item)
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_items(self, jid, node, item_ids=None, max_items=None,
+ iterator=False, ifrom=None, timeout_callback=None,
+ callback=None, timeout=None):
+ """
+ Request the contents of a node's items.
+
+ The desired items can be specified, or a query for the last
+ few published items can be used.
+
+ Pubsub services may use result set management for nodes with
+ many items, so an iterator can be returned if needed.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['items']['node'] = node
+ iq['pubsub']['items']['max_items'] = max_items
+
+ if item_ids is not None:
+ for item_id in item_ids:
+ item = stanza.Item()
+ item['id'] = item_id
+ iq['pubsub']['items'].append(item)
+
+ if iterator:
+ return self.xmpp['xep_0059'].iterate(iq, 'pubsub')
+ else:
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def get_item_ids(self, jid, node, ifrom=None, timeout_callback=None, callback=None,
+ timeout=None, iterator=False):
+ """
+ Retrieve the ItemIDs hosted by a given node, using disco.
+ """
+ self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom,
+ callback=callback, timeout=timeout,
+ iterator=iterator,
+ timeout_callback=timeout_callback)
+
+ def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
+ timeout_callback=None, callback=None,
+ timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['affiliations']['node'] = node
+
+ if affiliations is None:
+ affiliations = []
+
+ for jid, affiliation in affiliations:
+ aff = stanza.OwnerAffiliation()
+ aff['jid'] = jid
+ aff['affiliation'] = affiliation
+ iq['pubsub_owner']['affiliations'].append(aff)
+
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
+
+ def modify_subscriptions(self, jid, node, subscriptions=None,
+ ifrom=None, timeout_callback=None,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['subscriptions']['node'] = node
+
+ if subscriptions is None:
+ subscriptions = []
+
+ for jid, subscription in subscriptions:
+ sub = stanza.OwnerSubscription()
+ sub['jid'] = jid
+ sub['subscription'] = subscription
+ iq['pubsub_owner']['subscriptions'].append(sub)
+
+ return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0060/stanza/__init__.py b/slixmpp/plugins/xep_0060/stanza/__init__.py
new file mode 100644
index 00000000..31c4ac68
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/stanza/__init__.py
@@ -0,0 +1,12 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0060.stanza.pubsub import *
+from slixmpp.plugins.xep_0060.stanza.pubsub_owner import *
+from slixmpp.plugins.xep_0060.stanza.pubsub_event import *
+from slixmpp.plugins.xep_0060.stanza.pubsub_errors import *
diff --git a/slixmpp/plugins/xep_0060/stanza/base.py b/slixmpp/plugins/xep_0060/stanza/base.py
new file mode 100644
index 00000000..b8f3d6cc
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/stanza/base.py
@@ -0,0 +1,29 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET
+
+
+class OptionalSetting(object):
+
+ interfaces = set(('required',))
+
+ def set_required(self, value):
+ if value in (True, 'true', 'True', '1'):
+ self.xml.append(ET.Element("{%s}required" % self.namespace))
+ elif self['required']:
+ self.del_required()
+
+ def get_required(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ return required is not None
+
+ def del_required(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ if required is not None:
+ self.xml.remove(required)
diff --git a/slixmpp/plugins/xep_0060/stanza/pubsub.py b/slixmpp/plugins/xep_0060/stanza/pubsub.py
new file mode 100644
index 00000000..b4293918
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/stanza/pubsub.py
@@ -0,0 +1,272 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Iq, Message
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from slixmpp.plugins import xep_0004
+from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting
+
+
+class Pubsub(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'pubsub'
+ plugin_attrib = name
+ interfaces = set(tuple())
+
+
+class Affiliations(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliations'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class Affiliation(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliation'
+ plugin_attrib = name
+ interfaces = set(('node', 'affiliation', 'jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Subscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'subscription', 'subid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Subscriptions(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscriptions'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class SubscribeOptions(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe-options'
+ plugin_attrib = 'suboptions'
+ interfaces = set(('required',))
+
+
+class Item(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'item'
+ plugin_attrib = name
+ interfaces = set(('id', 'payload'))
+
+ def set_payload(self, value):
+ del self['payload']
+ if isinstance(value, ElementBase):
+ if value.tag_name() in self.plugin_tag_map:
+ self.init_plugin(value.plugin_attrib, existing_xml=value.xml)
+ self.xml.append(value.xml)
+ else:
+ self.xml.append(value)
+
+ def get_payload(self):
+ childs = list(self.xml)
+ if len(childs) > 0:
+ return childs[0]
+
+ def del_payload(self):
+ for child in self.xml:
+ self.xml.remove(child)
+
+
+class Items(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'items'
+ plugin_attrib = name
+ interfaces = set(('node', 'max_items'))
+
+ def set_max_items(self, value):
+ self._set_attr('max_items', str(value))
+
+
+class Create(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'create'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class Default(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'default'
+ plugin_attrib = name
+ interfaces = set(('node', 'type'))
+
+ def get_type(self):
+ t = self._get_attr('type')
+ if not t:
+ return 'leaf'
+ return t
+
+
+class Publish(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'publish'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class Retract(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'retract'
+ plugin_attrib = name
+ interfaces = set(('node', 'notify'))
+
+ def get_notify(self):
+ notify = self._get_attr('notify')
+ if notify in ('0', 'false'):
+ return False
+ elif notify in ('1', 'true'):
+ return True
+ return None
+
+ def set_notify(self, value):
+ del self['notify']
+ if value is None:
+ return
+ elif value in (True, '1', 'true', 'True'):
+ self._set_attr('notify', 'true')
+ else:
+ self._set_attr('notify', 'false')
+
+
+class Unsubscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'unsubscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid', 'subid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Subscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Configure(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'configure'
+ plugin_attrib = name
+ interfaces = set(('node', 'type'))
+
+ def getType(self):
+ t = self._get_attr('type')
+ if not t:
+ t == 'leaf'
+ return t
+
+
+class Options(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'options'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'options'))
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def get_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ form = xep_0004.Form(xml=config)
+ return form
+
+ def set_options(self, value):
+ self.xml.append(value.getXML())
+ return self
+
+ def del_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ self.xml.remove(config)
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class PublishOptions(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'publish-options'
+ plugin_attrib = 'publish_options'
+ interfaces = set(('publish_options',))
+ is_extension = True
+
+ def get_publish_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ if config is None:
+ return None
+ form = xep_0004.Form(xml=config)
+ return form
+
+ def set_publish_options(self, value):
+ if value is None:
+ self.del_publish_options()
+ else:
+ self.xml.append(value.getXML())
+ return self
+
+ def del_publish_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ if config is not None:
+ self.xml.remove(config)
+ self.parent().xml.remove(self.xml)
+
+
+register_stanza_plugin(Iq, Pubsub)
+register_stanza_plugin(Pubsub, Affiliations)
+register_stanza_plugin(Pubsub, Configure)
+register_stanza_plugin(Pubsub, Create)
+register_stanza_plugin(Pubsub, Default)
+register_stanza_plugin(Pubsub, Items)
+register_stanza_plugin(Pubsub, Options)
+register_stanza_plugin(Pubsub, Publish)
+register_stanza_plugin(Pubsub, PublishOptions)
+register_stanza_plugin(Pubsub, Retract)
+register_stanza_plugin(Pubsub, Subscribe)
+register_stanza_plugin(Pubsub, Subscription)
+register_stanza_plugin(Pubsub, Subscriptions)
+register_stanza_plugin(Pubsub, Unsubscribe)
+register_stanza_plugin(Affiliations, Affiliation, iterable=True)
+register_stanza_plugin(Configure, xep_0004.Form)
+register_stanza_plugin(Items, Item, iterable=True)
+register_stanza_plugin(Publish, Item, iterable=True)
+register_stanza_plugin(Retract, Item)
+register_stanza_plugin(Subscribe, Options)
+register_stanza_plugin(Subscription, SubscribeOptions)
+register_stanza_plugin(Subscriptions, Subscription, iterable=True)
diff --git a/slixmpp/plugins/xep_0060/stanza/pubsub_errors.py b/slixmpp/plugins/xep_0060/stanza/pubsub_errors.py
new file mode 100644
index 00000000..3e728009
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/stanza/pubsub_errors.py
@@ -0,0 +1,86 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Error
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class PubsubErrorCondition(ElementBase):
+
+ plugin_attrib = 'pubsub'
+ interfaces = set(('condition', 'unsupported'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ conditions = set(('closed-node', 'configuration-required', 'invalid-jid',
+ 'invalid-options', 'invalid-payload', 'invalid-subid',
+ 'item-forbidden', 'item-required', 'jid-required',
+ 'max-items-exceeded', 'max-nodes-exceeded',
+ 'nodeid-required', 'not-in-roster-group',
+ 'not-subscribed', 'payload-too-big',
+ 'payload-required', 'pending-subscription',
+ 'presence-subscription-required', 'subid-required',
+ 'too-many-subscriptions', 'unsupported'))
+ condition_ns = 'http://jabber.org/protocol/pubsub#errors'
+
+ def setup(self, xml):
+ """Don't create XML for the plugin."""
+ self.xml = ET.Element('')
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.parent().xml:
+ if "{%s}" % self.condition_ns in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return ''
+
+ def set_condition(self, value):
+ """
+ Set the tag name of the condition element.
+
+ Arguments:
+ value -- The tag name of the condition element.
+ """
+ if value in self.conditions:
+ del self['condition']
+ cond = ET.Element("{%s}%s" % (self.condition_ns, value))
+ self.parent().xml.append(cond)
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.parent().xml:
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.parent().xml.remove(child)
+ return self
+
+ def get_unsupported(self):
+ """Return the name of an unsupported feature"""
+ xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns)
+ if xml is not None:
+ return xml.attrib.get('feature', '')
+ return ''
+
+ def set_unsupported(self, value):
+ """Mark a feature as unsupported"""
+ self.del_unsupported()
+ xml = ET.Element('{%s}unsupported' % self.condition_ns)
+ xml.attrib['feature'] = value
+ self.parent().xml.append(xml)
+
+ def del_unsupported(self):
+ """Delete an unsupported feature condition."""
+ xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns)
+ if xml is not None:
+ self.parent().xml.remove(xml)
+
+
+register_stanza_plugin(Error, PubsubErrorCondition)
diff --git a/slixmpp/plugins/xep_0060/stanza/pubsub_event.py b/slixmpp/plugins/xep_0060/stanza/pubsub_event.py
new file mode 100644
index 00000000..1ba97c75
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/stanza/pubsub_event.py
@@ -0,0 +1,151 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from slixmpp import Message
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from slixmpp.plugins.xep_0004 import Form
+from slixmpp.plugins import xep_0082
+
+
+class Event(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'event'
+ plugin_attrib = 'pubsub_event'
+ interfaces = set()
+
+
+class EventItem(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'item'
+ plugin_attrib = name
+ interfaces = set(('id', 'payload', 'node', 'publisher'))
+
+ def set_payload(self, value):
+ self.xml.append(value)
+
+ def get_payload(self):
+ childs = list(self.xml)
+ if len(childs) > 0:
+ return childs[0]
+
+ def del_payload(self):
+ for child in self.xml:
+ self.xml.remove(child)
+
+
+class EventRetract(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'retract'
+ plugin_attrib = name
+ interfaces = set(('id',))
+
+
+class EventItems(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'items'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventCollection(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'collection'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventAssociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'associate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventDisassociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'disassociate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventConfiguration(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'configuration'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventPurge(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'purge'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventDelete(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'delete'
+ plugin_attrib = name
+ interfaces = set(('node', 'redirect'))
+
+ def set_redirect(self, uri):
+ del self['redirect']
+ redirect = ET.Element('{%s}redirect' % self.namespace)
+ redirect.attrib['uri'] = uri
+ self.xml.append(redirect)
+
+ def get_redirect(self):
+ redirect = self.xml.find('{%s}redirect' % self.namespace)
+ if redirect is not None:
+ return redirect.attrib.get('uri', '')
+ return ''
+
+ def del_redirect(self):
+ redirect = self.xml.find('{%s}redirect' % self.namespace)
+ if redirect is not None:
+ self.xml.remove(redirect)
+
+
+class EventSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('node', 'expiry', 'jid', 'subid', 'subscription'))
+
+ def get_expiry(self):
+ expiry = self._get_attr('expiry')
+ if expiry.lower() == 'presence':
+ return expiry
+ return xep_0082.parse(expiry)
+
+ def set_expiry(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('expiry', value)
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+register_stanza_plugin(Message, Event)
+register_stanza_plugin(Event, EventCollection)
+register_stanza_plugin(Event, EventConfiguration)
+register_stanza_plugin(Event, EventPurge)
+register_stanza_plugin(Event, EventDelete)
+register_stanza_plugin(Event, EventItems)
+register_stanza_plugin(Event, EventSubscription)
+register_stanza_plugin(EventCollection, EventAssociate)
+register_stanza_plugin(EventCollection, EventDisassociate)
+register_stanza_plugin(EventConfiguration, Form)
+register_stanza_plugin(EventItems, EventItem, iterable=True)
+register_stanza_plugin(EventItems, EventRetract, iterable=True)
diff --git a/slixmpp/plugins/xep_0060/stanza/pubsub_owner.py b/slixmpp/plugins/xep_0060/stanza/pubsub_owner.py
new file mode 100644
index 00000000..402e5e30
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/stanza/pubsub_owner.py
@@ -0,0 +1,134 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Iq
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from slixmpp.plugins.xep_0004 import Form
+from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting
+from slixmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation
+from slixmpp.plugins.xep_0060.stanza.pubsub import Configure, Subscriptions
+
+
+class PubsubOwner(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'pubsub'
+ plugin_attrib = 'pubsub_owner'
+ interfaces = set(tuple())
+
+
+class DefaultConfig(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'default'
+ plugin_attrib = name
+ interfaces = set(('node', 'config'))
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def get_config(self):
+ return self['form']
+
+ def set_config(self, value):
+ del self['from']
+ self.append(value)
+ return self
+
+
+class OwnerAffiliations(Affiliations):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+
+ def append(self, affiliation):
+ if not isinstance(affiliation, OwnerAffiliation):
+ raise TypeError
+ self.xml.append(affiliation.xml)
+
+
+class OwnerAffiliation(Affiliation):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('affiliation', 'jid'))
+
+
+class OwnerConfigure(Configure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'configure'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class OwnerDefault(OwnerConfigure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+
+
+class OwnerDelete(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'delete'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class OwnerPurge(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'purge'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class OwnerRedirect(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'redirect'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class OwnerSubscriptions(Subscriptions):
+ name = 'subscriptions'
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+ def append(self, subscription):
+ if not isinstance(subscription, OwnerSubscription):
+ raise TypeError
+ self.xml.append(subscription.xml)
+
+
+class OwnerSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'subscription'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+register_stanza_plugin(Iq, PubsubOwner)
+register_stanza_plugin(PubsubOwner, DefaultConfig)
+register_stanza_plugin(PubsubOwner, OwnerAffiliations)
+register_stanza_plugin(PubsubOwner, OwnerConfigure)
+register_stanza_plugin(PubsubOwner, OwnerDefault)
+register_stanza_plugin(PubsubOwner, OwnerDelete)
+register_stanza_plugin(PubsubOwner, OwnerPurge)
+register_stanza_plugin(PubsubOwner, OwnerSubscriptions)
+register_stanza_plugin(DefaultConfig, Form)
+register_stanza_plugin(OwnerAffiliations, OwnerAffiliation, iterable=True)
+register_stanza_plugin(OwnerConfigure, Form)
+register_stanza_plugin(OwnerDefault, Form)
+register_stanza_plugin(OwnerDelete, OwnerRedirect)
+register_stanza_plugin(OwnerSubscriptions, OwnerSubscription, iterable=True)
diff --git a/slixmpp/plugins/xep_0065/__init__.py b/slixmpp/plugins/xep_0065/__init__.py
new file mode 100644
index 00000000..c392bd23
--- /dev/null
+++ b/slixmpp/plugins/xep_0065/__init__.py
@@ -0,0 +1,8 @@
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0065.socks5 import Socks5Protocol
+from slixmpp.plugins.xep_0065.stanza import Socks5
+from slixmpp.plugins.xep_0065.proxy import XEP_0065
+
+
+register_plugin(XEP_0065)
diff --git a/slixmpp/plugins/xep_0065/proxy.py b/slixmpp/plugins/xep_0065/proxy.py
new file mode 100644
index 00000000..c5d358dd
--- /dev/null
+++ b/slixmpp/plugins/xep_0065/proxy.py
@@ -0,0 +1,279 @@
+import asyncio
+import logging
+import socket
+
+from hashlib import sha1
+from uuid import uuid4
+
+from slixmpp.stanza import Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.base import BasePlugin
+
+from slixmpp.plugins.xep_0065 import stanza, Socks5, Socks5Protocol
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0065(BasePlugin):
+
+ name = 'xep_0065'
+ description = "XEP-0065: SOCKS5 Bytestreams"
+ dependencies = set(['xep_0030'])
+ default_config = {
+ 'auto_accept': False
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Socks5)
+
+ self._proxies = {}
+ self._sessions = {}
+ self._preauthed_sids = {}
+
+ self.xmpp.register_handler(
+ Callback('Socks5 Bytestreams',
+ StanzaPath('iq@type=set/socks/streamhost'),
+ self._handle_streamhost))
+
+ self.api.register(self._authorized, 'authorized', default=True)
+ self.api.register(self._authorized_sid, 'authorized_sid', default=True)
+ self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Socks5.namespace)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Socks5 Bytestreams')
+ self.xmpp.remove_handler('Socks5 Streamhost Used')
+ self.xmpp['xep_0030'].del_feature(feature=Socks5.namespace)
+
+ def get_socket(self, sid):
+ """Returns the socket associated to the SID."""
+ return self._sessions.get(sid, None)
+
+ def handshake(self, to, ifrom=None, sid=None, timeout=None):
+ """ Starts the handshake to establish the socks5 bytestreams
+ connection.
+ """
+ if not self._proxies:
+ self._proxies = yield from self.discover_proxies()
+
+ if sid is None:
+ sid = uuid4().hex
+
+ used = yield from self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout)
+ proxy = used['socks']['streamhost_used']['jid']
+
+ if proxy not in self._proxies:
+ log.warning('Received unknown SOCKS5 proxy: %s', proxy)
+ return
+
+ try:
+ self._sessions[sid] = (yield from self._connect_proxy(
+ self._get_dest_sha1(sid, self.xmpp.boundjid, to),
+ self._proxies[proxy][0],
+ self._proxies[proxy][1]))[1]
+ except socket.error:
+ return None
+ addr, port = yield from self._sessions[sid].connected
+
+ # Request that the proxy activate the session with the target.
+ yield from self.activate(proxy, sid, to, timeout=timeout)
+ sock = self.get_socket(sid)
+ self.xmpp.event('stream:%s:%s' % (sid, to), sock)
+ return sock
+
+ def request_stream(self, to, sid=None, ifrom=None, timeout=None, callback=None):
+ if sid is None:
+ sid = uuid4().hex
+
+ # Requester initiates S5B negotiation with Target by sending
+ # IQ-set that includes the JabberID and network address of
+ # StreamHost as well as the StreamID (SID) of the proposed
+ # bytestream.
+ iq = self.xmpp.Iq()
+ iq['to'] = to
+ iq['from'] = ifrom
+ iq['type'] = 'set'
+ iq['socks']['sid'] = sid
+ for proxy, (host, port) in self._proxies.items():
+ iq['socks'].add_streamhost(proxy, host, port)
+ return iq.send(timeout=timeout, callback=callback)
+
+ def discover_proxies(self, jid=None, ifrom=None, timeout=None):
+ """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server."""
+ if jid is None:
+ if self.xmpp.is_component:
+ jid = self.xmpp.server
+ else:
+ jid = self.xmpp.boundjid.server
+
+ discovered = set()
+
+ disco_items = yield from self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
+ disco_items = {item[0] for item in disco_items['disco_items']['items']}
+
+ disco_info_futures = {}
+ for item in disco_items:
+ disco_info_futures[item] = self.xmpp['xep_0030'].get_info(item, timeout=timeout)
+
+ for item in disco_items:
+ try:
+ disco_info = yield from disco_info_futures[item]
+ except XMPPError:
+ continue
+ else:
+ # Verify that the identity is a bytestream proxy.
+ identities = disco_info['disco_info']['identities']
+ for identity in identities:
+ if identity[0] == 'proxy' and identity[1] == 'bytestreams':
+ discovered.add(disco_info['from'])
+
+ for jid in discovered:
+ try:
+ addr = yield from self.get_network_address(jid, ifrom=ifrom, timeout=timeout)
+ self._proxies[jid] = (addr['socks']['streamhost']['host'],
+ addr['socks']['streamhost']['port'])
+ except XMPPError:
+ continue
+
+ return self._proxies
+
+ def get_network_address(self, proxy, ifrom=None, timeout=None, callback=None):
+ """Get the network information of a proxy."""
+ iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom)
+ iq.enable('socks')
+ return iq.send(timeout=timeout, callback=callback)
+
+ def _get_dest_sha1(self, sid, requester, target):
+ # The hostname MUST be SHA1(SID + Requester JID + Target JID)
+ # where the output is hexadecimal-encoded (not binary).
+ digest = sha1()
+ digest.update(sid.encode('utf8'))
+ digest.update(str(requester).encode('utf8'))
+ digest.update(str(target).encode('utf8'))
+ return digest.hexdigest()
+
+ def _handle_streamhost(self, iq):
+ """Handle incoming SOCKS5 session request."""
+ sid = iq['socks']['sid']
+ if not sid:
+ raise XMPPError(etype='modify', condition='bad-request')
+
+ if not self._accept_stream(iq):
+ raise XMPPError(etype='modify', condition='not-acceptable')
+
+ streamhosts = iq['socks']['streamhosts']
+ requester = iq['from']
+ target = iq['to']
+
+ dest = self._get_dest_sha1(sid, requester, target)
+
+ proxy_futures = []
+ for streamhost in streamhosts:
+ proxy_futures.append(self._connect_proxy(
+ dest,
+ streamhost['host'],
+ streamhost['port']))
+
+ @asyncio.coroutine
+ def gather(futures, iq, streamhosts):
+ proxies = yield from asyncio.gather(*futures, return_exceptions=True)
+ for streamhost, proxy in zip(streamhosts, proxies):
+ if isinstance(proxy, ValueError):
+ continue
+ elif isinstance(proxy, socket.error):
+ log.error('Socket error while connecting to the proxy.')
+ continue
+ proxy = proxy[1]
+ # TODO: what if the future never happens?
+ try:
+ addr, port = yield from proxy.connected
+ except socket.error:
+ log.exception('Socket error while connecting to the proxy.')
+ continue
+ # TODO: make a better choice than just the first working one.
+ used_streamhost = streamhost['jid']
+ conn = proxy
+ break
+ else:
+ raise XMPPError(etype='cancel', condition='item-not-found')
+
+ # TODO: close properly the connection to the other proxies.
+
+ iq = iq.reply()
+ self._sessions[sid] = conn
+ iq['socks']['sid'] = sid
+ iq['socks']['streamhost_used']['jid'] = used_streamhost
+ iq.send()
+ self.xmpp.event('socks5_stream', conn)
+ self.xmpp.event('stream:%s:%s' % (sid, requester), conn)
+
+ asyncio.async(gather(proxy_futures, iq, streamhosts))
+
+ def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None):
+ """Activate the socks5 session that has been negotiated."""
+ iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom)
+ iq['socks']['sid'] = sid
+ iq['socks']['activate'] = target
+ return iq.send(timeout=timeout, callback=callback)
+
+ def deactivate(self, sid):
+ """Closes the proxy socket associated with this SID."""
+ sock = self._sessions.get(sid)
+ if sock:
+ try:
+ # sock.close() will also delete sid from self._sessions (see _connect_proxy)
+ sock.close()
+ except socket.error:
+ pass
+ # Though this should not be neccessary remove the closed session anyway
+ if sid in self._sessions:
+ log.warn(('SOCKS5 session with sid = "%s" was not ' +
+ 'removed from _sessions by sock.close()') % sid)
+ del self._sessions[sid]
+
+ def close(self):
+ """Closes all proxy sockets."""
+ for sid, sock in self._sessions.items():
+ sock.close()
+ self._sessions = {}
+
+ def _connect_proxy(self, dest, proxy, proxy_port):
+ """ Returns a future to a connection between the client and the server-side
+ Socks5 proxy.
+
+ dest : The SHA-1 of (SID + Requester JID + Target JID), in hex. <str>
+ host : The hostname or the IP of the proxy. <str>
+ port : The port of the proxy. <str> or <int>
+ """
+ factory = lambda: Socks5Protocol(dest, 0, self.xmpp.event)
+ return self.xmpp.loop.create_connection(factory, proxy, proxy_port)
+
+ def _accept_stream(self, iq):
+ receiver = iq['to']
+ sender = iq['from']
+ sid = iq['socks']['sid']
+
+ if self.api['authorized_sid'](receiver, sid, sender, iq):
+ return True
+ return self.api['authorized'](receiver, sid, sender, iq)
+
+ def _authorized(self, jid, sid, ifrom, iq):
+ return self.auto_accept
+
+ def _authorized_sid(self, jid, sid, ifrom, iq):
+ log.debug('>>> authed sids: %s', self._preauthed_sids)
+ log.debug('>>> lookup: %s %s %s', jid, sid, ifrom)
+ if (jid, sid, ifrom) in self._preauthed_sids:
+ del self._preauthed_sids[(jid, sid, ifrom)]
+ return True
+ return False
+
+ def _preauthorize_sid(self, jid, sid, ifrom, data):
+ log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data)
+ self._preauthed_sids[(jid, sid, ifrom)] = True
diff --git a/slixmpp/plugins/xep_0065/socks5.py b/slixmpp/plugins/xep_0065/socks5.py
new file mode 100644
index 00000000..54267b32
--- /dev/null
+++ b/slixmpp/plugins/xep_0065/socks5.py
@@ -0,0 +1,265 @@
+'''Pure asyncio implementation of RFC 1928 - SOCKS Protocol Version 5.'''
+
+import asyncio
+import enum
+import logging
+import socket
+import struct
+
+from slixmpp.stringprep import punycode, StringprepError
+
+
+log = logging.getLogger(__name__)
+
+
+class ProtocolMismatch(Exception):
+ '''We only implement SOCKS5, no other version or protocol.'''
+
+
+class ProtocolError(Exception):
+ '''Some protocol error.'''
+
+
+class MethodMismatch(Exception):
+ '''The server answered with a method we didn’t ask for.'''
+
+
+class MethodUnacceptable(Exception):
+ '''None of our methods is supported by the server.'''
+
+
+class AddressTypeUnacceptable(Exception):
+ '''The address type (ATYP) field isn’t one of IPv4, IPv6 or domain name.'''
+
+
+class ReplyError(Exception):
+ '''The server answered with an error.'''
+
+ possible_values = (
+ "succeeded",
+ "general SOCKS server failure",
+ "connection not allowed by ruleset",
+ "Network unreachable",
+ "Host unreachable",
+ "Connection refused",
+ "TTL expired",
+ "Command not supported",
+ "Address type not supported",
+ "Unknown error")
+
+ def __init__(self, result):
+ if result < 9:
+ Exception.__init__(self, self.possible_values[result])
+ else:
+ Exception.__init__(self, self.possible_values[9])
+
+
+class Method(enum.IntEnum):
+ '''Known methods for a SOCKS5 session.'''
+ none = 0
+ gssapi = 1
+ password = 2
+ # Methods 3 to 127 are reserved by IANA.
+ # Methods 128 to 254 are reserved for private use.
+ unacceptable = 255
+ not_yet_selected = -1
+
+
+class Command(enum.IntEnum):
+ '''Existing commands for requests.'''
+ connect = 1
+ bind = 2
+ udp_associate = 3
+
+
+class AddressType(enum.IntEnum):
+ '''Existing address types.'''
+ ipv4 = 1
+ domain = 3
+ ipv6 = 4
+
+
+class Socks5Protocol(asyncio.Protocol):
+ '''This implements SOCKS5 as an asyncio protocol.'''
+
+ def __init__(self, dest_addr, dest_port, event):
+ self.methods = {Method.none}
+ self.selected_method = Method.not_yet_selected
+ self.transport = None
+ self.dest = (dest_addr, dest_port)
+ self.connected = asyncio.Future()
+ self.event = event
+ self.paused = asyncio.Future()
+ self.paused.set_result(None)
+
+ def register_method(self, method):
+ '''Register a SOCKS5 method.'''
+ self.methods.add(method)
+
+ def unregister_method(self, method):
+ '''Unregister a SOCKS5 method.'''
+ self.methods.remove(method)
+
+ def connection_made(self, transport):
+ '''Called when the connection to the SOCKS5 server is established.'''
+
+ log.debug('SOCKS5 connection established.')
+
+ self.transport = transport
+ self._send_methods()
+
+ def data_received(self, data):
+ '''Called when we received some data from the SOCKS5 server.'''
+
+ log.debug('SOCKS5 message received.')
+
+ # If we are already connected, this is a data packet.
+ if self.connected.done():
+ return self.event('socks5_data', data)
+
+ # Every SOCKS5 message starts with the protocol version.
+ if data[0] != 5:
+ raise ProtocolMismatch()
+
+ # Then select the correct handler for the data we just received.
+ if self.selected_method == Method.not_yet_selected:
+ self._handle_method(data)
+ else:
+ self._handle_connect(data)
+
+ def connection_lost(self, exc):
+ log.debug('SOCKS5 connection closed.')
+ self.event('socks5_closed', exc)
+
+ def pause_writing(self):
+ self.paused = asyncio.Future()
+
+ def resume_writing(self):
+ self.paused.set_result(None)
+
+ def write(self, data):
+ yield from self.paused
+ self.transport.write(data)
+
+ def _send_methods(self):
+ '''Send the methods request, first thing a client should do.'''
+
+ # Create the buffer for our request.
+ request = bytearray(len(self.methods) + 2)
+
+ # Protocol version.
+ request[0] = 5
+
+ # Number of methods to send.
+ request[1] = len(self.methods)
+
+ # List every method we support.
+ for i, method in enumerate(self.methods):
+ request[i + 2] = method
+
+ # Send the request.
+ self.transport.write(request)
+
+ def _send_request(self, command):
+ '''Send a request, should be done after having negociated a method.'''
+
+ # Encode the destination address to embed it in our request.
+ # We need to do that first because its length is variable.
+ address, port = self.dest
+ addr = self._encode_addr(address)
+
+ # Create the buffer for our request.
+ request = bytearray(5 + len(addr))
+
+ # Protocol version.
+ request[0] = 5
+
+ # Specify the command we want to use.
+ request[1] = command
+
+ # request[2] is reserved, keeping it at 0.
+
+ # Add our destination address and port.
+ request[3:3+len(addr)] = addr
+ request[-2:] = struct.pack('>H', port)
+
+ # Send the request.
+ log.debug('SOCKS5 message sent.')
+ self.transport.write(request)
+
+ def _handle_method(self, data):
+ '''Handle a method reply from the server.'''
+
+ if len(data) != 2:
+ raise ProtocolError()
+ selected_method = data[1]
+ if selected_method not in self.methods:
+ raise MethodMismatch()
+ if selected_method == Method.unacceptable:
+ raise MethodUnacceptable()
+ self.selected_method = selected_method
+ self._send_request(Command.connect)
+
+ def _handle_connect(self, data):
+ '''Handle a connect reply from the server.'''
+
+ try:
+ addr, port = self._parse_result(data)
+ except ReplyError as exception:
+ self.connected.set_exception(exception)
+ self.connected.set_result((addr, port))
+ self.event('socks5_connected', (addr, port))
+
+ def _parse_result(self, data):
+ '''Parse a reply from the server.'''
+
+ result = data[1]
+ if result != 0:
+ raise ReplyError(result)
+ addr = self._parse_addr(data[3:-2])
+ port = struct.unpack('>H', data[-2:])[0]
+ return (addr, port)
+
+ @staticmethod
+ def _parse_addr(addr):
+ '''Parse an address (IP or domain) from a bytestream.'''
+
+ addr_type = addr[0]
+ if addr_type == AddressType.ipv6:
+ try:
+ return socket.inet_ntop(socket.AF_INET6, addr[1:])
+ except ValueError as e:
+ raise AddressTypeUnacceptable(e)
+ if addr_type == AddressType.ipv4:
+ try:
+ return socket.inet_ntop(socket.AF_INET, addr[1:])
+ except ValueError as e:
+ raise AddressTypeUnacceptable(e)
+ if addr_type == AddressType.domain:
+ length = addr[1]
+ address = addr[2:]
+ if length != len(address):
+ raise Exception('Size mismatch')
+ return address.decode()
+ raise AddressTypeUnacceptable(addr_type)
+
+ @staticmethod
+ def _encode_addr(addr):
+ '''Encode an address (IP or domain) into a bytestream.'''
+
+ try:
+ ipv6 = socket.inet_pton(socket.AF_INET6, addr)
+ return b'\x04' + ipv6
+ except OSError:
+ pass
+ try:
+ ipv4 = socket.inet_aton(addr)
+ return b'\x01' + ipv4
+ except OSError:
+ pass
+ try:
+ domain = punycode(addr)
+ return b'\x03' + bytes([len(domain)]) + domain
+ except StringprepError:
+ pass
+ raise Exception('Err…')
diff --git a/slixmpp/plugins/xep_0065/stanza.py b/slixmpp/plugins/xep_0065/stanza.py
new file mode 100644
index 00000000..5ba15b32
--- /dev/null
+++ b/slixmpp/plugins/xep_0065/stanza.py
@@ -0,0 +1,47 @@
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class Socks5(ElementBase):
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/bytestreams'
+ plugin_attrib = 'socks'
+ interfaces = set(['sid', 'activate'])
+ sub_interfaces = set(['activate'])
+
+ def add_streamhost(self, jid, host, port):
+ sh = StreamHost(parent=self)
+ sh['jid'] = jid
+ sh['host'] = host
+ sh['port'] = port
+
+
+class StreamHost(ElementBase):
+ name = 'streamhost'
+ namespace = 'http://jabber.org/protocol/bytestreams'
+ plugin_attrib = 'streamhost'
+ plugin_multi_attrib = 'streamhosts'
+ interfaces = set(['host', 'jid', 'port'])
+
+ def set_jid(self, value):
+ return self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class StreamHostUsed(ElementBase):
+ name = 'streamhost-used'
+ namespace = 'http://jabber.org/protocol/bytestreams'
+ plugin_attrib = 'streamhost_used'
+ interfaces = set(['jid'])
+
+ def set_jid(self, value):
+ return self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+register_stanza_plugin(Socks5, StreamHost, iterable=True)
+register_stanza_plugin(Socks5, StreamHostUsed)
diff --git a/slixmpp/plugins/xep_0066/__init__.py b/slixmpp/plugins/xep_0066/__init__.py
new file mode 100644
index 00000000..7f7e0ebd
--- /dev/null
+++ b/slixmpp/plugins/xep_0066/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0066 import stanza
+from slixmpp.plugins.xep_0066.stanza import OOB, OOBTransfer
+from slixmpp.plugins.xep_0066.oob import XEP_0066
+
+
+register_plugin(XEP_0066)
+
+
+# Retain some backwards compatibility
+xep_0066 = XEP_0066
diff --git a/slixmpp/plugins/xep_0066/oob.py b/slixmpp/plugins/xep_0066/oob.py
new file mode 100644
index 00000000..c9d4ae5b
--- /dev/null
+++ b/slixmpp/plugins/xep_0066/oob.py
@@ -0,0 +1,158 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import Message, Presence, Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0066 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0066(BasePlugin):
+
+ """
+ XEP-0066: Out of Band Data
+
+ Out of Band Data is a basic method for transferring files between
+ XMPP agents. The URL of the resource in question is sent to the receiving
+ entity, which then downloads the resource before responding to the OOB
+ request. OOB is also used as a generic means to transmit URLs in other
+ stanzas to indicate where to find additional information.
+
+ Also see <http://www.xmpp.org/extensions/xep-0066.html>.
+
+ Events:
+ oob_transfer -- Raised when a request to download a resource
+ has been received.
+
+ Methods:
+ send_oob -- Send a request to another entity to download a file
+ or other addressable resource.
+ """
+
+ name = 'xep_0066'
+ description = 'XEP-0066: Out of Band Data'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ """Start the XEP-0066 plugin."""
+
+ self.url_handlers = {'global': self._default_handler,
+ 'jid': {}}
+
+ register_stanza_plugin(Iq, stanza.OOBTransfer)
+ register_stanza_plugin(Message, stanza.OOB)
+ register_stanza_plugin(Presence, stanza.OOB)
+
+ self.xmpp.register_handler(
+ Callback('OOB Transfer',
+ StanzaPath('iq@type=set/oob_transfer'),
+ self._handle_transfer))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('OOB Transfer')
+ self.xmpp['xep_0030'].del_feature(feature=stanza.OOBTransfer.namespace)
+ self.xmpp['xep_0030'].del_feature(feature=stanza.OOB.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace)
+ self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace)
+
+ def register_url_handler(self, jid=None, handler=None):
+ """
+ Register a handler to process download requests, either for all
+ JIDs or a single JID.
+
+ Arguments:
+ jid -- If None, then set the handler as a global default.
+ handler -- If None, then remove the existing handler for the
+ given JID, or reset the global handler if the JID
+ is None.
+ """
+ if jid is None:
+ if handler is not None:
+ self.url_handlers['global'] = handler
+ else:
+ self.url_handlers['global'] = self._default_handler
+ else:
+ if handler is not None:
+ self.url_handlers['jid'][jid] = handler
+ else:
+ del self.url_handlers['jid'][jid]
+
+ def send_oob(self, to, url, desc=None, ifrom=None, **iqargs):
+ """
+ Initiate a basic file transfer by sending the URL of
+ a file or other resource.
+
+ Arguments:
+ url -- The URL of the resource to transfer.
+ desc -- An optional human readable description of the item
+ that is to be transferred.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = to
+ iq['from'] = ifrom
+ iq['oob_transfer']['url'] = url
+ iq['oob_transfer']['desc'] = desc
+ return iq.send(**iqargs)
+
+ def _run_url_handler(self, iq):
+ """
+ Execute the appropriate handler for a transfer request.
+
+ Arguments:
+ iq -- The Iq stanza containing the OOB transfer request.
+ """
+ if iq['to'] in self.url_handlers['jid']:
+ return self.url_handlers['jid'][iq['to']](iq)
+ else:
+ if self.url_handlers['global']:
+ self.url_handlers['global'](iq)
+ else:
+ raise XMPPError('service-unavailable')
+
+ def _default_handler(self, iq):
+ """
+ As a safe default, don't actually download files.
+
+ Register a new handler using self.register_url_handler to
+ screen requests and download files.
+
+ Arguments:
+ iq -- The Iq stanza containing the OOB transfer request.
+ """
+ raise XMPPError('service-unavailable')
+
+ def _handle_transfer(self, iq):
+ """
+ Handle receiving an out-of-band transfer request.
+
+ Arguments:
+ iq -- An Iq stanza containing an OOB transfer request.
+ """
+ log.debug('Received out-of-band data request for %s from %s:' % (
+ iq['oob_transfer']['url'], iq['from']))
+ self._run_url_handler(iq)
+ iq.reply().send()
diff --git a/slixmpp/plugins/xep_0066/stanza.py b/slixmpp/plugins/xep_0066/stanza.py
new file mode 100644
index 00000000..e1da5bdd
--- /dev/null
+++ b/slixmpp/plugins/xep_0066/stanza.py
@@ -0,0 +1,33 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class OOBTransfer(ElementBase):
+
+ """
+ """
+
+ name = 'query'
+ namespace = 'jabber:iq:oob'
+ plugin_attrib = 'oob_transfer'
+ interfaces = set(('url', 'desc', 'sid'))
+ sub_interfaces = set(('url', 'desc'))
+
+
+class OOB(ElementBase):
+
+ """
+ """
+
+ name = 'x'
+ namespace = 'jabber:x:oob'
+ plugin_attrib = 'oob'
+ interfaces = set(('url', 'desc'))
+ sub_interfaces = interfaces
diff --git a/slixmpp/plugins/xep_0071/__init__.py b/slixmpp/plugins/xep_0071/__init__.py
new file mode 100644
index 00000000..19275c7a
--- /dev/null
+++ b/slixmpp/plugins/xep_0071/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0071.stanza import XHTML_IM
+from slixmpp.plugins.xep_0071.xhtml_im import XEP_0071
+
+
+register_plugin(XEP_0071)
diff --git a/slixmpp/plugins/xep_0071/stanza.py b/slixmpp/plugins/xep_0071/stanza.py
new file mode 100644
index 00000000..3df686cf
--- /dev/null
+++ b/slixmpp/plugins/xep_0071/stanza.py
@@ -0,0 +1,81 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Message
+from slixmpp.util import unicode
+from collections import OrderedDict
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, tostring
+
+
+XHTML_NS = 'http://www.w3.org/1999/xhtml'
+
+
+class XHTML_IM(ElementBase):
+
+ namespace = 'http://jabber.org/protocol/xhtml-im'
+ name = 'html'
+ interfaces = set(['body'])
+ lang_interfaces = set(['body'])
+ plugin_attrib = name
+
+ def set_body(self, content, lang=None):
+ if lang is None:
+ lang = self.get_lang()
+ self.del_body(lang)
+ if lang == '*':
+ for sublang, subcontent in content.items():
+ self.set_body(subcontent, sublang)
+ else:
+ if isinstance(content, type(ET.Element('test'))):
+ content = unicode(ET.tostring(content))
+ else:
+ content = unicode(content)
+ header = '<body xmlns="%s"' % XHTML_NS
+ if lang:
+ header = '%s xml:lang="%s"' % (header, lang)
+ content = '%s>%s</body>' % (header, content)
+ xhtml = ET.fromstring(content)
+ self.xml.append(xhtml)
+
+ def get_body(self, lang=None):
+ """Return the contents of the HTML body."""
+ if lang is None:
+ lang = self.get_lang()
+
+ bodies = self.xml.findall('{%s}body' % XHTML_NS)
+
+ if lang == '*':
+ result = OrderedDict()
+ for body in bodies:
+ body_lang = body.attrib.get('{%s}lang' % self.xml_ns, '')
+ body_result = []
+ body_result.append(body.text if body.text else '')
+ for child in body:
+ body_result.append(tostring(child, xmlns=XHTML_NS))
+ body_result.append(body.tail if body.tail else '')
+ result[body_lang] = ''.join(body_result)
+ return result
+ else:
+ for body in bodies:
+ if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang:
+ result = []
+ result.append(body.text if body.text else '')
+ for child in body:
+ result.append(tostring(child, xmlns=XHTML_NS))
+ result.append(body.tail if body.tail else '')
+ return ''.join(result)
+ return ''
+
+ def del_body(self, lang=None):
+ if lang is None:
+ lang = self.get_lang()
+ bodies = self.xml.findall('{%s}body' % XHTML_NS)
+ for body in bodies:
+ if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang:
+ self.xml.remove(body)
+ return
diff --git a/slixmpp/plugins/xep_0071/xhtml_im.py b/slixmpp/plugins/xep_0071/xhtml_im.py
new file mode 100644
index 00000000..0b412126
--- /dev/null
+++ b/slixmpp/plugins/xep_0071/xhtml_im.py
@@ -0,0 +1,30 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.stanza import Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0071 import stanza, XHTML_IM
+
+
+class XEP_0071(BasePlugin):
+
+ name = 'xep_0071'
+ description = 'XEP-0071: XHTML-IM'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, XHTML_IM)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(feature=XHTML_IM.namespace)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=XHTML_IM.namespace)
diff --git a/slixmpp/plugins/xep_0077/__init__.py b/slixmpp/plugins/xep_0077/__init__.py
new file mode 100644
index 00000000..a73cfae9
--- /dev/null
+++ b/slixmpp/plugins/xep_0077/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0077.stanza import Register, RegisterFeature
+from slixmpp.plugins.xep_0077.register import XEP_0077
+
+
+register_plugin(XEP_0077)
+
+
+# Retain some backwards compatibility
+xep_0077 = XEP_0077
diff --git a/slixmpp/plugins/xep_0077/register.py b/slixmpp/plugins/xep_0077/register.py
new file mode 100644
index 00000000..eb2e7443
--- /dev/null
+++ b/slixmpp/plugins/xep_0077/register.py
@@ -0,0 +1,114 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import ssl
+
+from slixmpp.stanza import StreamFeatures, Iq
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0077 import stanza, Register, RegisterFeature
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0077(BasePlugin):
+
+ """
+ XEP-0077: In-Band Registration
+ """
+
+ name = 'xep_0077'
+ description = 'XEP-0077: In-Band Registration'
+ dependencies = set(['xep_0004', 'xep_0066'])
+ stanza = stanza
+ default_config = {
+ 'create_account': True,
+ 'force_registration': False,
+ 'order': 50
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(StreamFeatures, RegisterFeature)
+ register_stanza_plugin(Iq, Register)
+
+ if not self.xmpp.is_component:
+ self.xmpp.register_feature('register',
+ self._handle_register_feature,
+ restart=False,
+ order=self.order)
+
+ register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form)
+ register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB)
+
+ self.xmpp.add_event_handler('connected', self._force_registration)
+
+ def plugin_end(self):
+ if not self.xmpp.is_component:
+ self.xmpp.unregister_feature('register', self.order)
+
+ def _force_registration(self, event):
+ if self.force_registration:
+ self.xmpp.add_filter('in', self._force_stream_feature)
+
+ def _force_stream_feature(self, stanza):
+ if isinstance(stanza, StreamFeatures):
+ if self.xmpp.use_tls or self.xmpp.use_ssl:
+ if 'starttls' not in self.xmpp.features:
+ return stanza
+ elif not isinstance(self.xmpp.socket, ssl.SSLSocket):
+ return stanza
+ if 'mechanisms' not in self.xmpp.features:
+ log.debug('Forced adding in-band registration stream feature')
+ stanza.enable('register')
+ self.xmpp.del_filter('in', self._force_stream_feature)
+ return stanza
+
+ def _handle_register_feature(self, features):
+ if 'mechanisms' in self.xmpp.features:
+ # We have already logged in with an account
+ return False
+
+ if self.create_account and self.xmpp.event_handled('register'):
+ form = self.get_registration()
+ self.xmpp.event('register', form)
+ return True
+ return False
+
+ def get_registration(self, jid=None, ifrom=None,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq.enable('register')
+ return iq.send(timeout=timeout, callback=callback)
+
+ def cancel_registration(self, jid=None, ifrom=None,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['register']['remove'] = True
+ return iq.send(timeout=timeout, callback=callback)
+
+ def change_password(self, password, jid=None, ifrom=None,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ if self.xmpp.is_component:
+ ifrom = JID(ifrom)
+ iq['register']['username'] = ifrom.user
+ else:
+ iq['register']['username'] = self.xmpp.boundjid.user
+ iq['register']['password'] = password
+ return iq.send(timeout=timeout, callback=callback)
diff --git a/slixmpp/plugins/xep_0077/stanza.py b/slixmpp/plugins/xep_0077/stanza.py
new file mode 100644
index 00000000..6ac543c2
--- /dev/null
+++ b/slixmpp/plugins/xep_0077/stanza.py
@@ -0,0 +1,73 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Register(ElementBase):
+
+ namespace = 'jabber:iq:register'
+ name = 'query'
+ plugin_attrib = 'register'
+ interfaces = set(('username', 'password', 'email', 'nick', 'name',
+ 'first', 'last', 'address', 'city', 'state', 'zip',
+ 'phone', 'url', 'date', 'misc', 'text', 'key',
+ 'registered', 'remove', 'instructions', 'fields'))
+ sub_interfaces = interfaces
+ form_fields = set(('username', 'password', 'email', 'nick', 'name',
+ 'first', 'last', 'address', 'city', 'state', 'zip',
+ 'phone', 'url', 'date', 'misc', 'text', 'key'))
+
+ def get_registered(self):
+ present = self.xml.find('{%s}registered' % self.namespace)
+ return present is not None
+
+ def get_remove(self):
+ present = self.xml.find('{%s}remove' % self.namespace)
+ return present is not None
+
+ def set_registered(self, value):
+ if value:
+ self.add_field('registered')
+ else:
+ del self['registered']
+
+ def set_remove(self, value):
+ if value:
+ self.add_field('remove')
+ else:
+ del self['remove']
+
+ def add_field(self, value):
+ self._set_sub_text(value, '', keep=True)
+
+ def get_fields(self):
+ fields = set()
+ for field in self.form_fields:
+ if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
+ fields.add(field)
+ return fields
+
+ def set_fields(self, fields):
+ del self['fields']
+ for field in fields:
+ self._set_sub_text(field, '', keep=True)
+
+ def del_fields(self):
+ for field in self.form_fields:
+ self._del_sub(field)
+
+
+class RegisterFeature(ElementBase):
+
+ name = 'register'
+ namespace = 'http://jabber.org/features/iq-register'
+ plugin_attrib = name
+ interfaces = set()
diff --git a/slixmpp/plugins/xep_0078/__init__.py b/slixmpp/plugins/xep_0078/__init__.py
new file mode 100644
index 00000000..21bdc19e
--- /dev/null
+++ b/slixmpp/plugins/xep_0078/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0078 import stanza
+from slixmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature
+from slixmpp.plugins.xep_0078.legacyauth import XEP_0078
+
+
+register_plugin(XEP_0078)
+
+
+# Retain some backwards compatibility
+xep_0078 = XEP_0078
diff --git a/slixmpp/plugins/xep_0078/legacyauth.py b/slixmpp/plugins/xep_0078/legacyauth.py
new file mode 100644
index 00000000..d949a913
--- /dev/null
+++ b/slixmpp/plugins/xep_0078/legacyauth.py
@@ -0,0 +1,139 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import uuid
+import logging
+import hashlib
+
+from slixmpp.jid import JID
+from slixmpp.exceptions import IqError, IqTimeout
+from slixmpp.stanza import Iq, StreamFeatures
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0078 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0078(BasePlugin):
+
+ """
+ XEP-0078 NON-SASL Authentication
+
+ This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin
+ unless you are forced to use an old XMPP server implementation.
+ """
+
+ name = 'xep_0078'
+ description = 'XEP-0078: Non-SASL Authentication'
+ dependencies = set()
+ stanza = stanza
+ default_config = {
+ 'order': 15
+ }
+
+ def plugin_init(self):
+ self.xmpp.register_feature('auth',
+ self._handle_auth,
+ restart=False,
+ order=self.order)
+
+ self.xmpp.add_event_handler('legacy_protocol',
+ self._handle_legacy_protocol)
+
+ register_stanza_plugin(Iq, stanza.IqAuth)
+ register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
+
+ def plugin_end(self):
+ self.xmpp.del_event_handler('legacy_protocol',
+ self._handle_legacy_protocol)
+ self.xmpp.unregister_feature('auth', self.order)
+
+ def _handle_auth(self, features):
+ # If we can or have already authenticated with SASL, do nothing.
+ if 'mechanisms' in features['features']:
+ return False
+ return self.authenticate()
+
+ def _handle_legacy_protocol(self, event):
+ self.authenticate()
+
+ def authenticate(self):
+ if self.xmpp.authenticated:
+ return False
+
+ log.debug("Starting jabber:iq:auth Authentication")
+
+ # Step 1: Request the auth form
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.requested_jid.host
+ iq['auth']['username'] = self.xmpp.requested_jid.user
+
+ try:
+ resp = iq.send()
+ except IqError as err:
+ log.info("Authentication failed: %s", err.iq['error']['condition'])
+ self.xmpp.event('failed_auth')
+ self.xmpp.disconnect()
+ return True
+ except IqTimeout:
+ log.info("Authentication failed: %s", 'timeout')
+ self.xmpp.event('failed_auth')
+ self.xmpp.disconnect()
+ return True
+
+ # Step 2: Fill out auth form for either password or digest auth
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['auth']['username'] = self.xmpp.requested_jid.user
+
+ # A resource is required, so create a random one if necessary
+ resource = self.xmpp.requested_jid.resource
+ if not resource:
+ resource = str(uuid.uuid4())
+
+ iq['auth']['resource'] = resource
+
+ if 'digest' in resp['auth']['fields']:
+ log.debug('Authenticating via jabber:iq:auth Digest')
+ stream_id = bytes(self.xmpp.stream_id, encoding='utf-8')
+ password = bytes(self.xmpp.password, encoding='utf-8')
+
+ digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest()
+ iq['auth']['digest'] = digest
+ else:
+ log.warning('Authenticating via jabber:iq:auth Plain.')
+ iq['auth']['password'] = self.xmpp.password
+
+ # Step 3: Send credentials
+ try:
+ result = iq.send()
+ except IqError as err:
+ log.info("Authentication failed")
+ self.xmpp.event("failed_auth")
+ self.xmpp.disconnect()
+ except IqTimeout:
+ log.info("Authentication failed")
+ self.xmpp.event("failed_auth")
+ self.xmpp.disconnect()
+
+ self.xmpp.features.add('auth')
+
+ self.xmpp.authenticated = True
+
+ self.xmpp.boundjid = JID(self.xmpp.requested_jid)
+ self.xmpp.boundjid.resource = resource
+ self.xmpp.event('session_bind', self.xmpp.boundjid)
+
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.event('session_start')
+
+ return True
diff --git a/slixmpp/plugins/xep_0078/stanza.py b/slixmpp/plugins/xep_0078/stanza.py
new file mode 100644
index 00000000..7dc9401d
--- /dev/null
+++ b/slixmpp/plugins/xep_0078/stanza.py
@@ -0,0 +1,41 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class IqAuth(ElementBase):
+ namespace = 'jabber:iq:auth'
+ name = 'query'
+ plugin_attrib = 'auth'
+ interfaces = set(('fields', 'username', 'password', 'resource', 'digest'))
+ sub_interfaces = set(('username', 'password', 'resource', 'digest'))
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def get_fields(self):
+ fields = set()
+ for field in self.sub_interfaces:
+ if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
+ fields.add(field)
+ return fields
+
+ def set_resource(self, value):
+ self._set_sub_text('resource', value, keep=True)
+
+ def set_password(self, value):
+ self._set_sub_text('password', value, keep=True)
+
+
+class AuthFeature(ElementBase):
+ namespace = 'http://jabber.org/features/iq-auth'
+ name = 'auth'
+ plugin_attrib = 'auth'
+ interfaces = set()
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
diff --git a/slixmpp/plugins/xep_0079/__init__.py b/slixmpp/plugins/xep_0079/__init__.py
new file mode 100644
index 00000000..864c9018
--- /dev/null
+++ b/slixmpp/plugins/xep_0079/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0079.stanza import (
+ AMP, Rule, InvalidRules, UnsupportedConditions,
+ UnsupportedActions, FailedRules, FailedRule,
+ AMPFeature)
+from slixmpp.plugins.xep_0079.amp import XEP_0079
+
+
+register_plugin(XEP_0079)
diff --git a/slixmpp/plugins/xep_0079/amp.py b/slixmpp/plugins/xep_0079/amp.py
new file mode 100644
index 00000000..6e65d02a
--- /dev/null
+++ b/slixmpp/plugins/xep_0079/amp.py
@@ -0,0 +1,79 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+from slixmpp.stanza import Message, Error, StreamFeatures
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.matcher import StanzaPath, MatchMany
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0079 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0079(BasePlugin):
+
+ """
+ XEP-0079 Advanced Message Processing
+ """
+
+ name = 'xep_0079'
+ description = 'XEP-0079: Advanced Message Processing'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, stanza.AMP)
+ register_stanza_plugin(Error, stanza.InvalidRules)
+ register_stanza_plugin(Error, stanza.UnsupportedConditions)
+ register_stanza_plugin(Error, stanza.UnsupportedActions)
+ register_stanza_plugin(Error, stanza.FailedRules)
+
+ self.xmpp.register_handler(
+ Callback('AMP Response',
+ MatchMany([
+ StanzaPath('message/error/failed_rules'),
+ StanzaPath('message/amp')
+ ]),
+ self._handle_amp_response))
+
+ if not self.xmpp.is_component:
+ self.xmpp.register_feature('amp',
+ self._handle_amp_feature,
+ restart=False,
+ order=9000)
+ register_stanza_plugin(StreamFeatures, stanza.AMPFeature)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('AMP Response')
+
+ def _handle_amp_response(self, msg):
+ log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+ if msg['type'] == 'error':
+ self.xmpp.event('amp_error', msg)
+ elif msg['amp']['status'] in ('alert', 'notify'):
+ self.xmpp.event('amp_%s' % msg['amp']['status'], msg)
+
+ def _handle_amp_feature(self, features):
+ log.debug('Advanced Message Processing is available.')
+ self.xmpp.features.add('amp')
+
+ def discover_support(self, jid=None, **iqargs):
+ if jid is None:
+ if self.xmpp.is_component:
+ jid = self.xmpp.server_host
+ else:
+ jid = self.xmpp.boundjid.host
+
+ return self.xmpp['xep_0030'].get_info(
+ jid=jid,
+ node='http://jabber.org/protocol/amp',
+ **iqargs)
diff --git a/slixmpp/plugins/xep_0079/stanza.py b/slixmpp/plugins/xep_0079/stanza.py
new file mode 100644
index 00000000..e3e1553a
--- /dev/null
+++ b/slixmpp/plugins/xep_0079/stanza.py
@@ -0,0 +1,96 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class AMP(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'amp'
+ plugin_attrib = 'amp'
+ interfaces = set(['from', 'to', 'status', 'per_hop'])
+
+ def get_from(self):
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value):
+ return self._set_attr('from', str(value))
+
+ def get_to(self):
+ return JID(self._get_attr('from'))
+
+ def set_to(self, value):
+ return self._set_attr('to', str(value))
+
+ def get_per_hop(self):
+ return self._get_attr('per-hop') == 'true'
+
+ def set_per_hop(self, value):
+ if value:
+ return self._set_attr('per-hop', 'true')
+ else:
+ return self._del_attr('per-hop')
+
+ def del_per_hop(self):
+ return self._del_attr('per-hop')
+
+ def add_rule(self, action, condition, value):
+ rule = Rule(parent=self)
+ rule['action'] = action
+ rule['condition'] = condition
+ rule['value'] = value
+
+
+class Rule(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'rule'
+ plugin_attrib = name
+ plugin_multi_attrib = 'rules'
+ interfaces = set(['action', 'condition', 'value'])
+
+
+class InvalidRules(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'invalid-rules'
+ plugin_attrib = 'invalid_rules'
+
+
+class UnsupportedConditions(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'unsupported-conditions'
+ plugin_attrib = 'unsupported_conditions'
+
+
+class UnsupportedActions(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'unsupported-actions'
+ plugin_attrib = 'unsupported_actions'
+
+
+class FailedRule(Rule):
+ namespace = 'http://jabber.org/protocol/amp#errors'
+
+
+class FailedRules(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp#errors'
+ name = 'failed-rules'
+ plugin_attrib = 'failed_rules'
+
+
+class AMPFeature(ElementBase):
+ namespace = 'http://jabber.org/features/amp'
+ name = 'amp'
+
+
+register_stanza_plugin(AMP, Rule, iterable=True)
+register_stanza_plugin(InvalidRules, Rule, iterable=True)
+register_stanza_plugin(UnsupportedConditions, Rule, iterable=True)
+register_stanza_plugin(UnsupportedActions, Rule, iterable=True)
+register_stanza_plugin(FailedRules, FailedRule, iterable=True)
diff --git a/slixmpp/plugins/xep_0080/__init__.py b/slixmpp/plugins/xep_0080/__init__.py
new file mode 100644
index 00000000..c487ef9c
--- /dev/null
+++ b/slixmpp/plugins/xep_0080/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0080.stanza import Geoloc
+from slixmpp.plugins.xep_0080.geoloc import XEP_0080
+
+
+register_plugin(XEP_0080)
diff --git a/slixmpp/plugins/xep_0080/geoloc.py b/slixmpp/plugins/xep_0080/geoloc.py
new file mode 100644
index 00000000..c9d97edb
--- /dev/null
+++ b/slixmpp/plugins/xep_0080/geoloc.py
@@ -0,0 +1,121 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0080 import stanza, Geoloc
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0080(BasePlugin):
+
+ """
+ XEP-0080: User Location
+ """
+
+ name = 'xep_0080'
+ description = 'XEP-0080: User Location'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0163'].remove_interest(Geoloc.namespace)
+ self.xmpp['xep_0030'].del_feature(feature=Geoloc.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_location', Geoloc)
+
+ def publish_location(self, **kwargs):
+ """
+ Publish the user's current location.
+
+ Arguments:
+ accuracy -- Horizontal GPS error in meters.
+ alt -- Altitude in meters above or below sea level.
+ area -- A named area such as a campus or neighborhood.
+ bearing -- GPS bearing (direction in which the entity is
+ heading to reach its next waypoint), measured in
+ decimal degrees relative to true north.
+ building -- A specific building on a street or in an area.
+ country -- The nation where the user is located.
+ countrycode -- The ISO 3166 two-letter country code.
+ datum -- GPS datum.
+ description -- A natural-language name for or description of
+ the location.
+ error -- Horizontal GPS error in arc minutes. Obsoleted by
+ the accuracy parameter.
+ floor -- A particular floor in a building.
+ lat -- Latitude in decimal degrees North.
+ locality -- A locality within the administrative region, such
+ as a town or city.
+ lon -- Longitude in decimal degrees East.
+ postalcode -- A code used for postal delivery.
+ region -- An administrative region of the nation, such
+ as a state or province.
+ room -- A particular room in a building.
+ speed -- The speed at which the entity is moving,
+ in meters per second.
+ street -- A thoroughfare within the locality, or a crossing
+ of two thoroughfares.
+ text -- A catch-all element that captures any other
+ information about the location.
+ timestamp -- UTC timestamp specifying the moment when the
+ reading was taken.
+ uri -- A URI or URL pointing to information about
+ the location.
+
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ options = kwargs.get('options', None)
+ ifrom = kwargs.get('ifrom', None)
+ callback = kwargs.get('callback', None)
+ timeout = kwargs.get('timeout', None)
+ timeout_callback = kwargs.get('timeout_callback', None)
+ for param in ('ifrom', 'block', 'callback', 'timeout', 'options', 'timeout_callback'):
+ if param in kwargs:
+ del kwargs[param]
+
+ geoloc = Geoloc()
+ geoloc.values = kwargs
+
+ return self.xmpp['xep_0163'].publish(geoloc,
+ options=options,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None):
+ """
+ Clear existing user location information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ geoloc = Geoloc()
+ return self.xmpp['xep_0163'].publish(geoloc,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=None)
diff --git a/slixmpp/plugins/xep_0080/stanza.py b/slixmpp/plugins/xep_0080/stanza.py
new file mode 100644
index 00000000..e25fea4f
--- /dev/null
+++ b/slixmpp/plugins/xep_0080/stanza.py
@@ -0,0 +1,266 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+from slixmpp.plugins import xep_0082
+
+
+class Geoloc(ElementBase):
+
+ """
+ XMPP's <geoloc> stanza allows entities to know the current
+ geographical or physical location of an entity. (XEP-0080: User Location)
+
+ Example <geoloc> stanzas:
+ <geoloc xmlns='http://jabber.org/protocol/geoloc'/>
+
+ <geoloc xmlns='http://jabber.org/protocol/geoloc' xml:lang='en'>
+ <accuracy>20</accuracy>
+ <country>Italy</country>
+ <lat>45.44</lat>
+ <locality>Venice</locality>
+ <lon>12.33</lon>
+ </geoloc>
+
+ Stanza Interface:
+ accuracy -- Horizontal GPS error in meters.
+ alt -- Altitude in meters above or below sea level.
+ area -- A named area such as a campus or neighborhood.
+ bearing -- GPS bearing (direction in which the entity is
+ heading to reach its next waypoint), measured in
+ decimal degrees relative to true north.
+ building -- A specific building on a street or in an area.
+ country -- The nation where the user is located.
+ countrycode -- The ISO 3166 two-letter country code.
+ datum -- GPS datum.
+ description -- A natural-language name for or description of
+ the location.
+ error -- Horizontal GPS error in arc minutes. Obsoleted by
+ the accuracy parameter.
+ floor -- A particular floor in a building.
+ lat -- Latitude in decimal degrees North.
+ locality -- A locality within the administrative region, such
+ as a town or city.
+ lon -- Longitude in decimal degrees East.
+ postalcode -- A code used for postal delivery.
+ region -- An administrative region of the nation, such
+ as a state or province.
+ room -- A particular room in a building.
+ speed -- The speed at which the entity is moving,
+ in meters per second.
+ street -- A thoroughfare within the locality, or a crossing
+ of two thoroughfares.
+ text -- A catch-all element that captures any other
+ information about the location.
+ timestamp -- UTC timestamp specifying the moment when the
+ reading was taken.
+ uri -- A URI or URL pointing to information about
+ the location.
+ """
+
+ namespace = 'http://jabber.org/protocol/geoloc'
+ name = 'geoloc'
+ interfaces = set(('accuracy', 'alt', 'area', 'bearing', 'building',
+ 'country', 'countrycode', 'datum', 'dscription',
+ 'error', 'floor', 'lat', 'locality', 'lon',
+ 'postalcode', 'region', 'room', 'speed', 'street',
+ 'text', 'timestamp', 'uri'))
+ sub_interfaces = interfaces
+ plugin_attrib = name
+
+ def exception(self, e):
+ """
+ Override exception passback for presence.
+ """
+ pass
+
+ def set_accuracy(self, accuracy):
+ """
+ Set the value of the <accuracy> element.
+
+ Arguments:
+ accuracy -- Horizontal GPS error in meters
+ """
+ self._set_sub_text('accuracy', text=str(accuracy))
+ return self
+
+ def get_accuracy(self):
+ """
+ Return the value of the <accuracy> element as an integer.
+ """
+ p = self._get_sub_text('accuracy')
+ if not p:
+ return None
+ else:
+ try:
+ return int(p)
+ except ValueError:
+ return None
+
+ def set_alt(self, alt):
+ """
+ Set the value of the <alt> element.
+
+ Arguments:
+ alt -- Altitude in meters above or below sea level
+ """
+ self._set_sub_text('alt', text=str(alt))
+ return self
+
+ def get_alt(self):
+ """
+ Return the value of the <alt> element as an integer.
+ """
+ p = self._get_sub_text('alt')
+ if not p:
+ return None
+ else:
+ try:
+ return int(p)
+ except ValueError:
+ return None
+
+ def set_bearing(self, bearing):
+ """
+ Set the value of the <bearing> element.
+
+ Arguments:
+ bearing -- GPS bearing (direction in which the entity is heading
+ to reach its next waypoint), measured in decimal
+ degrees relative to true north
+ """
+ self._set_sub_text('bearing', text=str(bearing))
+ return self
+
+ def get_bearing(self):
+ """
+ Return the value of the <bearing> element as a float.
+ """
+ p = self._get_sub_text('bearing')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_error(self, error):
+ """
+ Set the value of the <error> element.
+
+ Arguments:
+ error -- Horizontal GPS error in arc minutes; this
+ element is deprecated in favor of <accuracy/>
+ """
+ self._set_sub_text('error', text=str(error))
+ return self
+
+ def get_error(self):
+ """
+ Return the value of the <error> element as a float.
+ """
+ p = self._get_sub_text('error')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_lat(self, lat):
+ """
+ Set the value of the <lat> element.
+
+ Arguments:
+ lat -- Latitude in decimal degrees North
+ """
+ self._set_sub_text('lat', text=str(lat))
+ return self
+
+ def get_lat(self):
+ """
+ Return the value of the <lat> element as a float.
+ """
+ p = self._get_sub_text('lat')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_lon(self, lon):
+ """
+ Set the value of the <lon> element.
+
+ Arguments:
+ lon -- Longitude in decimal degrees East
+ """
+ self._set_sub_text('lon', text=str(lon))
+ return self
+
+ def get_lon(self):
+ """
+ Return the value of the <lon> element as a float.
+ """
+ p = self._get_sub_text('lon')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_speed(self, speed):
+ """
+ Set the value of the <speed> element.
+
+ Arguments:
+ speed -- The speed at which the entity is moving,
+ in meters per second
+ """
+ self._set_sub_text('speed', text=str(speed))
+ return self
+
+ def get_speed(self):
+ """
+ Return the value of the <speed> element as a float.
+ """
+ p = self._get_sub_text('speed')
+ if not p:
+ return None
+ else:
+ try:
+ return float(p)
+ except ValueError:
+ return None
+
+ def set_timestamp(self, timestamp):
+ """
+ Set the value of the <timestamp> element.
+
+ Arguments:
+ timestamp -- UTC timestamp specifying the moment when
+ the reading was taken
+ """
+ self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp)))
+ return self
+
+ def get_timestamp(self):
+ """
+ Return the value of the <timestamp> element as a DateTime.
+ """
+ p = self._get_sub_text('timestamp')
+ if not p:
+ return None
+ else:
+ return xep_0082.datetime(p)
diff --git a/slixmpp/plugins/xep_0082.py b/slixmpp/plugins/xep_0082.py
new file mode 100644
index 00000000..24436622
--- /dev/null
+++ b/slixmpp/plugins/xep_0082.py
@@ -0,0 +1,228 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from slixmpp.plugins import BasePlugin, register_plugin
+from slixmpp.thirdparty import tzutc, tzoffset, parse_iso
+
+
+# =====================================================================
+# To make it easier for stanzas without direct access to plugin objects
+# to use the XEP-0082 utility methods, we will define them as top-level
+# functions and then just reference them in the plugin itself.
+
+def parse(time_str):
+ """
+ Convert a string timestamp into a datetime object.
+
+ Arguments:
+ time_str -- A formatted timestamp string.
+ """
+ return parse_iso(time_str)
+
+
+def format_date(time_obj):
+ """
+ Return a formatted string version of a date object.
+
+ Format:
+ YYYY-MM-DD
+
+ Arguments:
+ time_obj -- A date or datetime object.
+ """
+ if isinstance(time_obj, dt.datetime):
+ time_obj = time_obj.date()
+ return time_obj.isoformat()
+
+
+def format_time(time_obj):
+ """
+ Return a formatted string version of a time object.
+
+ format:
+ hh:mm:ss[.sss][TZD]
+
+ arguments:
+ time_obj -- A time or datetime object.
+ """
+ if isinstance(time_obj, dt.datetime):
+ time_obj = time_obj.timetz()
+ timestamp = time_obj.isoformat()
+ if time_obj.tzinfo == tzutc():
+ timestamp = timestamp[:-6]
+ return '%sZ' % timestamp
+ return timestamp
+
+
+def format_datetime(time_obj):
+ """
+ Return a formatted string version of a datetime object.
+
+ Format:
+ YYYY-MM-DDThh:mm:ss[.sss]TZD
+
+ arguments:
+ time_obj -- A datetime object.
+ """
+ timestamp = time_obj.isoformat('T')
+ if time_obj.tzinfo == tzutc():
+ timestamp = timestamp[:-6]
+ return '%sZ' % timestamp
+ return timestamp
+
+
+def date(year=None, month=None, day=None, obj=False):
+ """
+ Create a date only timestamp for the given instant.
+
+ Unspecified components default to their current counterparts.
+
+ Arguments:
+ year -- Integer value of the year (4 digits)
+ month -- Integer value of the month
+ day -- Integer value of the day of the month.
+ obj -- If True, return the date object instead
+ of a formatted string. Defaults to False.
+ """
+ today = dt.datetime.utcnow()
+ if year is None:
+ year = today.year
+ if month is None:
+ month = today.month
+ if day is None:
+ day = today.day
+ value = dt.date(year, month, day)
+ if obj:
+ return value
+ return format_date(value)
+
+
+def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False):
+ """
+ Create a time only timestamp for the given instant.
+
+ Unspecified components default to their current counterparts.
+
+ Arguments:
+ hour -- Integer value of the hour.
+ min -- Integer value of the number of minutes.
+ sec -- Integer value of the number of seconds.
+ micro -- Integer value of the number of microseconds.
+ offset -- Either a positive or negative number of seconds
+ to offset from UTC to match a desired timezone,
+ or a tzinfo object.
+ obj -- If True, return the time object instead
+ of a formatted string. Defaults to False.
+ """
+ now = dt.datetime.utcnow()
+ if hour is None:
+ hour = now.hour
+ if min is None:
+ min = now.minute
+ if sec is None:
+ sec = now.second
+ if micro is None:
+ micro = now.microsecond
+ if offset is None:
+ offset = tzutc()
+ elif not isinstance(offset, dt.tzinfo):
+ offset = tzoffset(None, offset)
+ value = dt.time(hour, min, sec, micro, offset)
+ if obj:
+ return value
+ return format_time(value)
+
+
+def datetime(year=None, month=None, day=None, hour=None,
+ min=None, sec=None, micro=None, offset=None,
+ separators=True, obj=False):
+ """
+ Create a datetime timestamp for the given instant.
+
+ Unspecified components default to their current counterparts.
+
+ Arguments:
+ year -- Integer value of the year (4 digits)
+ month -- Integer value of the month
+ day -- Integer value of the day of the month.
+ hour -- Integer value of the hour.
+ min -- Integer value of the number of minutes.
+ sec -- Integer value of the number of seconds.
+ micro -- Integer value of the number of microseconds.
+ offset -- Either a positive or negative number of seconds
+ to offset from UTC to match a desired timezone,
+ or a tzinfo object.
+ obj -- If True, return the datetime object instead
+ of a formatted string. Defaults to False.
+ """
+ now = dt.datetime.utcnow()
+ if year is None:
+ year = now.year
+ if month is None:
+ month = now.month
+ if day is None:
+ day = now.day
+ if hour is None:
+ hour = now.hour
+ if min is None:
+ min = now.minute
+ if sec is None:
+ sec = now.second
+ if micro is None:
+ micro = now.microsecond
+ if offset is None:
+ offset = tzutc()
+ elif not isinstance(offset, dt.tzinfo):
+ offset = tzoffset(None, offset)
+
+ value = dt.datetime(year, month, day, hour,
+ min, sec, micro, offset)
+ if obj:
+ return value
+ return format_datetime(value)
+
+
+class XEP_0082(BasePlugin):
+
+ """
+ XEP-0082: XMPP Date and Time Profiles
+
+ XMPP uses a subset of the formats allowed by ISO 8601 as a matter of
+ pragmatism based on the relatively few formats historically used by
+ the XMPP.
+
+ Also see <http://www.xmpp.org/extensions/xep-0082.html>.
+
+ Methods:
+ date -- Create a time stamp using the Date profile.
+ datetime -- Create a time stamp using the DateTime profile.
+ time -- Create a time stamp using the Time profile.
+ format_date -- Format an existing date object.
+ format_datetime -- Format an existing datetime object.
+ format_time -- Format an existing time object.
+ parse -- Convert a time string into a Python datetime object.
+ """
+
+ name = 'xep_0082'
+ description = 'XEP-0082: XMPP Date and Time Profiles'
+ dependencies = set()
+
+ def plugin_init(self):
+ """Start the XEP-0082 plugin."""
+ self.date = date
+ self.datetime = datetime
+ self.time = time
+ self.format_date = format_date
+ self.format_datetime = format_datetime
+ self.format_time = format_time
+ self.parse = parse
+
+
+register_plugin(XEP_0082)
diff --git a/slixmpp/plugins/xep_0084/__init__.py b/slixmpp/plugins/xep_0084/__init__.py
new file mode 100644
index 00000000..aa5fdae5
--- /dev/null
+++ b/slixmpp/plugins/xep_0084/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0084 import stanza
+from slixmpp.plugins.xep_0084.stanza import Data, MetaData
+from slixmpp.plugins.xep_0084.avatar import XEP_0084
+
+
+register_plugin(XEP_0084)
diff --git a/slixmpp/plugins/xep_0084/avatar.py b/slixmpp/plugins/xep_0084/avatar.py
new file mode 100644
index 00000000..e5f9dfaa
--- /dev/null
+++ b/slixmpp/plugins/xep_0084/avatar.py
@@ -0,0 +1,110 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import hashlib
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins.xep_0084 import stanza, Data, MetaData
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0084(BasePlugin):
+
+ name = 'xep_0084'
+ description = 'XEP-0084: User Avatar'
+ dependencies = set(['xep_0163', 'xep_0060'])
+ stanza = stanza
+
+ def plugin_init(self):
+ pubsub_stanza = self.xmpp['xep_0060'].stanza
+ register_stanza_plugin(pubsub_stanza.Item, Data)
+ register_stanza_plugin(pubsub_stanza.EventItem, Data)
+
+ self.xmpp['xep_0060'].map_node_event(Data.namespace, 'avatar_data')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=MetaData.namespace)
+ self.xmpp['xep_0163'].remove_interest(MetaData.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData)
+
+ def generate_id(self, data):
+ return hashlib.sha1(data).hexdigest()
+
+ def retrieve_avatar(self, jid, id, url=None, ifrom=None,
+ callback=None, timeout=None, timeout_callback=None):
+ return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def publish_avatar(self, data, ifrom=None, callback=None,
+ timeout=None, timeout_callback=None):
+ payload = Data()
+ payload['value'] = data
+ return self.xmpp['xep_0163'].publish(payload,
+ id=self.generate_id(data),
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def publish_avatar_metadata(self, items=None, pointers=None,
+ ifrom=None,
+ callback=None, timeout=None,
+ timeout_callback=None):
+ metadata = MetaData()
+ if items is None:
+ items = []
+ if not isinstance(items, (list, set)):
+ items = [items]
+ for info in items:
+ metadata.add_info(info['id'], info['type'], info['bytes'],
+ height=info.get('height', ''),
+ width=info.get('width', ''),
+ url=info.get('url', ''))
+
+ if pointers is not None:
+ for pointer in pointers:
+ metadata.add_pointer(pointer)
+
+ return self.xmpp['xep_0163'].publish(metadata,
+ id=info['id'],
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None):
+ """
+ Clear existing avatar metadata information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ metadata = MetaData()
+ return self.xmpp['xep_0163'].publish(metadata,
+ node=MetaData.namespace,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0084/stanza.py b/slixmpp/plugins/xep_0084/stanza.py
new file mode 100644
index 00000000..ebcd73e3
--- /dev/null
+++ b/slixmpp/plugins/xep_0084/stanza.py
@@ -0,0 +1,78 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from base64 import b64encode, b64decode
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Data(ElementBase):
+ name = 'data'
+ namespace = 'urn:xmpp:avatar:data'
+ plugin_attrib = 'avatar_data'
+ interfaces = set(['value'])
+
+ def get_value(self):
+ if self.xml.text:
+ return b64decode(bytes(self.xml.text))
+ return b''
+
+ def set_value(self, value):
+ if value:
+ self.xml.text = b64encode(bytes(value)).decode()
+ else:
+ self.xml.text = ''
+
+ def del_value(self):
+ self.xml.text = ''
+
+
+class MetaData(ElementBase):
+ name = 'metadata'
+ namespace = 'urn:xmpp:avatar:metadata'
+ plugin_attrib = 'avatar_metadata'
+ interfaces = set()
+
+ def add_info(self, id, itype, ibytes, height=None, width=None, url=None):
+ info = Info()
+ info.values = {'id': id,
+ 'type': itype,
+ 'bytes': '%s' % ibytes,
+ 'height': height,
+ 'width': width,
+ 'url': url}
+ self.append(info)
+
+ def add_pointer(self, xml):
+ if not isinstance(xml, Pointer):
+ pointer = Pointer()
+ pointer.append(xml)
+ self.append(pointer)
+ else:
+ self.append(xml)
+
+
+class Info(ElementBase):
+ name = 'info'
+ namespace = 'urn:xmpp:avatar:metadata'
+ plugin_attrib = 'info'
+ plugin_multi_attrib = 'items'
+ interfaces = set(['bytes', 'height', 'id', 'type', 'url', 'width'])
+
+
+class Pointer(ElementBase):
+ name = 'pointer'
+ namespace = 'urn:xmpp:avatar:metadata'
+ plugin_attrib = 'pointer'
+ plugin_multi_attrib = 'pointers'
+ interfaces = set()
+
+
+register_stanza_plugin(MetaData, Info, iterable=True)
+register_stanza_plugin(MetaData, Pointer, iterable=True)
diff --git a/slixmpp/plugins/xep_0085/__init__.py b/slixmpp/plugins/xep_0085/__init__.py
new file mode 100644
index 00000000..9b9da990
--- /dev/null
+++ b/slixmpp/plugins/xep_0085/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0085.stanza import ChatState
+from slixmpp.plugins.xep_0085.chat_states import XEP_0085
+
+
+register_plugin(XEP_0085)
+
+
+# Retain some backwards compatibility
+xep_0085 = XEP_0085
diff --git a/slixmpp/plugins/xep_0085/chat_states.py b/slixmpp/plugins/xep_0085/chat_states.py
new file mode 100644
index 00000000..1aab9eaa
--- /dev/null
+++ b/slixmpp/plugins/xep_0085/chat_states.py
@@ -0,0 +1,56 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.stanza import Message
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0085 import stanza, ChatState
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0085(BasePlugin):
+
+ """
+ XEP-0085 Chat State Notifications
+ """
+
+ name = 'xep_0085'
+ description = 'XEP-0085: Chat State Notifications'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Chat State',
+ StanzaPath('message/chat_state'),
+ self._handle_chat_state))
+
+ register_stanza_plugin(Message, stanza.Active)
+ register_stanza_plugin(Message, stanza.Composing)
+ register_stanza_plugin(Message, stanza.Gone)
+ register_stanza_plugin(Message, stanza.Inactive)
+ register_stanza_plugin(Message, stanza.Paused)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Chat State')
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
+
+ def _handle_chat_state(self, msg):
+ state = msg['chat_state']
+ log.debug("Chat State: %s, %s", state, msg['from'].jid)
+ self.xmpp.event('chatstate', msg)
+ self.xmpp.event('chatstate_%s' % state, msg)
diff --git a/slixmpp/plugins/xep_0085/stanza.py b/slixmpp/plugins/xep_0085/stanza.py
new file mode 100644
index 00000000..d1a5b151
--- /dev/null
+++ b/slixmpp/plugins/xep_0085/stanza.py
@@ -0,0 +1,94 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import slixmpp
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class ChatState(ElementBase):
+
+ """
+ Example chat state stanzas:
+ <message>
+ <active xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+
+ <message>
+ <paused xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+
+ Stanza Interfaces:
+ chat_state
+
+ Attributes:
+ states
+
+ Methods:
+ get_chat_state
+ set_chat_state
+ del_chat_state
+ """
+
+ name = ''
+ namespace = 'http://jabber.org/protocol/chatstates'
+ plugin_attrib = 'chat_state'
+ interfaces = set(('chat_state',))
+ sub_interfaces = interfaces
+ is_extension = True
+
+ states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def get_chat_state(self):
+ parent = self.parent()
+ for state in self.states:
+ state_xml = parent.find('{%s}%s' % (self.namespace, state))
+ if state_xml is not None:
+ self.xml = state_xml
+ return state
+ return ''
+
+ def set_chat_state(self, state):
+ self.del_chat_state()
+ parent = self.parent()
+ if state in self.states:
+ self.xml = ET.Element('{%s}%s' % (self.namespace, state))
+ parent.append(self.xml)
+ elif state not in [None, '']:
+ raise ValueError('Invalid chat state')
+
+ def del_chat_state(self):
+ parent = self.parent()
+ for state in self.states:
+ state_xml = parent.find('{%s}%s' % (self.namespace, state))
+ if state_xml is not None:
+ self.xml = ET.Element('')
+ parent.xml.remove(state_xml)
+
+
+class Active(ChatState):
+ name = 'active'
+
+
+class Composing(ChatState):
+ name = 'composing'
+
+
+class Gone(ChatState):
+ name = 'gone'
+
+
+class Inactive(ChatState):
+ name = 'inactive'
+
+
+class Paused(ChatState):
+ name = 'paused'
diff --git a/slixmpp/plugins/xep_0086/__init__.py b/slixmpp/plugins/xep_0086/__init__.py
new file mode 100644
index 00000000..c6946e5a
--- /dev/null
+++ b/slixmpp/plugins/xep_0086/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0086.stanza import LegacyError
+from slixmpp.plugins.xep_0086.legacy_error import XEP_0086
+
+
+register_plugin(XEP_0086)
+
+
+# Retain some backwards compatibility
+xep_0086 = XEP_0086
diff --git a/slixmpp/plugins/xep_0086/legacy_error.py b/slixmpp/plugins/xep_0086/legacy_error.py
new file mode 100644
index 00000000..0a6e0e87
--- /dev/null
+++ b/slixmpp/plugins/xep_0086/legacy_error.py
@@ -0,0 +1,46 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Error
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0086 import stanza, LegacyError
+
+
+class XEP_0086(BasePlugin):
+
+ """
+ XEP-0086: Error Condition Mappings
+
+ Older XMPP implementations used code based error messages, similar
+ to HTTP response codes. Since then, error condition elements have
+ been introduced. XEP-0086 provides a mapping between the new
+ condition elements and a combination of error types and the older
+ response codes.
+
+ Also see <http://xmpp.org/extensions/xep-0086.html>.
+
+ Configuration Values:
+ override -- Indicates if applying legacy error codes should
+ be done automatically. Defaults to True.
+ If False, then inserting legacy error codes can
+ be done using:
+ iq['error']['legacy']['condition'] = ...
+ """
+
+ name = 'xep_0086'
+ description = 'XEP-0086: Error Condition Mappings'
+ dependencies = set()
+ stanza = stanza
+ default_config = {
+ 'override': True
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Error, LegacyError,
+ overrides=self.override)
diff --git a/slixmpp/plugins/xep_0086/stanza.py b/slixmpp/plugins/xep_0086/stanza.py
new file mode 100644
index 00000000..cbc9429d
--- /dev/null
+++ b/slixmpp/plugins/xep_0086/stanza.py
@@ -0,0 +1,91 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Error
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class LegacyError(ElementBase):
+
+ """
+ Older XMPP implementations used code based error messages, similar
+ to HTTP response codes. Since then, error condition elements have
+ been introduced. XEP-0086 provides a mapping between the new
+ condition elements and a combination of error types and the older
+ response codes.
+
+ Also see <http://xmpp.org/extensions/xep-0086.html>.
+
+ Example legacy error stanzas:
+ <error xmlns="jabber:client" code="501" type="cancel">
+ <feature-not-implemented
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+
+ <error code="402" type="auth">
+ <payment-required
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+
+ Attributes:
+ error_map -- A map of error conditions to error types and
+ code values.
+ Methods:
+ setup -- Overrides ElementBase.setup
+ set_condition -- Remap the type and code interfaces when a
+ condition is set.
+ """
+
+ name = 'legacy'
+ namespace = Error.namespace
+ plugin_attrib = name
+ interfaces = set(('condition',))
+ overrides = ['set_condition']
+
+ error_map = {'bad-request': ('modify', '400'),
+ 'conflict': ('cancel', '409'),
+ 'feature-not-implemented': ('cancel', '501'),
+ 'forbidden': ('auth', '403'),
+ 'gone': ('modify', '302'),
+ 'internal-server-error': ('wait', '500'),
+ 'item-not-found': ('cancel', '404'),
+ 'jid-malformed': ('modify', '400'),
+ 'not-acceptable': ('modify', '406'),
+ 'not-allowed': ('cancel', '405'),
+ 'not-authorized': ('auth', '401'),
+ 'payment-required': ('auth', '402'),
+ 'recipient-unavailable': ('wait', '404'),
+ 'redirect': ('modify', '302'),
+ 'registration-required': ('auth', '407'),
+ 'remote-server-not-found': ('cancel', '404'),
+ 'remote-server-timeout': ('wait', '504'),
+ 'resource-constraint': ('wait', '500'),
+ 'service-unavailable': ('cancel', '503'),
+ 'subscription-required': ('auth', '407'),
+ 'undefined-condition': (None, '500'),
+ 'unexpected-request': ('wait', '400')}
+
+ def setup(self, xml):
+ """Don't create XML for the plugin."""
+ self.xml = ET.Element('')
+
+ def set_condition(self, value):
+ """
+ Set the error type and code based on the given error
+ condition value.
+
+ Arguments:
+ value -- The new error condition.
+ """
+ self.parent().set_condition(value)
+
+ error_data = self.error_map.get(value, None)
+ if error_data is not None:
+ if error_data[0] is not None:
+ self.parent()['type'] = error_data[0]
+ self.parent()['code'] = error_data[1]
diff --git a/slixmpp/plugins/xep_0091/__init__.py b/slixmpp/plugins/xep_0091/__init__.py
new file mode 100644
index 00000000..e5d166b1
--- /dev/null
+++ b/slixmpp/plugins/xep_0091/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0091 import stanza
+from slixmpp.plugins.xep_0091.stanza import LegacyDelay
+from slixmpp.plugins.xep_0091.legacy_delay import XEP_0091
+
+
+register_plugin(XEP_0091)
diff --git a/slixmpp/plugins/xep_0091/legacy_delay.py b/slixmpp/plugins/xep_0091/legacy_delay.py
new file mode 100644
index 00000000..6aef2ba1
--- /dev/null
+++ b/slixmpp/plugins/xep_0091/legacy_delay.py
@@ -0,0 +1,29 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.stanza import Message, Presence
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0091 import stanza
+
+
+class XEP_0091(BasePlugin):
+
+ """
+ XEP-0091: Legacy Delayed Delivery
+ """
+
+ name = 'xep_0091'
+ description = 'XEP-0091: Legacy Delayed Delivery'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, stanza.LegacyDelay)
+ register_stanza_plugin(Presence, stanza.LegacyDelay)
diff --git a/slixmpp/plugins/xep_0091/stanza.py b/slixmpp/plugins/xep_0091/stanza.py
new file mode 100644
index 00000000..ac6457e6
--- /dev/null
+++ b/slixmpp/plugins/xep_0091/stanza.py
@@ -0,0 +1,47 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase
+from slixmpp.plugins import xep_0082
+
+
+class LegacyDelay(ElementBase):
+
+ name = 'x'
+ namespace = 'jabber:x:delay'
+ plugin_attrib = 'legacy_delay'
+ interfaces = set(('from', 'stamp', 'text'))
+
+ def get_from(self):
+ from_ = self._get_attr('from')
+ return JID(from_) if from_ else None
+
+ def set_from(self, value):
+ self._set_attr('from', str(value))
+
+ def get_stamp(self):
+ timestamp = self._get_attr('stamp')
+ return xep_0082.parse('%sZ' % timestamp) if timestamp else None
+
+ def set_stamp(self, value):
+ if isinstance(value, dt.datetime):
+ value = value.astimezone(xep_0082.tzutc)
+ value = xep_0082.format_datetime(value)
+ self._set_attr('stamp', value[0:19].replace('-', ''))
+
+ def get_text(self):
+ return self.xml.text
+
+ def set_text(self, value):
+ self.xml.text = value
+
+ def del_text(self):
+ self.xml.text = ''
diff --git a/slixmpp/plugins/xep_0092/__init__.py b/slixmpp/plugins/xep_0092/__init__.py
new file mode 100644
index 00000000..93743a14
--- /dev/null
+++ b/slixmpp/plugins/xep_0092/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0092 import stanza
+from slixmpp.plugins.xep_0092.stanza import Version
+from slixmpp.plugins.xep_0092.version import XEP_0092
+
+
+register_plugin(XEP_0092)
+
+
+# Retain some backwards compatibility
+xep_0092 = XEP_0092
diff --git a/slixmpp/plugins/xep_0092/stanza.py b/slixmpp/plugins/xep_0092/stanza.py
new file mode 100644
index 00000000..04097a8b
--- /dev/null
+++ b/slixmpp/plugins/xep_0092/stanza.py
@@ -0,0 +1,42 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Version(ElementBase):
+
+ """
+ XMPP allows for an agent to advertise the name and version of the
+ underlying software libraries, as well as the operating system
+ that the agent is running on.
+
+ Example version stanzas:
+ <iq type="get">
+ <query xmlns="jabber:iq:version" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="jabber:iq:version">
+ <name>Slixmpp</name>
+ <version>1.0</version>
+ <os>Linux</os>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ name -- The human readable name of the software.
+ version -- The specific version of the software.
+ os -- The name of the operating system running the program.
+ """
+
+ name = 'query'
+ namespace = 'jabber:iq:version'
+ plugin_attrib = 'software_version'
+ interfaces = set(('name', 'version', 'os'))
+ sub_interfaces = interfaces
diff --git a/slixmpp/plugins/xep_0092/version.py b/slixmpp/plugins/xep_0092/version.py
new file mode 100644
index 00000000..ff0317da
--- /dev/null
+++ b/slixmpp/plugins/xep_0092/version.py
@@ -0,0 +1,87 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp import Iq
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0092 import Version, stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0092(BasePlugin):
+
+ """
+ XEP-0092: Software Version
+ """
+
+ name = 'xep_0092'
+ description = 'XEP-0092: Software Version'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ default_config = {
+ 'software_name': 'Slixmpp',
+ 'version': slixmpp.__version__,
+ 'os': ''
+ }
+
+ def plugin_init(self):
+ """
+ Start the XEP-0092 plugin.
+ """
+ if 'name' in self.config:
+ self.software_name = self.config['name']
+
+ self.xmpp.register_handler(
+ Callback('Software Version',
+ StanzaPath('iq@type=get/software_version'),
+ self._handle_version))
+
+ register_stanza_plugin(Iq, Version)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Software Version')
+ self.xmpp['xep_0030'].del_feature(feature='jabber:iq:version')
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
+
+ def _handle_version(self, iq):
+ """
+ Respond to a software version query.
+
+ Arguments:
+ iq -- The Iq stanza containing the software version query.
+ """
+ iq = iq.reply()
+ iq['software_version']['name'] = self.software_name
+ iq['software_version']['version'] = self.version
+ iq['software_version']['os'] = self.os
+ iq.send()
+
+ def get_version(self, jid, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ """
+ Retrieve the software version of a remote agent.
+
+ Arguments:
+ jid -- The JID of the entity to query.
+ """
+ iq = self.xmpp.Iq()
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq['query'] = Version.namespace
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0095/__init__.py b/slixmpp/plugins/xep_0095/__init__.py
new file mode 100644
index 00000000..3c6380e1
--- /dev/null
+++ b/slixmpp/plugins/xep_0095/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0095 import stanza
+from slixmpp.plugins.xep_0095.stanza import SI
+from slixmpp.plugins.xep_0095.stream_initiation import XEP_0095
+
+
+register_plugin(XEP_0095)
diff --git a/slixmpp/plugins/xep_0095/stanza.py b/slixmpp/plugins/xep_0095/stanza.py
new file mode 100644
index 00000000..62b5f6f8
--- /dev/null
+++ b/slixmpp/plugins/xep_0095/stanza.py
@@ -0,0 +1,25 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class SI(ElementBase):
+ name = 'si'
+ namespace = 'http://jabber.org/protocol/si'
+ plugin_attrib = 'si'
+ interfaces = set(['id', 'mime_type', 'profile'])
+
+ def get_mime_type(self):
+ return self._get_attr('mime-type', 'application/octet-stream')
+
+ def set_mime_type(self, value):
+ self._set_attr('mime-type', value)
+
+ def del_mime_type(self):
+ self._del_attr('mime-type')
diff --git a/slixmpp/plugins/xep_0095/stream_initiation.py b/slixmpp/plugins/xep_0095/stream_initiation.py
new file mode 100644
index 00000000..3f909d93
--- /dev/null
+++ b/slixmpp/plugins/xep_0095/stream_initiation.py
@@ -0,0 +1,213 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+
+from uuid import uuid4
+
+from slixmpp import Iq, Message
+from slixmpp.exceptions import XMPPError
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins.xep_0095 import stanza, SI
+
+
+log = logging.getLogger(__name__)
+
+
+SOCKS5 = 'http://jabber.org/protocol/bytestreams'
+IBB = 'http://jabber.org/protocol/ibb'
+
+
+class XEP_0095(BasePlugin):
+
+ name = 'xep_0095'
+ description = 'XEP-0095: Stream Initiation'
+ dependencies = set(['xep_0020', 'xep_0030', 'xep_0047', 'xep_0065'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self._profiles = {}
+ self._methods = {}
+ self._methods_order = []
+ self._pending_lock = threading.Lock()
+ self._pending= {}
+
+ self.register_method(SOCKS5, 'xep_0065', 100)
+ self.register_method(IBB, 'xep_0047', 50)
+
+ register_stanza_plugin(Iq, SI)
+ register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation)
+
+ self.xmpp.register_handler(
+ Callback('SI Request',
+ StanzaPath('iq@type=set/si'),
+ self._handle_request))
+
+ self.api.register(self._add_pending, 'add_pending', default=True)
+ self.api.register(self._get_pending, 'get_pending', default=True)
+ self.api.register(self._del_pending, 'del_pending', default=True)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(SI.namespace)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('SI Request')
+ self.xmpp['xep_0030'].del_feature(feature=SI.namespace)
+
+ def register_profile(self, profile_name, plugin):
+ self._profiles[profile_name] = plugin
+
+ def unregister_profile(self, profile_name):
+ try:
+ del self._profiles[profile_name]
+ except KeyError:
+ pass
+
+ def register_method(self, method, plugin_name, order=50):
+ self._methods[method] = (plugin_name, order)
+ self._methods_order.append((order, method, plugin_name))
+ self._methods_order.sort()
+
+ def unregister_method(self, method):
+ if method in self._methods:
+ plugin_name, order = self._methods[method]
+ del self._methods[method]
+ self._methods_order.remove((order, method, plugin_name))
+ self._methods_order.sort()
+
+ def _handle_request(self, iq):
+ profile = iq['si']['profile']
+ sid = iq['si']['id']
+
+ if not sid:
+ raise XMPPError(etype='modify', condition='bad-request')
+ if profile not in self._profiles:
+ raise XMPPError(
+ etype='modify',
+ condition='bad-request',
+ extension='bad-profile',
+ extension_ns=SI.namespace)
+
+ neg = iq['si']['feature_neg']['form']['fields']
+ options = neg['stream-method']['options'] or []
+ methods = []
+ for opt in options:
+ methods.append(opt['value'])
+ for method in methods:
+ if method in self._methods:
+ supported = True
+ break
+ else:
+ raise XMPPError('bad-request',
+ extension='no-valid-streams',
+ extension_ns=SI.namespace)
+
+ selected_method = None
+ log.debug('Available: %s', methods)
+ for order, method, plugin in self._methods_order:
+ log.debug('Testing: %s', method)
+ if method in methods:
+ selected_method = method
+ break
+
+ receiver = iq['to']
+ sender = iq['from']
+
+ self.api['add_pending'](receiver, sid, sender, {
+ 'response_id': iq['id'],
+ 'method': selected_method,
+ 'profile': profile
+ })
+ self.xmpp.event('si_request', iq)
+
+ def offer(self, jid, sid=None, mime_type=None, profile=None,
+ methods=None, payload=None, ifrom=None,
+ **iqargs):
+ if sid is None:
+ sid = uuid4().hex
+ if methods is None:
+ methods = list(self._methods.keys())
+ if not isinstance(methods, (list, tuple, set)):
+ methods = [methods]
+
+ si = self.xmpp.Iq()
+ si['to'] = jid
+ si['from'] = ifrom
+ si['type'] = 'set'
+ si['si']['id'] = sid
+ si['si']['mime_type'] = mime_type
+ si['si']['profile'] = profile
+ if not isinstance(payload, (list, tuple, set)):
+ payload = [payload]
+ for item in payload:
+ si['si'].append(item)
+ si['si']['feature_neg']['form'].add_field(
+ var='stream-method',
+ ftype='list-single',
+ options=methods)
+ return si.send(**iqargs)
+
+ def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None):
+ stream = self.api['get_pending'](ifrom, sid, jid)
+ iq = self.xmpp.Iq()
+ iq['id'] = stream['response_id']
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'result'
+ if payload:
+ iq['si'].append(payload)
+ iq['si']['feature_neg']['form']['type'] = 'submit'
+ iq['si']['feature_neg']['form'].add_field(
+ var='stream-method',
+ ftype='list-single',
+ value=stream['method'])
+
+ if ifrom is None:
+ ifrom = self.xmpp.boundjid
+
+ method_plugin = self._methods[stream['method']][0]
+ self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid)
+
+ self.api['del_pending'](ifrom, sid, jid)
+
+ if stream_handler:
+ self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid),
+ stream_handler,
+ disposable=True)
+ return iq.send()
+
+ def decline(self, jid, sid, ifrom=None):
+ stream = self.api['get_pending'](ifrom, sid, jid)
+ if not stream:
+ return
+ iq = self.xmpp.Iq()
+ iq['id'] = stream['response_id']
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'error'
+ iq['error']['condition'] = 'forbidden'
+ iq['error']['text'] = 'Offer declined'
+ self.api['del_pending'](ifrom, sid, jid)
+ return iq.send()
+
+ def _add_pending(self, jid, node, ifrom, data):
+ with self._pending_lock:
+ self._pending[(jid, node, ifrom)] = data
+
+ def _get_pending(self, jid, node, ifrom, data):
+ with self._pending_lock:
+ return self._pending.get((jid, node, ifrom), None)
+
+ def _del_pending(self, jid, node, ifrom, data):
+ with self._pending_lock:
+ if (jid, node, ifrom) in self._pending:
+ del self._pending[(jid, node, ifrom)]
diff --git a/slixmpp/plugins/xep_0096/__init__.py b/slixmpp/plugins/xep_0096/__init__.py
new file mode 100644
index 00000000..866a9820
--- /dev/null
+++ b/slixmpp/plugins/xep_0096/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0096 import stanza
+from slixmpp.plugins.xep_0096.stanza import File
+from slixmpp.plugins.xep_0096.file_transfer import XEP_0096
+
+
+register_plugin(XEP_0096)
diff --git a/slixmpp/plugins/xep_0096/file_transfer.py b/slixmpp/plugins/xep_0096/file_transfer.py
new file mode 100644
index 00000000..3c09a5b5
--- /dev/null
+++ b/slixmpp/plugins/xep_0096/file_transfer.py
@@ -0,0 +1,59 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq, Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins.xep_0096 import stanza, File
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0096(BasePlugin):
+
+ name = 'xep_0096'
+ description = 'XEP-0096: SI File Transfer'
+ dependencies = set(['xep_0095'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File)
+
+ self.xmpp['xep_0095'].register_profile(File.namespace, self)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(File.namespace)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=File.namespace)
+ self.xmpp['xep_0095'].unregister_profile(File.namespace, self)
+
+ def request_file_transfer(self, jid, sid=None, name=None, size=None,
+ desc=None, hash=None, date=None,
+ allow_ranged=False, mime_type=None,
+ **iqargs):
+ data = File()
+ data['name'] = name
+ data['size'] = size
+ data['date'] = date
+ data['desc'] = desc
+ data['hash'] = hash
+ if allow_ranged:
+ data.enable('range')
+
+ return self.xmpp['xep_0095'].offer(jid,
+ sid=sid,
+ mime_type=mime_type,
+ profile=File.namespace,
+ payload=data,
+ **iqargs)
diff --git a/slixmpp/plugins/xep_0096/stanza.py b/slixmpp/plugins/xep_0096/stanza.py
new file mode 100644
index 00000000..d3781c8d
--- /dev/null
+++ b/slixmpp/plugins/xep_0096/stanza.py
@@ -0,0 +1,48 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+from slixmpp.plugins import xep_0082
+
+
+class File(ElementBase):
+ name = 'file'
+ namespace = 'http://jabber.org/protocol/si/profile/file-transfer'
+ plugin_attrib = 'file'
+ interfaces = set(['name', 'size', 'date', 'hash', 'desc'])
+ sub_interfaces = set(['desc'])
+
+ def set_size(self, value):
+ self._set_attr('size', str(value))
+
+ def get_date(self):
+ timestamp = self._get_attr('date')
+ return xep_0082.parse(timestamp)
+
+ def set_date(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('date', value)
+
+
+class Range(ElementBase):
+ name = 'range'
+ namespace = 'http://jabber.org/protocol/si/profile/file-transfer'
+ plugin_attrib = 'range'
+ interfaces = set(['length', 'offset'])
+
+ def set_length(self, value):
+ self._set_attr('length', str(value))
+
+ def set_offset(self, value):
+ self._set_attr('offset', str(value))
+
+
+register_stanza_plugin(File, Range)
diff --git a/slixmpp/plugins/xep_0106.py b/slixmpp/plugins/xep_0106.py
new file mode 100644
index 00000000..a4717956
--- /dev/null
+++ b/slixmpp/plugins/xep_0106.py
@@ -0,0 +1,26 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.plugins import BasePlugin, register_plugin
+
+
+class XEP_0106(BasePlugin):
+
+ name = 'xep_0106'
+ description = 'XEP-0106: JID Escaping'
+ dependencies = set(['xep_0030'])
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(feature='jid\\20escaping')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='jid\\20escaping')
+
+
+register_plugin(XEP_0106)
diff --git a/slixmpp/plugins/xep_0107/__init__.py b/slixmpp/plugins/xep_0107/__init__.py
new file mode 100644
index 00000000..778fd33b
--- /dev/null
+++ b/slixmpp/plugins/xep_0107/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0107 import stanza
+from slixmpp.plugins.xep_0107.stanza import UserMood
+from slixmpp.plugins.xep_0107.user_mood import XEP_0107
+
+
+register_plugin(XEP_0107)
diff --git a/slixmpp/plugins/xep_0107/stanza.py b/slixmpp/plugins/xep_0107/stanza.py
new file mode 100644
index 00000000..05967de9
--- /dev/null
+++ b/slixmpp/plugins/xep_0107/stanza.py
@@ -0,0 +1,55 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class UserMood(ElementBase):
+
+ name = 'mood'
+ namespace = 'http://jabber.org/protocol/mood'
+ plugin_attrib = 'mood'
+ interfaces = set(['value', 'text'])
+ sub_interfaces = set(['text'])
+ moods = set(['afraid', 'amazed', 'amorous', 'angry', 'annoyed', 'anxious',
+ 'aroused', 'ashamed', 'bored', 'brave', 'calm', 'cautious',
+ 'cold', 'confident', 'confused', 'contemplative', 'contented',
+ 'cranky', 'crazy', 'creative', 'curious', 'dejected',
+ 'depressed', 'disappointed', 'disgusted', 'dismayed',
+ 'distracted', 'embarrassed', 'envious', 'excited',
+ 'flirtatious', 'frustrated', 'grateful', 'grieving', 'grumpy',
+ 'guilty', 'happy', 'hopeful', 'hot', 'humbled', 'humiliated',
+ 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love',
+ 'indignant', 'interested', 'intoxicated', 'invincible',
+ 'jealous', 'lonely', 'lost', 'lucky', 'mean', 'moody',
+ 'nervous', 'neutral', 'offended', 'outraged', 'playful',
+ 'proud', 'relaxed', 'relieved', 'remorseful', 'restless',
+ 'sad', 'sarcastic', 'satisfied', 'serious', 'shocked',
+ 'shy', 'sick', 'sleepy', 'spontaneous', 'stressed', 'strong',
+ 'surprised', 'thankful', 'thirsty', 'tired', 'undefined',
+ 'weak', 'worried'])
+
+ def set_value(self, value):
+ self.del_value()
+ if value in self.moods:
+ self._set_sub_text(value, '', keep=True)
+ else:
+ raise ValueError('Unknown mood value')
+
+ def get_value(self):
+ for child in self.xml:
+ if child.tag.startswith('{%s}' % self.namespace):
+ elem_name = child.tag.split('}')[-1]
+ if elem_name in self.moods:
+ return elem_name
+ return ''
+
+ def del_value(self):
+ curr_value = self.get_value()
+ if curr_value:
+ self._set_sub_text(curr_value, '', keep=False)
diff --git a/slixmpp/plugins/xep_0107/user_mood.py b/slixmpp/plugins/xep_0107/user_mood.py
new file mode 100644
index 00000000..c56d15fa
--- /dev/null
+++ b/slixmpp/plugins/xep_0107/user_mood.py
@@ -0,0 +1,85 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Message
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0107 import stanza, UserMood
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0107(BasePlugin):
+
+ """
+ XEP-0107: User Mood
+ """
+
+ name = 'xep_0107'
+ description = 'XEP-0107: User Mood'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, UserMood)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=UserMood.namespace)
+ self.xmpp['xep_0163'].remove_interest(UserMood.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_mood', UserMood)
+
+ def publish_mood(self, value=None, text=None, options=None, ifrom=None,
+ callback=None, timeout=None, timeout_callback=None):
+ """
+ Publish the user's current mood.
+
+ Arguments:
+ value -- The name of the mood to publish.
+ text -- Optional natural-language description or reason
+ for the mood.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ mood = UserMood()
+ mood['value'] = value
+ mood['text'] = text
+ self.xmpp['xep_0163'].publish(mood, node=UserMood.namespace,
+ options=options, ifrom=ifrom,
+ callback=callback, timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None,
+ timeout_callback=None):
+ """
+ Clear existing user mood information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ mood = UserMood()
+ self.xmpp['xep_0163'].publish(mood, node=UserMood.namespace,
+ ifrom=ifrom, callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0108/__init__.py b/slixmpp/plugins/xep_0108/__init__.py
new file mode 100644
index 00000000..54cc5ddd
--- /dev/null
+++ b/slixmpp/plugins/xep_0108/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0108 import stanza
+from slixmpp.plugins.xep_0108.stanza import UserActivity
+from slixmpp.plugins.xep_0108.user_activity import XEP_0108
+
+
+register_plugin(XEP_0108)
diff --git a/slixmpp/plugins/xep_0108/stanza.py b/slixmpp/plugins/xep_0108/stanza.py
new file mode 100644
index 00000000..d65dfdf9
--- /dev/null
+++ b/slixmpp/plugins/xep_0108/stanza.py
@@ -0,0 +1,83 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class UserActivity(ElementBase):
+
+ name = 'activity'
+ namespace = 'http://jabber.org/protocol/activity'
+ plugin_attrib = 'activity'
+ interfaces = set(['value', 'text'])
+ sub_interfaces = set(['text'])
+ general = set(['doing_chores', 'drinking', 'eating', 'exercising',
+ 'grooming', 'having_appointment', 'inactive', 'relaxing',
+ 'talking', 'traveling', 'undefined', 'working'])
+ specific = set(['at_the_spa', 'brushing_teeth', 'buying_groceries',
+ 'cleaning', 'coding', 'commuting', 'cooking', 'cycling',
+ 'dancing', 'day_off', 'doing_maintenance',
+ 'doing_the_dishes', 'doing_the_laundry', 'driving',
+ 'fishing', 'gaming', 'gardening', 'getting_a_haircut',
+ 'going_out', 'hanging_out', 'having_a_beer',
+ 'having_a_snack', 'having_breakfast', 'having_coffee',
+ 'having_dinner', 'having_lunch', 'having_tea', 'hiding',
+ 'hiking', 'in_a_car', 'in_a_meeting', 'in_real_life',
+ 'jogging', 'on_a_bus', 'on_a_plane', 'on_a_train',
+ 'on_a_trip', 'on_the_phone', 'on_vacation',
+ 'on_video_phone', 'other', 'partying', 'playing_sports',
+ 'praying', 'reading', 'rehearsing', 'running',
+ 'running_an_errand', 'scheduled_holiday', 'shaving',
+ 'shopping', 'skiing', 'sleeping', 'smoking',
+ 'socializing', 'studying', 'sunbathing', 'swimming',
+ 'taking_a_bath', 'taking_a_shower', 'thinking',
+ 'walking', 'walking_the_dog', 'watching_a_movie',
+ 'watching_tv', 'working_out', 'writing'])
+
+ def set_value(self, value):
+ self.del_value()
+ general = value
+ specific = None
+ if isinstance(value, tuple) or isinstance(value, list):
+ general = value[0]
+ specific = value[1]
+
+ if general in self.general:
+ gen_xml = ET.Element('{%s}%s' % (self.namespace, general))
+ if specific:
+ spec_xml = ET.Element('{%s}%s' % (self.namespace, specific))
+ if specific in self.specific:
+ gen_xml.append(spec_xml)
+ else:
+ raise ValueError('Unknown specific activity')
+ self.xml.append(gen_xml)
+ else:
+ raise ValueError('Unknown general activity')
+
+ def get_value(self):
+ general = None
+ specific = None
+ gen_xml = None
+ for child in self.xml:
+ if child.tag.startswith('{%s}' % self.namespace):
+ elem_name = child.tag.split('}')[-1]
+ if elem_name in self.general:
+ general = elem_name
+ gen_xml = child
+ if gen_xml is not None:
+ for child in gen_xml:
+ if child.tag.startswith('{%s}' % self.namespace):
+ elem_name = child.tag.split('}')[-1]
+ if elem_name in self.specific:
+ specific = elem_name
+ return (general, specific)
+
+ def del_value(self):
+ curr_value = self.get_value()
+ if curr_value[0]:
+ self._set_sub_text(curr_value[0], '', keep=False)
diff --git a/slixmpp/plugins/xep_0108/user_activity.py b/slixmpp/plugins/xep_0108/user_activity.py
new file mode 100644
index 00000000..502dfae0
--- /dev/null
+++ b/slixmpp/plugins/xep_0108/user_activity.py
@@ -0,0 +1,82 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0108 import stanza, UserActivity
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0108(BasePlugin):
+
+ """
+ XEP-0108: User Activity
+ """
+
+ name = 'xep_0108'
+ description = 'XEP-0108: User Activity'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=UserActivity.namespace)
+ self.xmpp['xep_0163'].remove_interest(UserActivity.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_activity', UserActivity)
+
+ def publish_activity(self, general, specific=None, text=None,
+ options=None, ifrom=None, callback=None,
+ timeout=None, timeout_callback=None):
+ """
+ Publish the user's current activity.
+
+ Arguments:
+ general -- The required general category of the activity.
+ specific -- Optional specific activity being done as part
+ of the general category.
+ text -- Optional natural-language description or reason
+ for the activity.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ activity = UserActivity()
+ activity['value'] = (general, specific)
+ activity['text'] = text
+ self.xmpp['xep_0163'].publish(activity, node=UserActivity.namespace,
+ options=options, ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None,
+ timeout_callback=None):
+ """
+ Clear existing user activity information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ activity = UserActivity()
+ self.xmpp['xep_0163'].publish(activity, node=UserActivity.namespace,
+ ifrom=ifrom, callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0115/__init__.py b/slixmpp/plugins/xep_0115/__init__.py
new file mode 100644
index 00000000..51c437c6
--- /dev/null
+++ b/slixmpp/plugins/xep_0115/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0115.stanza import Capabilities
+from slixmpp.plugins.xep_0115.static import StaticCaps
+from slixmpp.plugins.xep_0115.caps import XEP_0115
+
+
+register_plugin(XEP_0115)
+
+
+# Retain some backwards compatibility
+xep_0115 = XEP_0115
diff --git a/slixmpp/plugins/xep_0115/caps.py b/slixmpp/plugins/xep_0115/caps.py
new file mode 100644
index 00000000..c6f9ea10
--- /dev/null
+++ b/slixmpp/plugins/xep_0115/caps.py
@@ -0,0 +1,334 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import hashlib
+import base64
+
+from slixmpp import __version__
+from slixmpp.stanza import StreamFeatures, Presence, Iq
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp import asyncio
+from slixmpp.exceptions import XMPPError, IqError, IqTimeout
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0115 import stanza, StaticCaps
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0115(BasePlugin):
+
+ """
+ XEP-0115: Entity Capabalities
+ """
+
+ name = 'xep_0115'
+ description = 'XEP-0115: Entity Capabilities'
+ dependencies = set(['xep_0030', 'xep_0128', 'xep_0004'])
+ stanza = stanza
+ default_config = {
+ 'hash': 'sha-1',
+ 'caps_node': None,
+ 'broadcast': True
+ }
+
+ def plugin_init(self):
+ self.hashes = {'sha-1': hashlib.sha1,
+ 'sha1': hashlib.sha1,
+ 'md5': hashlib.md5}
+
+ if self.caps_node is None:
+ self.caps_node = 'http://slixmpp.com/ver/%s' % __version__
+
+ register_stanza_plugin(Presence, stanza.Capabilities)
+ register_stanza_plugin(StreamFeatures, stanza.Capabilities)
+
+ self._disco_ops = ['cache_caps',
+ 'get_caps',
+ 'assign_verstring',
+ 'get_verstring',
+ 'supports',
+ 'has_identity']
+
+ self.xmpp.register_handler(
+ Callback('Entity Capabilites',
+ StanzaPath('presence/caps'),
+ self._handle_caps))
+
+ self.xmpp.add_filter('out', self._filter_add_caps)
+
+ self.xmpp.add_event_handler('entity_caps', self._process_caps)
+
+ if not self.xmpp.is_component:
+ self.xmpp.register_feature('caps',
+ self._handle_caps_feature,
+ restart=False,
+ order=10010)
+
+ disco = self.xmpp['xep_0030']
+ self.static = StaticCaps(self.xmpp, disco.static)
+
+ for op in self._disco_ops:
+ self.api.register(getattr(self.static, op), op, default=True)
+
+ for op in ('supports', 'has_identity'):
+ self.xmpp['xep_0030'].api.register(getattr(self.static, op), op)
+
+ self._run_node_handler = disco._run_node_handler
+
+ disco.cache_caps = self.cache_caps
+ disco.update_caps = self.update_caps
+ disco.assign_verstring = self.assign_verstring
+ disco.get_verstring = self.get_verstring
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace)
+ self.xmpp.del_filter('out', self._filter_add_caps)
+ self.xmpp.del_event_handler('entity_caps', self._process_caps)
+ self.xmpp.remove_handler('Entity Capabilities')
+ if not self.xmpp.is_component:
+ self.xmpp.unregister_feature('caps', 10010)
+ for op in ('supports', 'has_identity'):
+ self.xmpp['xep_0030'].restore_defaults(op)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
+
+ def _filter_add_caps(self, stanza):
+ if not isinstance(stanza, Presence) or not self.broadcast:
+ return stanza
+
+ if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'):
+ return stanza
+
+ ver = self.get_verstring(stanza['from'])
+ if ver:
+ stanza['caps']['node'] = self.caps_node
+ stanza['caps']['hash'] = self.hash
+ stanza['caps']['ver'] = ver
+ return stanza
+
+ def _handle_caps(self, presence):
+ if not self.xmpp.is_component:
+ if presence['from'] == self.xmpp.boundjid:
+ return
+ self.xmpp.event('entity_caps', presence)
+
+ def _handle_caps_feature(self, features):
+ # We already have a method to process presence with
+ # caps, so wrap things up and use that.
+ p = Presence()
+ p['from'] = self.xmpp.boundjid.domain
+ p.append(features['caps'])
+ self.xmpp.features.add('caps')
+
+ self.xmpp.event('entity_caps', p)
+
+ @asyncio.coroutine
+ def _process_caps(self, pres):
+ if not pres['caps']['hash']:
+ log.debug("Received unsupported legacy caps: %s, %s, %s",
+ pres['caps']['node'],
+ pres['caps']['ver'],
+ pres['caps']['ext'])
+ self.xmpp.event('entity_caps_legacy', pres)
+ return
+
+ ver = pres['caps']['ver']
+
+ existing_verstring = self.get_verstring(pres['from'].full)
+ if str(existing_verstring) == str(ver):
+ return
+
+ existing_caps = self.get_caps(verstring=ver)
+ if existing_caps is not None:
+ self.assign_verstring(pres['from'], ver)
+ return
+
+ if pres['caps']['hash'] not in self.hashes:
+ try:
+ log.debug("Unknown caps hash: %s", pres['caps']['hash'])
+ self.xmpp['xep_0030'].get_info(jid=pres['from'])
+ return
+ except XMPPError:
+ return
+
+ log.debug("New caps verification string: %s", ver)
+ try:
+ node = '%s#%s' % (pres['caps']['node'], ver)
+ caps = yield from self.xmpp['xep_0030'].get_info(pres['from'], node,
+ coroutine=True)
+
+ if isinstance(caps, Iq):
+ caps = caps['disco_info']
+
+ if self._validate_caps(caps, pres['caps']['hash'],
+ pres['caps']['ver']):
+ self.assign_verstring(pres['from'], pres['caps']['ver'])
+ except XMPPError:
+ log.debug("Could not retrieve disco#info results for caps for %s", node)
+
+ def _validate_caps(self, caps, hash, check_verstring):
+ # Check Identities
+ full_ids = caps.get_identities(dedupe=False)
+ deduped_ids = caps.get_identities()
+ if len(full_ids) != len(deduped_ids):
+ log.debug("Duplicate disco identities found, invalid for caps")
+ return False
+
+ # Check Features
+ full_features = caps.get_features(dedupe=False)
+ deduped_features = caps.get_features()
+ if len(full_features) != len(deduped_features):
+ log.debug("Duplicate disco features found, invalid for caps")
+ return False
+
+ # Check Forms
+ form_types = []
+ deduped_form_types = set()
+ for stanza in caps['substanzas']:
+ if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ log.debug("Non form extension found, ignoring for caps")
+ caps.xml.remove(stanza.xml)
+ continue
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
+ form_types.append(f_type)
+ deduped_form_types.add(f_type)
+ if len(form_types) != len(deduped_form_types):
+ log.debug("Duplicated FORM_TYPE values, " + \
+ "invalid for caps")
+ return False
+
+ if len(f_type) > 1:
+ deduped_type = set(f_type)
+ if len(f_type) != len(deduped_type):
+ log.debug("Extra FORM_TYPE data, invalid for caps")
+ return False
+
+ if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
+ log.debug("Field FORM_TYPE type not 'hidden', " + \
+ "ignoring form for caps")
+ caps.xml.remove(stanza.xml)
+ else:
+ log.debug("No FORM_TYPE found, ignoring form for caps")
+ caps.xml.remove(stanza.xml)
+
+ verstring = self.generate_verstring(caps, hash)
+ if verstring != check_verstring:
+ log.debug("Verification strings do not match: %s, %s" % (
+ verstring, check_verstring))
+ return False
+
+ self.cache_caps(verstring, caps)
+ return True
+
+ def generate_verstring(self, info, hash):
+ hash = self.hashes.get(hash, None)
+ if hash is None:
+ return None
+
+ S = ''
+
+ # Convert None to '' in the identities
+ def clean_identity(id):
+ return map(lambda i: i or '', id)
+ identities = map(clean_identity, info['identities'])
+
+ identities = sorted(('/'.join(i) for i in identities))
+ features = sorted(info['features'])
+
+ S += '<'.join(identities) + '<'
+ S += '<'.join(features) + '<'
+
+ form_types = {}
+
+ for stanza in info['substanzas']:
+ if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = stanza['values']['FORM_TYPE']
+ if len(f_type):
+ f_type = f_type[0]
+ if f_type not in form_types:
+ form_types[f_type] = []
+ form_types[f_type].append(stanza)
+
+ sorted_forms = sorted(form_types.keys())
+ for f_type in sorted_forms:
+ for form in form_types[f_type]:
+ S += '%s<' % f_type
+ fields = sorted(form['fields'].keys())
+ fields.remove('FORM_TYPE')
+ for field in fields:
+ S += '%s<' % field
+ vals = form['fields'][field].get_value(convert=False)
+ if vals is None:
+ S += '<'
+ else:
+ if not isinstance(vals, list):
+ vals = [vals]
+ S += '<'.join(sorted(vals)) + '<'
+
+ binary = hash(S.encode('utf8')).digest()
+ return base64.b64encode(binary).decode('utf-8')
+
+ @asyncio.coroutine
+ def update_caps(self, jid=None, node=None, preserve=False):
+ try:
+ info = yield from self.xmpp['xep_0030'].get_info(jid, node, local=True)
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ ver = self.generate_verstring(info, self.hash)
+ self.xmpp['xep_0030'].set_info(
+ jid=jid,
+ node='%s#%s' % (self.caps_node, ver),
+ info=info)
+ self.cache_caps(ver, info)
+ self.assign_verstring(jid, ver)
+
+ if self.xmpp.sessionstarted and self.broadcast:
+ if self.xmpp.is_component or preserve:
+ for contact in self.xmpp.roster[jid]:
+ self.xmpp.roster[jid][contact].send_last_presence()
+ else:
+ self.xmpp.roster[jid].send_last_presence()
+ except XMPPError:
+ return
+
+ def get_verstring(self, jid=None):
+ if jid in ('', None):
+ jid = self.xmpp.boundjid.full
+ if isinstance(jid, JID):
+ jid = jid.full
+ return self.api['get_verstring'](jid)
+
+ def assign_verstring(self, jid=None, verstring=None):
+ if jid in (None, ''):
+ jid = self.xmpp.boundjid.full
+ if isinstance(jid, JID):
+ jid = jid.full
+ return self.api['assign_verstring'](jid, args={
+ 'verstring': verstring})
+
+ def cache_caps(self, verstring=None, info=None):
+ data = {'verstring': verstring, 'info': info}
+ return self.api['cache_caps'](args=data)
+
+ def get_caps(self, jid=None, verstring=None):
+ if verstring is None:
+ if jid is not None:
+ verstring = self.get_verstring(jid)
+ else:
+ return None
+ if isinstance(jid, JID):
+ jid = jid.full
+ data = {'verstring': verstring}
+ return self.api['get_caps'](jid, args=data)
diff --git a/slixmpp/plugins/xep_0115/stanza.py b/slixmpp/plugins/xep_0115/stanza.py
new file mode 100644
index 00000000..36fb173c
--- /dev/null
+++ b/slixmpp/plugins/xep_0115/stanza.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Capabilities(ElementBase):
+
+ namespace = 'http://jabber.org/protocol/caps'
+ name = 'c'
+ plugin_attrib = 'caps'
+ interfaces = set(('hash', 'node', 'ver', 'ext'))
diff --git a/slixmpp/plugins/xep_0115/static.py b/slixmpp/plugins/xep_0115/static.py
new file mode 100644
index 00000000..0d1caa01
--- /dev/null
+++ b/slixmpp/plugins/xep_0115/static.py
@@ -0,0 +1,146 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.xmlstream import JID
+from slixmpp.exceptions import IqError, IqTimeout
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticCaps(object):
+
+ """
+ Extend the default StaticDisco implementation to provide
+ support for extended identity information.
+ """
+
+ def __init__(self, xmpp, static):
+ """
+ Augment the default XEP-0030 static handler object.
+
+ Arguments:
+ static -- The default static XEP-0030 handler object.
+ """
+ self.xmpp = xmpp
+ self.disco = self.xmpp['xep_0030']
+ self.caps = self.xmpp['xep_0115']
+ self.static = static
+ self.ver_cache = {}
+ self.jid_vers = {}
+
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and feature in info['features']:
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ return feature in info['disco_info']['features']
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Slixmpp instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ trunc = lambda i: (i[0], i[1], i[2])
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and identity in map(trunc, info['identities']):
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def cache_caps(self, jid, node, ifrom, data):
+ with self.static.lock:
+ verstring = data.get('verstring', None)
+ info = data.get('info', None)
+ if not verstring or not info:
+ return
+ self.ver_cache[verstring] = info
+
+ def assign_verstring(self, jid, node, ifrom, data):
+ with self.static.lock:
+ if isinstance(jid, JID):
+ jid = jid.full
+ self.jid_vers[jid] = data.get('verstring', None)
+
+ def get_verstring(self, jid, node, ifrom, data):
+ with self.static.lock:
+ return self.jid_vers.get(jid, None)
+
+ def get_caps(self, jid, node, ifrom, data):
+ with self.static.lock:
+ return self.ver_cache.get(data.get('verstring', None), None)
diff --git a/slixmpp/plugins/xep_0118/__init__.py b/slixmpp/plugins/xep_0118/__init__.py
new file mode 100644
index 00000000..7ad48998
--- /dev/null
+++ b/slixmpp/plugins/xep_0118/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0118 import stanza
+from slixmpp.plugins.xep_0118.stanza import UserTune
+from slixmpp.plugins.xep_0118.user_tune import XEP_0118
+
+
+register_plugin(XEP_0118)
diff --git a/slixmpp/plugins/xep_0118/stanza.py b/slixmpp/plugins/xep_0118/stanza.py
new file mode 100644
index 00000000..4f5a1795
--- /dev/null
+++ b/slixmpp/plugins/xep_0118/stanza.py
@@ -0,0 +1,25 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class UserTune(ElementBase):
+
+ name = 'tune'
+ namespace = 'http://jabber.org/protocol/tune'
+ plugin_attrib = 'tune'
+ interfaces = set(['artist', 'length', 'rating', 'source',
+ 'title', 'track', 'uri'])
+ sub_interfaces = interfaces
+
+ def set_length(self, value):
+ self._set_sub_text('length', str(value))
+
+ def set_rating(self, value):
+ self._set_sub_text('rating', str(value))
diff --git a/slixmpp/plugins/xep_0118/user_tune.py b/slixmpp/plugins/xep_0118/user_tune.py
new file mode 100644
index 00000000..0882a5ba
--- /dev/null
+++ b/slixmpp/plugins/xep_0118/user_tune.py
@@ -0,0 +1,92 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0118 import stanza, UserTune
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0118(BasePlugin):
+
+ """
+ XEP-0118: User Tune
+ """
+
+ name = 'xep_0118'
+ description = 'XEP-0118: User Tune'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=UserTune.namespace)
+ self.xmpp['xep_0163'].remove_interest(UserTune.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_tune', UserTune)
+
+ def publish_tune(self, artist=None, length=None, rating=None, source=None,
+ title=None, track=None, uri=None, options=None,
+ ifrom=None, callback=None, timeout=None, timeout_callback=None):
+ """
+ Publish the user's current tune.
+
+ Arguments:
+ artist -- The artist or performer of the song.
+ length -- The length of the song in seconds.
+ rating -- The user's rating of the song (from 1 to 10)
+ source -- The album name, website, or other source of the song.
+ title -- The title of the song.
+ track -- The song's track number, or other unique identifier.
+ uri -- A URL to more information about the song.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ tune = UserTune()
+ tune['artist'] = artist
+ tune['length'] = length
+ tune['rating'] = rating
+ tune['source'] = source
+ tune['title'] = title
+ tune['track'] = track
+ tune['uri'] = uri
+ return self.xmpp['xep_0163'].publish(tune,
+ node=UserTune.namespace,
+ options=options,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None):
+ """
+ Clear existing user tune information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ tune = UserTune()
+ return self.xmpp['xep_0163'].publish(tune,
+ node=UserTune.namespace,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0122/__init__.py b/slixmpp/plugins/xep_0122/__init__.py
new file mode 100644
index 00000000..76ca80b2
--- /dev/null
+++ b/slixmpp/plugins/xep_0122/__init__.py
@@ -0,0 +1,11 @@
+
+from slixmpp.plugins.base import register_plugin
+from slixmpp.plugins.xep_0122.stanza import FormValidation
+from slixmpp.plugins.xep_0122.data_validation import XEP_0122
+
+
+register_plugin(XEP_0122)
+
+
+# Retain some backwards compatibility
+xep_0122 = XEP_0122
diff --git a/slixmpp/plugins/xep_0122/data_validation.py b/slixmpp/plugins/xep_0122/data_validation.py
new file mode 100644
index 00000000..6129db51
--- /dev/null
+++ b/slixmpp/plugins/xep_0122/data_validation.py
@@ -0,0 +1,19 @@
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0004 import stanza
+from slixmpp.plugins.xep_0004.stanza import FormField
+from slixmpp.plugins.xep_0122.stanza import FormValidation
+
+
+class XEP_0122(BasePlugin):
+ """
+ XEP-0122: Data Forms
+ """
+
+ name = 'xep_0122'
+ description = 'XEP-0122: Data Forms Validation'
+ dependencies = set(['xep_0004'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(FormField, FormValidation)
diff --git a/slixmpp/plugins/xep_0122/stanza.py b/slixmpp/plugins/xep_0122/stanza.py
new file mode 100644
index 00000000..9f1c423d
--- /dev/null
+++ b/slixmpp/plugins/xep_0122/stanza.py
@@ -0,0 +1,93 @@
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class FormValidation(ElementBase):
+ """
+ Validation values for form fields.
+
+ Example:
+
+ <field var='evt.date' type='text-single' label='Event Date/Time'>
+ <validate xmlns='http://jabber.org/protocol/xdata-validate'
+ datatype='xs:dateTime'/>
+ <value>2003-10-06T11:22:00-07:00</value>
+ </field>
+
+ Questions:
+ Should this look at the datatype value and convert the range values as appropriate?
+ Should this stanza provide a pass/fail for a value from the field, or convert field value to datatype?
+ """
+
+ namespace = 'http://jabber.org/protocol/xdata-validate'
+ name = 'validate'
+ plugin_attrib = 'validate'
+ interfaces = {'datatype', 'basic', 'open', 'range', 'regex', }
+ sub_interfaces = {'basic', 'open', 'range', 'regex', }
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def _add_field(self, name):
+ self.remove_all()
+ item_xml = ET.Element('{%s}%s' % (self.namespace, name))
+ self.xml.append(item_xml)
+ return item_xml
+
+ def set_basic(self, value):
+ if value:
+ self._add_field('basic')
+ else:
+ del self['basic']
+
+ def set_open(self, value):
+ if value:
+ self._add_field('open')
+ else:
+ del self['open']
+
+ def set_regex(self, regex):
+ if regex:
+ _regex = self._add_field('regex')
+ _regex.text = regex
+ else:
+ del self['regex']
+
+ def set_range(self, value, minimum=None, maximum=None):
+ if value:
+ _range = self._add_field('range')
+ _range.attrib['min'] = str(minimum)
+ _range.attrib['max'] = str(maximum)
+ else:
+ del self['range']
+
+ def remove_all(self, except_tag=None):
+ for a in self.sub_interfaces:
+ if a != except_tag:
+ del self[a]
+
+ def get_basic(self):
+ present = self.xml.find('{%s}basic' % self.namespace)
+ return present is not None
+
+ def get_open(self):
+ present = self.xml.find('{%s}open' % self.namespace)
+ return present is not None
+
+ def get_regex(self):
+ present = self.xml.find('{%s}regex' % self.namespace)
+ if present is not None:
+ return present.text
+
+ return False
+
+ def get_range(self):
+ present = self.xml.find('{%s}range' % self.namespace)
+ if present is not None:
+ attributes = present.attrib
+ return_value = dict()
+ if 'min' in attributes:
+ return_value['minimum'] = attributes['min']
+ if 'max' in attributes:
+ return_value['maximum'] = attributes['max']
+ return return_value
+
+ return False
diff --git a/slixmpp/plugins/xep_0128/__init__.py b/slixmpp/plugins/xep_0128/__init__.py
new file mode 100644
index 00000000..fb6dbf7c
--- /dev/null
+++ b/slixmpp/plugins/xep_0128/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0128.static import StaticExtendedDisco
+from slixmpp.plugins.xep_0128.extended_disco import XEP_0128
+
+
+register_plugin(XEP_0128)
+
+
+# Retain some backwards compatibility
+xep_0128 = XEP_0128
diff --git a/slixmpp/plugins/xep_0128/extended_disco.py b/slixmpp/plugins/xep_0128/extended_disco.py
new file mode 100644
index 00000000..5cc1d35a
--- /dev/null
+++ b/slixmpp/plugins/xep_0128/extended_disco.py
@@ -0,0 +1,99 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp import Iq
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0004 import Form
+from slixmpp.plugins.xep_0030 import DiscoInfo
+from slixmpp.plugins.xep_0128 import StaticExtendedDisco
+
+
+class XEP_0128(BasePlugin):
+
+ """
+ XEP-0128: Service Discovery Extensions
+
+ Allow the use of data forms to add additional identity
+ information to disco#info results.
+
+ Also see <http://www.xmpp.org/extensions/xep-0128.html>.
+
+ Attributes:
+ disco -- A reference to the XEP-0030 plugin.
+ static -- Object containing the default set of static
+ node handlers.
+ xmpp -- The main Slixmpp object.
+
+ Methods:
+ set_extended_info -- Set extensions to a disco#info result.
+ add_extended_info -- Add an extension to a disco#info result.
+ del_extended_info -- Remove all extensions from a disco#info result.
+ """
+
+ name = 'xep_0128'
+ description = 'XEP-0128: Service Discovery Extensions'
+ dependencies = set(['xep_0030', 'xep_0004'])
+
+ def plugin_init(self):
+ """Start the XEP-0128 plugin."""
+ self._disco_ops = ['set_extended_info',
+ 'add_extended_info',
+ 'del_extended_info']
+
+ register_stanza_plugin(DiscoInfo, Form, iterable=True)
+
+ self.disco = self.xmpp['xep_0030']
+ self.static = StaticExtendedDisco(self.disco.static)
+
+ self.disco.set_extended_info = self.set_extended_info
+ self.disco.add_extended_info = self.add_extended_info
+ self.disco.del_extended_info = self.del_extended_info
+
+ for op in self._disco_ops:
+ self.api.register(getattr(self.static, op), op, default=True)
+
+ def set_extended_info(self, jid=None, node=None, **kwargs):
+ """
+ Set additional, extended identity information to a node.
+
+ Replaces any existing extended information.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ data -- Either a form, or a list of forms to use
+ as extended information, replacing any
+ existing extensions.
+ """
+ self.api['set_extended_info'](jid, node, None, kwargs)
+
+ def add_extended_info(self, jid=None, node=None, **kwargs):
+ """
+ Add additional, extended identity information to a node.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ data -- Either a form, or a list of forms to add
+ as extended information.
+ """
+ self.api['add_extended_info'](jid, node, None, kwargs)
+
+ def del_extended_info(self, jid=None, node=None, **kwargs):
+ """
+ Remove all extended identity information to a node.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ """
+ self.api['del_extended_info'](jid, node, None, kwargs)
diff --git a/slixmpp/plugins/xep_0128/static.py b/slixmpp/plugins/xep_0128/static.py
new file mode 100644
index 00000000..ab1ea590
--- /dev/null
+++ b/slixmpp/plugins/xep_0128/static.py
@@ -0,0 +1,73 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.plugins.xep_0030 import StaticDisco
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticExtendedDisco(object):
+
+ """
+ Extend the default StaticDisco implementation to provide
+ support for extended identity information.
+ """
+
+ def __init__(self, static):
+ """
+ Augment the default XEP-0030 static handler object.
+
+ Arguments:
+ static -- The default static XEP-0030 handler object.
+ """
+ self.static = static
+
+ def set_extended_info(self, jid, node, ifrom, data):
+ """
+ Replace the extended identity data for a JID/node combination.
+
+ The data parameter may provide:
+ data -- Either a single data form, or a list of data forms.
+ """
+ with self.static.lock:
+ self.del_extended_info(jid, node, ifrom, data)
+ self.add_extended_info(jid, node, ifrom, data)
+
+ def add_extended_info(self, jid, node, ifrom, data):
+ """
+ Add additional extended identity data for a JID/node combination.
+
+ The data parameter may provide:
+ data -- Either a single data form, or a list of data forms.
+ """
+ with self.static.lock:
+ self.static.add_node(jid, node)
+
+ forms = data.get('data', [])
+ if not isinstance(forms, list):
+ forms = [forms]
+
+ info = self.static.get_node(jid, node)['info']
+ for form in forms:
+ info.append(form)
+
+ def del_extended_info(self, jid, node, ifrom, data):
+ """
+ Replace the extended identity data for a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.static.lock:
+ if self.static.node_exists(jid, node):
+ info = self.static.get_node(jid, node)['info']
+ for form in info['substanza']:
+ info.xml.remove(form.xml)
diff --git a/slixmpp/plugins/xep_0131/__init__.py b/slixmpp/plugins/xep_0131/__init__.py
new file mode 100644
index 00000000..4151cc72
--- /dev/null
+++ b/slixmpp/plugins/xep_0131/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0131 import stanza
+from slixmpp.plugins.xep_0131.stanza import Headers
+from slixmpp.plugins.xep_0131.headers import XEP_0131
+
+
+register_plugin(XEP_0131)
diff --git a/slixmpp/plugins/xep_0131/headers.py b/slixmpp/plugins/xep_0131/headers.py
new file mode 100644
index 00000000..81fc9188
--- /dev/null
+++ b/slixmpp/plugins/xep_0131/headers.py
@@ -0,0 +1,41 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Message, Presence
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0131 import stanza
+from slixmpp.plugins.xep_0131.stanza import Headers
+
+
+class XEP_0131(BasePlugin):
+
+ name = 'xep_0131'
+ description = 'XEP-0131: Stanza Headers and Internet Metadata'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ default_config = {
+ 'supported_headers': set()
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, Headers)
+ register_stanza_plugin(Presence, Headers)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Headers.namespace)
+ for header in self.supported_headers:
+ self.xmpp['xep_0030'].del_feature(
+ feature='%s#%s' % (Headers.namespace, header))
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Headers.namespace)
+ for header in self.supported_headers:
+ self.xmpp['xep_0030'].add_feature('%s#%s' % (
+ Headers.namespace,
+ header))
diff --git a/slixmpp/plugins/xep_0131/stanza.py b/slixmpp/plugins/xep_0131/stanza.py
new file mode 100644
index 00000000..cbbe61a7
--- /dev/null
+++ b/slixmpp/plugins/xep_0131/stanza.py
@@ -0,0 +1,51 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from collections import OrderedDict
+from slixmpp.xmlstream import ET, ElementBase
+
+
+class Headers(ElementBase):
+ name = 'headers'
+ namespace = 'http://jabber.org/protocol/shim'
+ plugin_attrib = 'headers'
+ interfaces = set(['headers'])
+ is_extension = True
+
+ def get_headers(self):
+ result = OrderedDict()
+ headers = self.xml.findall('{%s}header' % self.namespace)
+ for header in headers:
+ name = header.attrib.get('name', '')
+ value = header.text
+ if name in result:
+ if not isinstance(result[name], set):
+ result[name] = [result[name]]
+ else:
+ result[name] = []
+ result[name].add(value)
+ else:
+ result[name] = value
+ return result
+
+ def set_headers(self, values):
+ self.del_headers()
+ for name in values:
+ vals = values[name]
+ if not isinstance(vals, (list, set)):
+ vals = [values[name]]
+ for value in vals:
+ header = ET.Element('{%s}header' % self.namespace)
+ header.attrib['name'] = name
+ header.text = value
+ self.xml.append(header)
+
+ def del_headers(self):
+ headers = self.xml.findall('{%s}header' % self.namespace)
+ for header in headers:
+ self.xml.remove(header)
diff --git a/slixmpp/plugins/xep_0133.py b/slixmpp/plugins/xep_0133.py
new file mode 100644
index 00000000..a1eb9e02
--- /dev/null
+++ b/slixmpp/plugins/xep_0133.py
@@ -0,0 +1,53 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.plugins import BasePlugin, register_plugin
+
+
+class XEP_0133(BasePlugin):
+
+ name = 'xep_0133'
+ description = 'XEP-0133: Service Administration'
+ dependencies = set(['xep_0030', 'xep_0004', 'xep_0050'])
+ commands = set(['add-user', 'delete-user', 'disable-user',
+ 'reenable-user', 'end-user-session', 'get-user-password',
+ 'change-user-password', 'get-user-roster',
+ 'get-user-lastlogin', 'user-stats', 'edit-blacklist',
+ 'edit-whitelist', 'get-registered-users-num',
+ 'get-disabled-users-num', 'get-online-users-num',
+ 'get-active-users-num', 'get-idle-users-num',
+ 'get-registered-users-list', 'get-disabled-users-list',
+ 'get-online-users-list', 'get-online-users',
+ 'get-active-users', 'get-idle-userslist', 'announce',
+ 'set-motd', 'edit-motd', 'delete-motd', 'set-welcome',
+ 'delete-welcome', 'edit-admin', 'restart', 'shutdown'])
+
+ def get_commands(self, jid=None, **kwargs):
+ if jid is None:
+ jid = self.xmpp.boundjid.server
+ return self.xmpp['xep_0050'].get_commands(jid, **kwargs)
+
+
+def create_command(name):
+ def admin_command(self, jid=None, session=None, ifrom=None):
+ if jid is None:
+ jid = self.xmpp.boundjid.server
+ self.xmpp['xep_0050'].start_command(
+ jid=jid,
+ node='http://jabber.org/protocol/admin#%s' % name,
+ session=session,
+ ifrom=ifrom)
+ return admin_command
+
+
+for cmd in XEP_0133.commands:
+ setattr(XEP_0133, cmd.replace('-', '_'), create_command(cmd))
+
+
+register_plugin(XEP_0133)
diff --git a/slixmpp/plugins/xep_0138.py b/slixmpp/plugins/xep_0138.py
new file mode 100644
index 00000000..049060cf
--- /dev/null
+++ b/slixmpp/plugins/xep_0138.py
@@ -0,0 +1,145 @@
+"""
+ slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import zlib
+
+
+from slixmpp.stanza import StreamFeatures
+from slixmpp.xmlstream import RestartStream, register_stanza_plugin, ElementBase, StanzaBase
+from slixmpp.xmlstream.matcher import *
+from slixmpp.xmlstream.handler import *
+from slixmpp.plugins import BasePlugin, register_plugin
+
+log = logging.getLogger(__name__)
+
+
+class Compression(ElementBase):
+ name = 'compression'
+ namespace = 'http://jabber.org/features/compress'
+ interfaces = set(('methods',))
+ plugin_attrib = 'compression'
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def get_methods(self):
+ methods = []
+ for method in self.xml.findall('{%s}method' % self.namespace):
+ methods.append(method.text)
+ return methods
+
+
+class Compress(StanzaBase):
+ name = 'compress'
+ namespace = 'http://jabber.org/protocol/compress'
+ interfaces = set(('method',))
+ sub_interfaces = interfaces
+ plugin_attrib = 'compress'
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+
+class Compressed(StanzaBase):
+ name = 'compressed'
+ namespace = 'http://jabber.org/protocol/compress'
+ interfaces = set()
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+
+
+
+class ZlibSocket(object):
+
+ def __init__(self, socketobj):
+ self.__socket = socketobj
+ self.compressor = zlib.compressobj()
+ self.decompressor = zlib.decompressobj(zlib.MAX_WBITS)
+
+ def __getattr__(self, name):
+ return getattr(self.__socket, name)
+
+ def send(self, data):
+ sentlen = len(data)
+ data = self.compressor.compress(data)
+ data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
+ log.debug(b'>>> (compressed)' + (data.encode("hex")))
+ #return self.__socket.send(data)
+ sentactuallen = self.__socket.send(data)
+ assert(sentactuallen == len(data))
+
+ return sentlen
+
+ def recv(self, *args, **kwargs):
+ data = self.__socket.recv(*args, **kwargs)
+ log.debug(b'<<< (compressed)' + data.encode("hex"))
+ return self.decompressor.decompress(self.decompressor.unconsumed_tail + data)
+
+
+class XEP_0138(BasePlugin):
+ """
+ XEP-0138: Compression
+ """
+ name = "xep_0138"
+ description = "XEP-0138: Compression"
+ dependencies = set(["xep_0030"])
+
+ def plugin_init(self):
+ self.xep = '0138'
+ self.description = 'Stream Compression (Generic)'
+
+ self.compression_methods = {'zlib': True}
+
+ register_stanza_plugin(StreamFeatures, Compression)
+ self.xmpp.register_stanza(Compress)
+ self.xmpp.register_stanza(Compressed)
+
+ self.xmpp.register_handler(
+ Callback('Compressed',
+ StanzaPath('compressed'),
+ self._handle_compressed,
+ instream=True))
+
+ self.xmpp.register_feature('compression',
+ self._handle_compression,
+ restart=True,
+ order=self.config.get('order', 5))
+
+ def register_compression_method(self, name, handler):
+ self.compression_methods[name] = handler
+
+ def _handle_compression(self, features):
+ for method in features['compression']['methods']:
+ if method in self.compression_methods:
+ log.info('Attempting to use %s compression' % method)
+ c = Compress(self.xmpp)
+ c['method'] = method
+ c.send(now=True)
+ return True
+ return False
+
+ def _handle_compressed(self, stanza):
+ self.xmpp.features.add('compression')
+ log.debug('Stream Compressed!')
+ compressed_socket = ZlibSocket(self.xmpp.socket)
+ self.xmpp.set_socket(compressed_socket)
+ raise RestartStream()
+
+ def _handle_failure(self, stanza):
+ pass
+
+xep_0138 = XEP_0138
+register_plugin(XEP_0138)
diff --git a/slixmpp/plugins/xep_0152/__init__.py b/slixmpp/plugins/xep_0152/__init__.py
new file mode 100644
index 00000000..4e6d3a09
--- /dev/null
+++ b/slixmpp/plugins/xep_0152/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0152 import stanza
+from slixmpp.plugins.xep_0152.stanza import Reachability
+from slixmpp.plugins.xep_0152.reachability import XEP_0152
+
+
+register_plugin(XEP_0152)
diff --git a/slixmpp/plugins/xep_0152/reachability.py b/slixmpp/plugins/xep_0152/reachability.py
new file mode 100644
index 00000000..e6d94b65
--- /dev/null
+++ b/slixmpp/plugins/xep_0152/reachability.py
@@ -0,0 +1,90 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0152 import stanza, Reachability
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0152(BasePlugin):
+
+ """
+ XEP-0152: Reachability Addresses
+ """
+
+ name = 'xep_0152'
+ description = 'XEP-0152: Reachability Addresses'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace)
+ self.xmpp['xep_0163'].remove_interest(Reachability.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('reachability', Reachability)
+
+ def publish_reachability(self, addresses, options=None, ifrom=None,
+ callback=None, timeout=None,
+ timeout_callback=None):
+ """
+ Publish alternative addresses where the user can be reached.
+
+ Arguments:
+ addresses -- A list of dictionaries containing the URI and
+ optional description for each address.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if not isinstance(addresses, (list, tuple)):
+ addresses = [addresses]
+ reach = Reachability()
+ for address in addresses:
+ if not hasattr(address, 'items'):
+ address = {'uri': address}
+
+ addr = stanza.Address()
+ for key, val in address.items():
+ addr[key] = val
+ reach.append(addr)
+ return self.xmpp['xep_0163'].publish(reach,
+ node=Reachability.namespace,
+ options=options,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None):
+ """
+ Clear existing user activity information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ reach = Reachability()
+ return self.xmpp['xep_0163'].publish(reach,
+ node=Reachability.namespace,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0152/stanza.py b/slixmpp/plugins/xep_0152/stanza.py
new file mode 100644
index 00000000..661544e3
--- /dev/null
+++ b/slixmpp/plugins/xep_0152/stanza.py
@@ -0,0 +1,29 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class Reachability(ElementBase):
+ name = 'reach'
+ namespace = 'urn:xmpp:reach:0'
+ plugin_attrib = 'reach'
+ interfaces = set()
+
+
+class Address(ElementBase):
+ name = 'addr'
+ namespace = 'urn:xmpp:reach:0'
+ plugin_attrib = 'address'
+ plugin_multi_attrib = 'addresses'
+ interfaces = set(['uri', 'desc'])
+ lang_interfaces = set(['desc'])
+ sub_interfaces = set(['desc'])
+
+
+register_stanza_plugin(Reachability, Address, iterable=True)
diff --git a/slixmpp/plugins/xep_0153/__init__.py b/slixmpp/plugins/xep_0153/__init__.py
new file mode 100644
index 00000000..378cec90
--- /dev/null
+++ b/slixmpp/plugins/xep_0153/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0153.stanza import VCardTempUpdate
+from slixmpp.plugins.xep_0153.vcard_avatar import XEP_0153
+
+
+register_plugin(XEP_0153)
diff --git a/slixmpp/plugins/xep_0153/stanza.py b/slixmpp/plugins/xep_0153/stanza.py
new file mode 100644
index 00000000..fe8d5e98
--- /dev/null
+++ b/slixmpp/plugins/xep_0153/stanza.py
@@ -0,0 +1,29 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class VCardTempUpdate(ElementBase):
+ name = 'x'
+ namespace = 'vcard-temp:x:update'
+ plugin_attrib = 'vcard_temp_update'
+ interfaces = set(['photo'])
+ sub_interfaces = interfaces
+
+ def set_photo(self, value):
+ if value is not None:
+ self._set_sub_text('photo', value, keep=True)
+ else:
+ self._del_sub('photo')
+
+ def get_photo(self):
+ photo = self.xml.find('{%s}photo' % self.namespace)
+ if photo is None:
+ return None
+ return photo.text
diff --git a/slixmpp/plugins/xep_0153/vcard_avatar.py b/slixmpp/plugins/xep_0153/vcard_avatar.py
new file mode 100644
index 00000000..b2c4caf5
--- /dev/null
+++ b/slixmpp/plugins/xep_0153/vcard_avatar.py
@@ -0,0 +1,178 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import hashlib
+import logging
+
+from slixmpp.stanza import Presence
+from slixmpp.exceptions import XMPPError, IqTimeout
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0153 import stanza, VCardTempUpdate
+from slixmpp import asyncio, future_wrapper
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0153(BasePlugin):
+
+ name = 'xep_0153'
+ description = 'XEP-0153: vCard-Based Avatars'
+ dependencies = set(['xep_0054'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self._hashes = {}
+
+ register_stanza_plugin(Presence, VCardTempUpdate)
+
+ self.xmpp.add_filter('out', self._update_presence)
+
+ self.xmpp.add_event_handler('session_start', self._start)
+ self.xmpp.add_event_handler('session_end', self._end)
+
+ self.xmpp.add_event_handler('presence_available', self._recv_presence)
+ self.xmpp.add_event_handler('presence_dnd', self._recv_presence)
+ self.xmpp.add_event_handler('presence_xa', self._recv_presence)
+ self.xmpp.add_event_handler('presence_chat', self._recv_presence)
+ self.xmpp.add_event_handler('presence_away', self._recv_presence)
+
+ self.api.register(self._set_hash, 'set_hash', default=True)
+ self.api.register(self._get_hash, 'get_hash', default=True)
+ self.api.register(self._reset_hash, 'reset_hash', default=True)
+
+ def plugin_end(self):
+ self.xmpp.del_filter('out', self._update_presence)
+ self.xmpp.del_event_handler('session_start', self._start)
+ self.xmpp.del_event_handler('session_end', self._end)
+ self.xmpp.del_event_handler('presence_available', self._recv_presence)
+ self.xmpp.del_event_handler('presence_dnd', self._recv_presence)
+ self.xmpp.del_event_handler('presence_xa', self._recv_presence)
+ self.xmpp.del_event_handler('presence_chat', self._recv_presence)
+ self.xmpp.del_event_handler('presence_away', self._recv_presence)
+
+ @future_wrapper
+ def set_avatar(self, jid=None, avatar=None, mtype=None, timeout=None,
+ callback=None, timeout_callback=None):
+ if jid is None:
+ jid = self.xmpp.boundjid.bare
+
+ future = asyncio.Future()
+
+ def propagate_timeout_exception(fut):
+ try:
+ fut.done()
+ except IqTimeout as e:
+ future.set_exception(e)
+
+ def custom_callback(result):
+ vcard = result['vcard_temp']
+ vcard['PHOTO']['TYPE'] = mtype
+ vcard['PHOTO']['BINVAL'] = avatar
+
+ new_future = self.xmpp['xep_0054'].publish_vcard(jid=jid,
+ vcard=vcard,
+ timeout=timeout,
+ callback=next_callback,
+ timeout_callback=timeout_callback)
+ new_future.add_done_callback(propagate_timeout_exception)
+
+ def next_callback(result):
+ if result['type'] == 'error':
+ future.set_exception(result)
+ else:
+ self.api['reset_hash'](jid)
+ self.xmpp.roster[jid].send_last_presence()
+
+ future.set_result(result)
+
+ first_future = self.xmpp['xep_0054'].get_vcard(jid, cached=False, timeout=timeout,
+ callback=custom_callback,
+ timeout_callback=timeout_callback)
+ first_future.add_done_callback(propagate_timeout_exception)
+ return future
+
+ @asyncio.coroutine
+ def _start(self, event):
+ try:
+ vcard = yield from self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare)
+ data = vcard['vcard_temp']['PHOTO']['BINVAL']
+ if not data:
+ new_hash = ''
+ else:
+ new_hash = hashlib.sha1(data).hexdigest()
+ self.api['set_hash'](self.xmpp.boundjid, args=new_hash)
+ except XMPPError:
+ log.debug('Could not retrieve vCard for %s', self.xmpp.boundjid.bare)
+
+ def _end(self, event):
+ pass
+
+ def _update_presence(self, stanza):
+ if not isinstance(stanza, Presence):
+ return stanza
+
+ if stanza['type'] not in ('available', 'dnd', 'chat', 'away', 'xa'):
+ return stanza
+
+ current_hash = self.api['get_hash'](stanza['from'])
+ stanza['vcard_temp_update']['photo'] = current_hash
+ return stanza
+
+ def _reset_hash(self, jid, node, ifrom, args):
+ own_jid = (jid.bare == self.xmpp.boundjid.bare)
+ if self.xmpp.is_component:
+ own_jid = (jid.domain == self.xmpp.boundjid.domain)
+
+ self.api['set_hash'](jid, args=None)
+ if own_jid:
+ self.xmpp.roster[jid].send_last_presence()
+
+ def callback(iq):
+ if iq['type'] == 'error':
+ log.debug('Could not retrieve vCard for %s', jid)
+ return
+ data = iq['vcard_temp']['PHOTO']['BINVAL']
+ if not data:
+ new_hash = ''
+ else:
+ new_hash = hashlib.sha1(data).hexdigest()
+
+ self.api['set_hash'](jid, args=new_hash)
+
+ self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom,
+ callback=callback)
+
+ def _recv_presence(self, pres):
+ try:
+ if pres['muc']['affiliation']:
+ # Don't process vCard avatars for MUC occupants
+ # since they all share the same bare JID.
+ return
+ except: pass
+
+ if not pres.match('presence/vcard_temp_update'):
+ self.api['set_hash'](pres['from'], args=None)
+ return
+
+ data = pres['vcard_temp_update']['photo']
+ if data is None:
+ return
+ elif data == '' or data != self.api['get_hash'](pres['from']):
+ ifrom = pres['to'] if self.xmpp.is_component else None
+ self.api['reset_hash'](pres['from'], ifrom=ifrom)
+ self.xmpp.event('vcard_avatar_update', pres)
+
+ # =================================================================
+
+ def _get_hash(self, jid, node, ifrom, args):
+ return self._hashes.get(jid.bare, None)
+
+ def _set_hash(self, jid, node, ifrom, args):
+ self._hashes[jid.bare] = args
diff --git a/slixmpp/plugins/xep_0163.py b/slixmpp/plugins/xep_0163.py
new file mode 100644
index 00000000..b85c662c
--- /dev/null
+++ b/slixmpp/plugins/xep_0163.py
@@ -0,0 +1,120 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import asyncio
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin, register_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0163(BasePlugin):
+
+ """
+ XEP-0163: Personal Eventing Protocol (PEP)
+ """
+
+ name = 'xep_0163'
+ description = 'XEP-0163: Personal Eventing Protocol (PEP)'
+ dependencies = set(['xep_0030', 'xep_0060', 'xep_0115'])
+
+ def register_pep(self, name, stanza):
+ """
+ Setup and configure events and stanza registration for
+ the given PEP stanza:
+
+ - Add disco feature for the PEP content.
+ - Register disco interest in the PEP content.
+ - Map events from the PEP content's namespace to the given name.
+
+ :param str name: The event name prefix to use for PEP events.
+ :param stanza: The stanza class for the PEP content.
+ """
+ pubsub_stanza = self.xmpp['xep_0060'].stanza
+ register_stanza_plugin(pubsub_stanza.EventItem, stanza)
+
+ self.add_interest(stanza.namespace)
+ self.xmpp['xep_0030'].add_feature(stanza.namespace)
+ self.xmpp['xep_0060'].map_node_event(stanza.namespace, name)
+
+ def add_interest(self, namespace, jid=None):
+ """
+ Mark an interest in a PEP subscription by including a disco
+ feature with the '+notify' extension.
+
+ Arguments:
+ namespace -- The base namespace to register as an interest, such
+ as 'http://jabber.org/protocol/tune'. This may also
+ be a list of such namespaces.
+ jid -- Optionally specify the JID.
+ """
+ if not isinstance(namespace, set) and not isinstance(namespace, list):
+ namespace = [namespace]
+
+ for ns in namespace:
+ self.xmpp['xep_0030'].add_feature('%s+notify' % ns,
+ jid=jid)
+ asyncio.async(self.xmpp['xep_0115'].update_caps(jid))
+
+ def remove_interest(self, namespace, jid=None):
+ """
+ Mark an interest in a PEP subscription by including a disco
+ feature with the '+notify' extension.
+
+ Arguments:
+ namespace -- The base namespace to remove as an interest, such
+ as 'http://jabber.org/protocol/tune'. This may also
+ be a list of such namespaces.
+ jid -- Optionally specify the JID.
+ """
+ if not isinstance(namespace, (set, list)):
+ namespace = [namespace]
+
+ for ns in namespace:
+ self.xmpp['xep_0030'].del_feature(jid=jid,
+ feature='%s+notify' % namespace)
+ asyncio.async(self.xmpp['xep_0115'].update_caps(jid))
+
+ def publish(self, stanza, node=None, id=None, options=None, ifrom=None,
+ timeout_callback=None, callback=None, timeout=None):
+ """
+ Publish a PEP update.
+
+ This is just a (very) thin wrapper around the XEP-0060 publish()
+ method to set the defaults expected by PEP.
+
+ Arguments:
+ stanza -- The PEP update stanza to publish.
+ node -- The node to publish the item to. If not specified,
+ the stanza's namespace will be used.
+ id -- Optionally specify the ID of the item.
+ options -- A form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if node is None:
+ node = stanza.namespace
+ if id is None:
+ id = 'current'
+
+ return self.xmpp['xep_0060'].publish(ifrom, node, id=id,
+ payload=stanza.xml,
+ options=options, ifrom=ifrom,
+ callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+
+register_plugin(XEP_0163)
diff --git a/slixmpp/plugins/xep_0172/__init__.py b/slixmpp/plugins/xep_0172/__init__.py
new file mode 100644
index 00000000..6e8d25e0
--- /dev/null
+++ b/slixmpp/plugins/xep_0172/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0172 import stanza
+from slixmpp.plugins.xep_0172.stanza import UserNick
+from slixmpp.plugins.xep_0172.user_nick import XEP_0172
+
+
+register_plugin(XEP_0172)
diff --git a/slixmpp/plugins/xep_0172/stanza.py b/slixmpp/plugins/xep_0172/stanza.py
new file mode 100644
index 00000000..305f3a00
--- /dev/null
+++ b/slixmpp/plugins/xep_0172/stanza.py
@@ -0,0 +1,67 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class UserNick(ElementBase):
+
+ """
+ XEP-0172: User Nickname allows the addition of a <nick> element
+ in several stanza types, including <message> and <presence> stanzas.
+
+ The nickname contained in a <nick> should be the global, friendly or
+ informal name chosen by the owner of a bare JID. The <nick> element
+ may be included when establishing communications with new entities,
+ such as normal XMPP users or MUC services.
+
+ The nickname contained in a <nick> element will not necessarily be
+ the same as the nickname used in a MUC.
+
+ Example stanzas:
+ <message to="user@example.com">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ <body>...</body>
+ </message>
+
+ <presence to="otheruser@example.com" type="subscribe">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ </presence>
+
+ Stanza Interface:
+ nick -- A global, friendly or informal name chosen by a user.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_nick -- Return the nickname in the <nick> element.
+ set_nick -- Add a <nick> element with the given nickname.
+ del_nick -- Remove the <nick> element.
+ """
+
+ namespace = 'http://jabber.org/protocol/nick'
+ name = 'nick'
+ plugin_attrib = name
+ interfaces = set(('nick',))
+
+ def set_nick(self, nick):
+ """
+ Add a <nick> element with the given nickname.
+
+ Arguments:
+ nick -- A human readable, informal name.
+ """
+ self.xml.text = nick
+
+ def get_nick(self):
+ """Return the nickname in the <nick> element."""
+ return self.xml.text
+
+ def del_nick(self):
+ """Remove the <nick> element."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
diff --git a/slixmpp/plugins/xep_0172/user_nick.py b/slixmpp/plugins/xep_0172/user_nick.py
new file mode 100644
index 00000000..b9f20b27
--- /dev/null
+++ b/slixmpp/plugins/xep_0172/user_nick.py
@@ -0,0 +1,83 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza.message import Message
+from slixmpp.stanza.presence import Presence
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import MatchXPath
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0172 import stanza, UserNick
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0172(BasePlugin):
+
+ """
+ XEP-0172: User Nickname
+ """
+
+ name = 'xep_0172'
+ description = 'XEP-0172: User Nickname'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, UserNick)
+ register_stanza_plugin(Presence, UserNick)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=UserNick.namespace)
+ self.xmpp['xep_0163'].remove_interest(UserNick.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_nick', UserNick)
+
+ def publish_nick(self, nick=None, options=None, ifrom=None, timeout_callback=None,
+ callback=None, timeout=None):
+ """
+ Publish the user's current nick.
+
+ Arguments:
+ nick -- The user nickname to publish.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ nickname = UserNick()
+ nickname['nick'] = nick
+ self.xmpp['xep_0163'].publish(nickname, node=UserNick.namespace,
+ options=options, ifrom=ifrom,
+ callback=callback, timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, timeout_callback=None, callback=None, timeout=None):
+ """
+ Clear existing user nick information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ nick = UserNick()
+ return self.xmpp['xep_0163'].publish(nick, node=UserNick.namespace,
+ ifrom=ifrom, callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0184/__init__.py b/slixmpp/plugins/xep_0184/__init__.py
new file mode 100644
index 00000000..83c2d7c2
--- /dev/null
+++ b/slixmpp/plugins/xep_0184/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0184.stanza import Request, Received
+from slixmpp.plugins.xep_0184.receipt import XEP_0184
+
+
+register_plugin(XEP_0184)
+
+
+# Retain some backwards compatibility
+xep_0184 = XEP_0184
diff --git a/slixmpp/plugins/xep_0184/receipt.py b/slixmpp/plugins/xep_0184/receipt.py
new file mode 100644
index 00000000..2c3555dc
--- /dev/null
+++ b/slixmpp/plugins/xep_0184/receipt.py
@@ -0,0 +1,129 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import Message
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0184 import stanza, Request, Received
+
+
+class XEP_0184(BasePlugin):
+
+ """
+ XEP-0184: Message Delivery Receipts
+ """
+
+ name = 'xep_0184'
+ description = 'XEP-0184: Message Delivery Receipts'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ default_config = {
+ 'auto_ack': True,
+ 'auto_request': False
+ }
+
+ ack_types = ('normal', 'chat', 'headline')
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, Request)
+ register_stanza_plugin(Message, Received)
+
+ self.xmpp.add_filter('out', self._filter_add_receipt_request)
+
+ self.xmpp.register_handler(
+ Callback('Message Receipt',
+ StanzaPath('message/receipt'),
+ self._handle_receipt_received))
+
+ self.xmpp.register_handler(
+ Callback('Message Receipt Request',
+ StanzaPath('message/request_receipt'),
+ self._handle_receipt_request))
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature('urn:xmpp:receipts')
+ self.xmpp.del_filter('out', self._filter_add_receipt_request)
+ self.xmpp.remove_handler('Message Receipt')
+ self.xmpp.remove_handler('Message Receipt Request')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts')
+
+ def ack(self, msg):
+ """
+ Acknowledge a message by sending a receipt.
+
+ Arguments:
+ msg -- The message to acknowledge.
+ """
+ ack = self.xmpp.Message()
+ ack['to'] = msg['from']
+ ack['receipt'] = msg['id']
+ ack.send()
+
+ def _handle_receipt_received(self, msg):
+ self.xmpp.event('receipt_received', msg)
+
+ def _handle_receipt_request(self, msg):
+ """
+ Auto-ack message receipt requests if ``self.auto_ack`` is ``True``.
+
+ Arguments:
+ msg -- The incoming message requesting a receipt.
+ """
+ if self.auto_ack:
+ if msg['type'] in self.ack_types:
+ if not msg['receipt']:
+ self.ack(msg)
+
+ def _filter_add_receipt_request(self, stanza):
+ """
+ Auto add receipt requests to outgoing messages, if:
+
+ - ``self.auto_request`` is set to ``True``
+ - The message is not for groupchat
+ - The message does not contain a receipt acknowledgment
+ - The recipient is a bare JID or, if a full JID, one
+ that has the ``urn:xmpp:receipts`` feature enabled
+
+ The disco cache is checked if a full JID is specified in
+ the outgoing message, which may mean a round-trip disco#info
+ delay for the first message sent to the JID if entity caps
+ are not used.
+ """
+
+ if not self.auto_request:
+ return stanza
+
+ if not isinstance(stanza, Message):
+ return stanza
+
+ if stanza['request_receipt']:
+ return stanza
+
+ if not stanza['type'] in self.ack_types:
+ return stanza
+
+ if stanza['receipt']:
+ return stanza
+
+ if not stanza['body']:
+ return stanza
+
+ if stanza['to'].resource:
+ if not self.xmpp['xep_0030'].supports(stanza['to'],
+ feature='urn:xmpp:receipts',
+ cached=True):
+ return stanza
+
+ stanza['request_receipt'] = True
+ return stanza
diff --git a/slixmpp/plugins/xep_0184/stanza.py b/slixmpp/plugins/xep_0184/stanza.py
new file mode 100644
index 00000000..16a640e7
--- /dev/null
+++ b/slixmpp/plugins/xep_0184/stanza.py
@@ -0,0 +1,72 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.stanzabase import ElementBase, ET
+
+
+class Request(ElementBase):
+ namespace = 'urn:xmpp:receipts'
+ name = 'request'
+ plugin_attrib = 'request_receipt'
+ interfaces = set(('request_receipt',))
+ sub_interfaces = interfaces
+ is_extension = True
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def set_request_receipt(self, val):
+ self.del_request_receipt()
+ if val:
+ parent = self.parent()
+ parent._set_sub_text("{%s}request" % self.namespace, keep=True)
+ if not parent['id']:
+ if parent.stream:
+ parent['id'] = parent.stream.new_id()
+
+ def get_request_receipt(self):
+ parent = self.parent()
+ if parent.find("{%s}request" % self.namespace) is not None:
+ return True
+ else:
+ return False
+
+ def del_request_receipt(self):
+ self.parent()._del_sub("{%s}request" % self.namespace)
+
+
+class Received(ElementBase):
+ namespace = 'urn:xmpp:receipts'
+ name = 'received'
+ plugin_attrib = 'receipt'
+ interfaces = set(['receipt'])
+ sub_interfaces = interfaces
+ is_extension = True
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def set_receipt(self, value):
+ self.del_receipt()
+ if value:
+ parent = self.parent()
+ xml = ET.Element("{%s}received" % self.namespace)
+ xml.attrib['id'] = value
+ parent.append(xml)
+
+ def get_receipt(self):
+ parent = self.parent()
+ xml = parent.find("{%s}received" % self.namespace)
+ if xml is not None:
+ return xml.attrib.get('id', '')
+ return ''
+
+ def del_receipt(self):
+ self.parent()._del_sub('{%s}received' % self.namespace)
diff --git a/slixmpp/plugins/xep_0186/__init__.py b/slixmpp/plugins/xep_0186/__init__.py
new file mode 100644
index 00000000..0dc09337
--- /dev/null
+++ b/slixmpp/plugins/xep_0186/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0186 import stanza
+from slixmpp.plugins.xep_0186.stanza import Invisible, Visible
+from slixmpp.plugins.xep_0186.invisible_command import XEP_0186
+
+
+register_plugin(XEP_0186)
diff --git a/slixmpp/plugins/xep_0186/invisible_command.py b/slixmpp/plugins/xep_0186/invisible_command.py
new file mode 100644
index 00000000..c20a3a06
--- /dev/null
+++ b/slixmpp/plugins/xep_0186/invisible_command.py
@@ -0,0 +1,44 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0186 import stanza, Visible, Invisible
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0186(BasePlugin):
+
+ name = 'xep_0186'
+ description = 'XEP-0186: Invisible Command'
+ dependencies = set(['xep_0030'])
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Visible)
+ register_stanza_plugin(Iq, Invisible)
+
+ def set_invisible(self, ifrom=None, callback=None,
+ timeout=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq.enable('invisible')
+ iq.send(callback=callback, timeout=timeout)
+
+ def set_visible(self, ifrom=None, callback=None,
+ timeout=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq.enable('visible')
+ iq.send(callback=callback, timeout=timeout)
diff --git a/slixmpp/plugins/xep_0186/stanza.py b/slixmpp/plugins/xep_0186/stanza.py
new file mode 100644
index 00000000..1ae7e834
--- /dev/null
+++ b/slixmpp/plugins/xep_0186/stanza.py
@@ -0,0 +1,23 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Invisible(ElementBase):
+ name = 'invisible'
+ namespace = 'urn:xmpp:invisible:0'
+ plugin_attrib = 'invisible'
+ interfaces = set()
+
+
+class Visible(ElementBase):
+ name = 'visible'
+ namespace = 'urn:xmpp:visible:0'
+ plugin_attrib = 'visible'
+ interfaces = set()
diff --git a/slixmpp/plugins/xep_0191/__init__.py b/slixmpp/plugins/xep_0191/__init__.py
new file mode 100644
index 00000000..cd75684a
--- /dev/null
+++ b/slixmpp/plugins/xep_0191/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0191.stanza import Block, Unblock, BlockList
+from slixmpp.plugins.xep_0191.blocking import XEP_0191
+
+
+register_plugin(XEP_0191)
diff --git a/slixmpp/plugins/xep_0191/blocking.py b/slixmpp/plugins/xep_0191/blocking.py
new file mode 100644
index 00000000..fa2a013e
--- /dev/null
+++ b/slixmpp/plugins/xep_0191/blocking.py
@@ -0,0 +1,89 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin, JID
+from slixmpp.plugins.xep_0191 import stanza, Block, Unblock, BlockList
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0191(BasePlugin):
+
+ name = 'xep_0191'
+ description = 'XEP-0191: Blocking Command'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, BlockList)
+ register_stanza_plugin(Iq, Block)
+ register_stanza_plugin(Iq, Unblock)
+
+ self.xmpp.register_handler(
+ Callback('Blocked Contact',
+ StanzaPath('iq@type=set/block'),
+ self._handle_blocked))
+
+ self.xmpp.register_handler(
+ Callback('Unblocked Contact',
+ StanzaPath('iq@type=set/unblock'),
+ self._handle_unblocked))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Blocked Contact')
+ self.xmpp.remove_handler('Unblocked Contact')
+
+ def get_blocked(self, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq.enable('blocklist')
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def block(self, jids, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+
+ if not isinstance(jids, (set, list)):
+ jids = [jids]
+
+ iq['block']['items'] = jids
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def unblock(self, jids=None, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+
+ if jids is None:
+ jids = []
+ if not isinstance(jids, (set, list)):
+ jids = [jids]
+
+ iq['unblock']['items'] = jids
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def _handle_blocked(self, iq):
+ self.xmpp.event('blocked', iq)
+
+ def _handle_unblocked(self, iq):
+ self.xmpp.event('unblocked', iq)
diff --git a/slixmpp/plugins/xep_0191/stanza.py b/slixmpp/plugins/xep_0191/stanza.py
new file mode 100644
index 00000000..4dac7bfc
--- /dev/null
+++ b/slixmpp/plugins/xep_0191/stanza.py
@@ -0,0 +1,50 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET, ElementBase, JID
+
+
+class BlockList(ElementBase):
+ name = 'blocklist'
+ namespace = 'urn:xmpp:blocking'
+ plugin_attrib = 'blocklist'
+ interfaces = set(['items'])
+
+ def get_items(self):
+ result = set()
+ items = self.xml.findall('{%s}item' % self.namespace)
+ if items is not None:
+ for item in items:
+ jid = JID(item.attrib.get('jid', ''))
+ if jid:
+ result.add(jid)
+ return result
+
+ def set_items(self, values):
+ self.del_items()
+ for jid in values:
+ if jid:
+ item = ET.Element('{%s}item' % self.namespace)
+ item.attrib['jid'] = JID(jid).full
+ self.xml.append(item)
+
+ def del_items(self):
+ items = self.xml.findall('{%s}item' % self.namespace)
+ if items is not None:
+ for item in items:
+ self.xml.remove(item)
+
+
+class Block(BlockList):
+ name = 'block'
+ plugin_attrib = 'block'
+
+
+class Unblock(BlockList):
+ name = 'unblock'
+ plugin_attrib = 'unblock'
diff --git a/slixmpp/plugins/xep_0196/__init__.py b/slixmpp/plugins/xep_0196/__init__.py
new file mode 100644
index 00000000..89f0f89e
--- /dev/null
+++ b/slixmpp/plugins/xep_0196/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0196 import stanza
+from slixmpp.plugins.xep_0196.stanza import UserGaming
+from slixmpp.plugins.xep_0196.user_gaming import XEP_0196
+
+
+register_plugin(XEP_0196)
diff --git a/slixmpp/plugins/xep_0196/stanza.py b/slixmpp/plugins/xep_0196/stanza.py
new file mode 100644
index 00000000..9c3cd0ed
--- /dev/null
+++ b/slixmpp/plugins/xep_0196/stanza.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class UserGaming(ElementBase):
+
+ name = 'gaming'
+ namespace = 'urn:xmpp:gaming:0'
+ plugin_attrib = 'gaming'
+ interfaces = set(['character_name', 'character_profile', 'name',
+ 'level', 'server_address', 'server_name', 'uri'])
+ sub_interfaces = interfaces
+
diff --git a/slixmpp/plugins/xep_0196/user_gaming.py b/slixmpp/plugins/xep_0196/user_gaming.py
new file mode 100644
index 00000000..f0dee99f
--- /dev/null
+++ b/slixmpp/plugins/xep_0196/user_gaming.py
@@ -0,0 +1,93 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0196 import stanza, UserGaming
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0196(BasePlugin):
+
+ """
+ XEP-0196: User Gaming
+ """
+
+ name = 'xep_0196'
+ description = 'XEP-0196: User Gaming'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=UserGaming.namespace)
+ self.xmpp['xep_0163'].remove_interest(UserGaming.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_gaming', UserGaming)
+
+ def publish_gaming(self, name=None, level=None, server_name=None,
+ uri=None, character_name=None,
+ character_profile=None, server_address=None,
+ options=None, ifrom=None, callback=None,
+ timeout=None, timeout_callback=None):
+ """
+ Publish the user's current gaming status.
+
+ Arguments:
+ name -- The name of the game.
+ level -- The user's level in the game.
+ uri -- A URI for the game or relevant gaming service
+ server_name -- The name of the server where the user is playing.
+ server_address -- The hostname or IP address of the server where the
+ user is playing.
+ character_name -- The name of the user's character in the game.
+ character_profile -- A URI for a profile of the user's character.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ gaming = UserGaming()
+ gaming['name'] = name
+ gaming['level'] = level
+ gaming['uri'] = uri
+ gaming['character_name'] = character_name
+ gaming['character_profile'] = character_profile
+ gaming['server_name'] = server_name
+ gaming['server_address'] = server_address
+ return self.xmpp['xep_0163'].publish(gaming,
+ node=UserGaming.namespace,
+ options=options, ifrom=ifrom,
+ callback=callback, timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def stop(self, ifrom=None, callback=None, timeout=None,
+ timeout_callback=None):
+ """
+ Clear existing user gaming information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ gaming = UserGaming()
+ return self.xmpp['xep_0163'].publish(gaming,
+ node=UserGaming.namespace,
+ ifrom=ifrom, callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0198/__init__.py b/slixmpp/plugins/xep_0198/__init__.py
new file mode 100644
index 00000000..bd709041
--- /dev/null
+++ b/slixmpp/plugins/xep_0198/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0198.stanza import Enable, Enabled
+from slixmpp.plugins.xep_0198.stanza import Resume, Resumed
+from slixmpp.plugins.xep_0198.stanza import Failed
+from slixmpp.plugins.xep_0198.stanza import StreamManagement
+from slixmpp.plugins.xep_0198.stanza import Ack, RequestAck
+
+from slixmpp.plugins.xep_0198.stream_management import XEP_0198
+
+
+register_plugin(XEP_0198)
diff --git a/slixmpp/plugins/xep_0198/stanza.py b/slixmpp/plugins/xep_0198/stanza.py
new file mode 100644
index 00000000..b1c4c010
--- /dev/null
+++ b/slixmpp/plugins/xep_0198/stanza.py
@@ -0,0 +1,150 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Error
+from slixmpp.xmlstream import ElementBase, StanzaBase
+
+
+class Enable(StanzaBase):
+ name = 'enable'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['max', 'resume'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_resume(self):
+ return self._get_attr('resume', 'false').lower() in ('true', '1')
+
+ def set_resume(self, val):
+ self._del_attr('resume')
+ self._set_attr('resume', 'true' if val else 'false')
+
+
+class Enabled(StanzaBase):
+ name = 'enabled'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['id', 'location', 'max', 'resume'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_resume(self):
+ return self._get_attr('resume', 'false').lower() in ('true', '1')
+
+ def set_resume(self, val):
+ self._del_attr('resume')
+ self._set_attr('resume', 'true' if val else 'false')
+
+
+class Resume(StanzaBase):
+ name = 'resume'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['h', 'previd'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_h(self):
+ h = self._get_attr('h', None)
+ if h:
+ return int(h)
+ return None
+
+ def set_h(self, val):
+ self._set_attr('h', str(val))
+
+
+class Resumed(StanzaBase):
+ name = 'resumed'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['h', 'previd'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_h(self):
+ h = self._get_attr('h', None)
+ if h:
+ return int(h)
+ return None
+
+ def set_h(self, val):
+ self._set_attr('h', str(val))
+
+
+class Failed(StanzaBase, Error):
+ name = 'failed'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set()
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+
+class StreamManagement(ElementBase):
+ name = 'sm'
+ namespace = 'urn:xmpp:sm:3'
+ plugin_attrib = name
+ interfaces = set(['required', 'optional'])
+
+ def get_required(self):
+ return self.find('{%s}required' % self.namespace) is not None
+
+ def set_required(self, val):
+ self.del_required()
+ if val:
+ self._set_sub_text('required', '', keep=True)
+
+ def del_required(self):
+ self._del_sub('required')
+
+ def get_optional(self):
+ return self.find('{%s}optional' % self.namespace) is not None
+
+ def set_optional(self, val):
+ self.del_optional()
+ if val:
+ self._set_sub_text('optional', '', keep=True)
+
+ def del_optional(self):
+ self._del_sub('optional')
+
+
+class RequestAck(StanzaBase):
+ name = 'r'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set()
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+
+class Ack(StanzaBase):
+ name = 'a'
+ namespace = 'urn:xmpp:sm:3'
+ interfaces = set(['h'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_h(self):
+ h = self._get_attr('h', None)
+ if h:
+ return int(h)
+ return None
+
+ def set_h(self, val):
+ self._set_attr('h', str(val))
diff --git a/slixmpp/plugins/xep_0198/stream_management.py b/slixmpp/plugins/xep_0198/stream_management.py
new file mode 100644
index 00000000..acf37cd7
--- /dev/null
+++ b/slixmpp/plugins/xep_0198/stream_management.py
@@ -0,0 +1,313 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+import collections
+
+from slixmpp.stanza import Message, Presence, Iq, StreamFeatures
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback, Waiter
+from slixmpp.xmlstream.matcher import MatchXPath, MatchMany
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0198 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+MAX_SEQ = 2 ** 32
+
+
+class XEP_0198(BasePlugin):
+
+ """
+ XEP-0198: Stream Management
+ """
+
+ name = 'xep_0198'
+ description = 'XEP-0198: Stream Management'
+ dependencies = set()
+ stanza = stanza
+ default_config = {
+ #: The last ack number received from the server.
+ 'last_ack': 0,
+
+ #: The number of stanzas to wait between sending ack requests to
+ #: the server. Setting this to ``1`` will send an ack request after
+ #: every sent stanza. Defaults to ``5``.
+ 'window': 5,
+
+ #: The stream management ID for the stream. Knowing this value is
+ #: required in order to do stream resumption.
+ 'sm_id': None,
+
+ #: A counter of handled incoming stanzas, mod 2^32.
+ 'handled': 0,
+
+ #: A counter of unacked outgoing stanzas, mod 2^32.
+ 'seq': 0,
+
+ #: Control whether or not the ability to resume the stream will be
+ #: requested when enabling stream management. Defaults to ``True``.
+ 'allow_resume': True,
+
+ 'order': 10100,
+ 'resume_order': 9000
+ }
+
+ def plugin_init(self):
+ """Start the XEP-0198 plugin."""
+
+ # Only enable stream management for non-components,
+ # since components do not yet perform feature negotiation.
+ if self.xmpp.is_component:
+ return
+
+ self.window_counter = self.window
+ self.window_counter_lock = threading.Lock()
+
+ self.enabled = threading.Event()
+ self.unacked_queue = collections.deque()
+
+ self.seq_lock = threading.Lock()
+ self.handled_lock = threading.Lock()
+ self.ack_lock = threading.Lock()
+
+ register_stanza_plugin(StreamFeatures, stanza.StreamManagement)
+ self.xmpp.register_stanza(stanza.Enable)
+ self.xmpp.register_stanza(stanza.Enabled)
+ self.xmpp.register_stanza(stanza.Resume)
+ self.xmpp.register_stanza(stanza.Resumed)
+ self.xmpp.register_stanza(stanza.Ack)
+ self.xmpp.register_stanza(stanza.RequestAck)
+
+ # Only end the session when a </stream> element is sent,
+ # not just because the connection has died.
+ self.xmpp.end_session_on_disconnect = False
+
+ # Register the feature twice because it may be ordered two
+ # different ways: enabling after binding and resumption
+ # before binding.
+ self.xmpp.register_feature('sm',
+ self._handle_sm_feature,
+ restart=True,
+ order=self.order)
+ self.xmpp.register_feature('sm',
+ self._handle_sm_feature,
+ restart=True,
+ order=self.resume_order)
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Enabled',
+ MatchXPath(stanza.Enabled.tag_name()),
+ self._handle_enabled,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Resumed',
+ MatchXPath(stanza.Resumed.tag_name()),
+ self._handle_resumed,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Failed',
+ MatchXPath(stanza.Failed.tag_name()),
+ self._handle_failed,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Ack',
+ MatchXPath(stanza.Ack.tag_name()),
+ self._handle_ack,
+ instream=True))
+
+ self.xmpp.register_handler(
+ Callback('Stream Management Request Ack',
+ MatchXPath(stanza.RequestAck.tag_name()),
+ self._handle_request_ack,
+ instream=True))
+
+ self.xmpp.add_filter('in', self._handle_incoming)
+ self.xmpp.add_filter('out_sync', self._handle_outgoing)
+
+ self.xmpp.add_event_handler('session_end', self.session_end)
+
+ def plugin_end(self):
+ if self.xmpp.is_component:
+ return
+
+ self.xmpp.unregister_feature('sm', self.order)
+ self.xmpp.unregister_feature('sm', self.resume_order)
+ self.xmpp.del_event_handler('session_end', self.session_end)
+ self.xmpp.del_filter('in', self._handle_incoming)
+ self.xmpp.del_filter('out_sync', self._handle_outgoing)
+ self.xmpp.remove_handler('Stream Management Enabled')
+ self.xmpp.remove_handler('Stream Management Resumed')
+ self.xmpp.remove_handler('Stream Management Failed')
+ self.xmpp.remove_handler('Stream Management Ack')
+ self.xmpp.remove_handler('Stream Management Request Ack')
+ self.xmpp.remove_stanza(stanza.Enable)
+ self.xmpp.remove_stanza(stanza.Enabled)
+ self.xmpp.remove_stanza(stanza.Resume)
+ self.xmpp.remove_stanza(stanza.Resumed)
+ self.xmpp.remove_stanza(stanza.Ack)
+ self.xmpp.remove_stanza(stanza.RequestAck)
+
+ def session_end(self, event):
+ """Reset stream management state."""
+ self.enabled.clear()
+ self.unacked_queue.clear()
+ self.sm_id = None
+ self.handled = 0
+ self.seq = 0
+ self.last_ack = 0
+
+ def send_ack(self):
+ """Send the current ack count to the server."""
+ ack = stanza.Ack(self.xmpp)
+ with self.handled_lock:
+ ack['h'] = self.handled
+ self.xmpp.send_raw(str(ack))
+
+ def request_ack(self, e=None):
+ """Request an ack from the server."""
+ req = stanza.RequestAck(self.xmpp)
+ self.xmpp.send_queue.put(str(req))
+
+ def _handle_sm_feature(self, features):
+ """
+ Enable or resume stream management.
+
+ If no SM-ID is stored, and resource binding has taken place,
+ stream management will be enabled.
+
+ If an SM-ID is known, and the server allows resumption, the
+ previous stream will be resumed.
+ """
+ if 'stream_management' in self.xmpp.features:
+ # We've already negotiated stream management,
+ # so no need to do it again.
+ return False
+ if not self.sm_id:
+ if 'bind' in self.xmpp.features:
+ self.enabled.set()
+ enable = stanza.Enable(self.xmpp)
+ enable['resume'] = self.allow_resume
+ enable.send()
+ self.handled = 0
+ elif self.sm_id and self.allow_resume:
+ self.enabled.set()
+ resume = stanza.Resume(self.xmpp)
+ resume['h'] = self.handled
+ resume['previd'] = self.sm_id
+ resume.send()
+
+ # Wait for a response before allowing stream feature processing
+ # to continue. The actual result processing will be done in the
+ # _handle_resumed() or _handle_failed() methods.
+ waiter = Waiter('resumed_or_failed',
+ MatchMany([
+ MatchXPath(stanza.Resumed.tag_name()),
+ MatchXPath(stanza.Failed.tag_name())]))
+ self.xmpp.register_handler(waiter)
+ result = waiter.wait()
+ if result is not None and result.name == 'resumed':
+ return True
+ return False
+
+ def _handle_enabled(self, stanza):
+ """Save the SM-ID, if provided.
+
+ Raises an :term:`sm_enabled` event.
+ """
+ self.xmpp.features.add('stream_management')
+ if stanza['id']:
+ self.sm_id = stanza['id']
+ self.xmpp.event('sm_enabled', stanza)
+
+ def _handle_resumed(self, stanza):
+ """Finish resuming a stream by resending unacked stanzas.
+
+ Raises a :term:`session_resumed` event.
+ """
+ self.xmpp.features.add('stream_management')
+ self._handle_ack(stanza)
+ for id, stanza in self.unacked_queue:
+ self.xmpp.send(stanza, use_filters=False)
+ self.xmpp.event('session_resumed', stanza)
+
+ def _handle_failed(self, stanza):
+ """
+ Disable and reset any features used since stream management was
+ requested (tracked stanzas may have been sent during the interval
+ between the enable request and the enabled response).
+
+ Raises an :term:`sm_failed` event.
+ """
+ self.enabled.clear()
+ self.unacked_queue.clear()
+ self.xmpp.event('sm_failed', stanza)
+
+ def _handle_ack(self, ack):
+ """Process a server ack by freeing acked stanzas from the queue.
+
+ Raises a :term:`stanza_acked` event for each acked stanza.
+ """
+ if ack['h'] == self.last_ack:
+ return
+
+ with self.ack_lock:
+ num_acked = (ack['h'] - self.last_ack) % MAX_SEQ
+ num_unacked = len(self.unacked_queue)
+ log.debug("Ack: %s, Last Ack: %s, " + \
+ "Unacked: %s, Num Acked: %s, " + \
+ "Remaining: %s",
+ ack['h'],
+ self.last_ack,
+ num_unacked,
+ num_acked,
+ num_unacked - num_acked)
+ for x in range(num_acked):
+ seq, stanza = self.unacked_queue.popleft()
+ self.xmpp.event('stanza_acked', stanza)
+ self.last_ack = ack['h']
+
+ def _handle_request_ack(self, req):
+ """Handle an ack request by sending an ack."""
+ self.send_ack()
+
+ def _handle_incoming(self, stanza):
+ """Increment the handled counter for each inbound stanza."""
+ if not self.enabled.is_set():
+ return stanza
+
+ if isinstance(stanza, (Message, Presence, Iq)):
+ with self.handled_lock:
+ # Sequence numbers are mod 2^32
+ self.handled = (self.handled + 1) % MAX_SEQ
+ return stanza
+
+ def _handle_outgoing(self, stanza):
+ """Store outgoing stanzas in a queue to be acked."""
+ if not self.enabled.is_set():
+ return stanza
+
+ if isinstance(stanza, (Message, Presence, Iq)):
+ seq = None
+ with self.seq_lock:
+ # Sequence numbers are mod 2^32
+ self.seq = (self.seq + 1) % MAX_SEQ
+ seq = self.seq
+ self.unacked_queue.append((seq, stanza))
+ with self.window_counter_lock:
+ self.window_counter -= 1
+ if self.window_counter == 0:
+ self.window_counter = self.window
+ self.request_ack()
+ return stanza
diff --git a/slixmpp/plugins/xep_0199/__init__.py b/slixmpp/plugins/xep_0199/__init__.py
new file mode 100644
index 00000000..7c7bd221
--- /dev/null
+++ b/slixmpp/plugins/xep_0199/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0199.stanza import Ping
+from slixmpp.plugins.xep_0199.ping import XEP_0199
+
+
+register_plugin(XEP_0199)
+
+
+# Backwards compatibility for names
+xep_0199 = XEP_0199
+xep_0199.sendPing = xep_0199.send_ping
diff --git a/slixmpp/plugins/xep_0199/ping.py b/slixmpp/plugins/xep_0199/ping.py
new file mode 100644
index 00000000..bb2ceb38
--- /dev/null
+++ b/slixmpp/plugins/xep_0199/ping.py
@@ -0,0 +1,187 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import time
+import logging
+
+from slixmpp.jid import JID
+from slixmpp.stanza import Iq
+from slixmpp import asyncio
+from slixmpp.exceptions import IqError, IqTimeout
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0199 import stanza, Ping
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0199(BasePlugin):
+
+ """
+ XEP-0199: XMPP Ping
+
+ Given that XMPP is based on TCP connections, it is possible for the
+ underlying connection to be terminated without the application's
+ awareness. Ping stanzas provide an alternative to whitespace based
+ keepalive methods for detecting lost connections.
+
+ Also see <http://www.xmpp.org/extensions/xep-0199.html>.
+
+ Attributes:
+ keepalive -- If True, periodically send ping requests
+ to the server. If a ping is not answered,
+ the connection will be reset.
+ interval -- Time in seconds between keepalive pings.
+ Defaults to 300 seconds.
+ timeout -- Time in seconds to wait for a ping response.
+ Defaults to 30 seconds.
+ Methods:
+ send_ping -- Send a ping to a given JID, returning the
+ round trip time.
+ """
+
+ name = 'xep_0199'
+ description = 'XEP-0199: XMPP Ping'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ default_config = {
+ 'keepalive': False,
+ 'interval': 300,
+ 'timeout': 30
+ }
+
+ def plugin_init(self):
+ """
+ Start the XEP-0199 plugin.
+ """
+
+ register_stanza_plugin(Iq, Ping)
+
+ self.xmpp.register_handler(
+ Callback('Ping',
+ StanzaPath('iq@type=get/ping'),
+ self._handle_ping))
+
+ if self.keepalive:
+ self.xmpp.add_event_handler('session_start',
+ self.enable_keepalive)
+ self.xmpp.add_event_handler('session_end',
+ self.disable_keepalive)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Ping.namespace)
+ self.xmpp.remove_handler('Ping')
+ if self.keepalive:
+ self.xmpp.del_event_handler('session_start',
+ self.enable_keepalive)
+ self.xmpp.del_event_handler('session_end',
+ self.disable_keepalive)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Ping.namespace)
+
+ def enable_keepalive(self, interval=None, timeout=None):
+ if interval:
+ self.interval = interval
+ if timeout:
+ self.timeout = timeout
+
+ self.keepalive = True
+ self.xmpp.schedule('Ping keepalive',
+ self.interval,
+ self._keepalive,
+ repeat=True)
+
+ def disable_keepalive(self, event=None):
+ self.xmpp.cancel_schedule('Ping keepalive')
+
+ def _keepalive(self, event=None):
+ log.debug("Keepalive ping...")
+ try:
+ rtt = self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
+ except IqTimeout:
+ log.debug("Did not recieve ping back in time." + \
+ "Requesting Reconnect.")
+ self.xmpp.reconnect()
+ else:
+ log.debug('Keepalive RTT: %s' % rtt)
+
+ def _handle_ping(self, iq):
+ """Automatically reply to ping requests."""
+ log.debug("Pinged by %s", iq['from'])
+ iq.reply().send()
+
+ def send_ping(self, jid, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ """Send a ping request.
+
+ Arguments:
+ jid -- The JID that will receive the ping.
+ ifrom -- Specifiy the sender JID.
+ timeout -- Time in seconds to wait for a response.
+ Defaults to self.timeout.
+ callback -- Optional handler to execute when a pong
+ is received.
+ """
+ if not timeout:
+ timeout = self.timeout
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq.enable('ping')
+
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ @asyncio.coroutine
+ def ping(self, jid=None, ifrom=None, timeout=None):
+ """Send a ping request and calculate RTT.
+ This is a coroutine.
+
+ Arguments:
+ jid -- The JID that will receive the ping.
+ ifrom -- Specifiy the sender JID.
+ timeout -- Time in seconds to wait for a response.
+ Defaults to self.timeout.
+ """
+ own_host = False
+ if not jid:
+ if self.xmpp.is_component:
+ jid = self.xmpp.server
+ else:
+ jid = self.xmpp.boundjid.host
+ jid = JID(jid)
+ if jid == self.xmpp.boundjid.host or \
+ self.xmpp.is_component and jid == self.xmpp.server:
+ own_host = True
+
+ if not timeout:
+ timeout = self.timeout
+
+ start = time.time()
+
+ log.debug('Pinging %s' % jid)
+ try:
+ yield from self.send_ping(jid, ifrom=ifrom, timeout=timeout,
+ coroutine=True)
+ except IqError as e:
+ if own_host:
+ rtt = time.time() - start
+ log.debug('Pinged %s, RTT: %s', jid, rtt)
+ return rtt
+ else:
+ raise e
+ else:
+ rtt = time.time() - start
+ log.debug('Pinged %s, RTT: %s', jid, rtt)
+ return rtt
diff --git a/slixmpp/plugins/xep_0199/stanza.py b/slixmpp/plugins/xep_0199/stanza.py
new file mode 100644
index 00000000..425e891b
--- /dev/null
+++ b/slixmpp/plugins/xep_0199/stanza.py
@@ -0,0 +1,36 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import slixmpp
+from slixmpp.xmlstream import ElementBase
+
+
+class Ping(ElementBase):
+
+ """
+ Given that XMPP is based on TCP connections, it is possible for the
+ underlying connection to be terminated without the application's
+ awareness. Ping stanzas provide an alternative to whitespace based
+ keepalive methods for detecting lost connections.
+
+ Example ping stanza:
+ <iq type="get">
+ <ping xmlns="urn:xmpp:ping" />
+ </iq>
+
+ Stanza Interface:
+ None
+
+ Methods:
+ None
+ """
+
+ name = 'ping'
+ namespace = 'urn:xmpp:ping'
+ plugin_attrib = 'ping'
+ interfaces = set()
diff --git a/slixmpp/plugins/xep_0202/__init__.py b/slixmpp/plugins/xep_0202/__init__.py
new file mode 100644
index 00000000..1197eabc
--- /dev/null
+++ b/slixmpp/plugins/xep_0202/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0202 import stanza
+from slixmpp.plugins.xep_0202.stanza import EntityTime
+from slixmpp.plugins.xep_0202.time import XEP_0202
+
+
+register_plugin(XEP_0202)
+
+
+# Retain some backwards compatibility
+xep_0202 = XEP_0202
diff --git a/slixmpp/plugins/xep_0202/stanza.py b/slixmpp/plugins/xep_0202/stanza.py
new file mode 100644
index 00000000..c855663b
--- /dev/null
+++ b/slixmpp/plugins/xep_0202/stanza.py
@@ -0,0 +1,127 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import datetime as dt
+
+from slixmpp.xmlstream import ElementBase
+from slixmpp.plugins import xep_0082
+from slixmpp.thirdparty import tzutc, tzoffset
+
+
+class EntityTime(ElementBase):
+
+ """
+ The <time> element represents the local time for an XMPP agent.
+ The time is expressed in UTC to make synchronization easier
+ between entities, but the offset for the local timezone is also
+ included.
+
+ Example <time> stanzas:
+ <iq type="result">
+ <time xmlns="urn:xmpp:time">
+ <utc>2011-07-03T11:37:12.234569</utc>
+ <tzo>-07:00</tzo>
+ </time>
+ </iq>
+
+ Stanza Interface:
+ time -- The local time for the entity (updates utc and tzo).
+ utc -- The UTC equivalent to local time.
+ tzo -- The local timezone offset from UTC.
+
+ Methods:
+ get_time -- Return local time datetime object.
+ set_time -- Set UTC and TZO fields.
+ del_time -- Remove both UTC and TZO fields.
+ get_utc -- Return datetime object of UTC time.
+ set_utc -- Set the UTC time.
+ get_tzo -- Return tzinfo object.
+ set_tzo -- Set the local timezone offset.
+ """
+
+ name = 'time'
+ namespace = 'urn:xmpp:time'
+ plugin_attrib = 'entity_time'
+ interfaces = set(('tzo', 'utc', 'time'))
+ sub_interfaces = interfaces
+
+ def set_time(self, value):
+ """
+ Set both the UTC and TZO fields given a time object.
+
+ Arguments:
+ value -- A datetime object or properly formatted
+ string equivalent.
+ """
+ date = value
+ if not isinstance(value, dt.datetime):
+ date = xep_0082.parse(value)
+ self['utc'] = date
+ self['tzo'] = date.tzinfo
+
+ def get_time(self):
+ """
+ Return the entity's local time based on the UTC and TZO data.
+ """
+ date = self['utc']
+ tz = self['tzo']
+ return date.astimezone(tz)
+
+ def del_time(self):
+ """Remove both the UTC and TZO fields."""
+ del self['utc']
+ del self['tzo']
+
+ def get_tzo(self):
+ """
+ Return the timezone offset from UTC as a tzinfo object.
+ """
+ tzo = self._get_sub_text('tzo')
+ if tzo == '':
+ tzo = 'Z'
+ time = xep_0082.parse('00:00:00%s' % tzo)
+ return time.tzinfo
+
+ def set_tzo(self, value):
+ """
+ Set the timezone offset from UTC.
+
+ Arguments:
+ value -- Either a tzinfo object or the number of
+ seconds (positive or negative) to offset.
+ """
+ time = xep_0082.time(offset=value)
+ if xep_0082.parse(time).tzinfo == tzutc():
+ self._set_sub_text('tzo', 'Z')
+ else:
+ self._set_sub_text('tzo', time[-6:])
+
+ def get_utc(self):
+ """
+ Return the time in UTC as a datetime object.
+ """
+ value = self._get_sub_text('utc')
+ if value == '':
+ return xep_0082.parse(xep_0082.datetime())
+ return xep_0082.parse('%sZ' % value)
+
+ def set_utc(self, value):
+ """
+ Set the time in UTC.
+
+ Arguments:
+ value -- A datetime object or properly formatted
+ string equivalent.
+ """
+ date = value
+ if not isinstance(value, dt.datetime):
+ date = xep_0082.parse(value)
+ date = date.astimezone(tzutc())
+ value = xep_0082.format_datetime(date)[:-1]
+ self._set_sub_text('utc', value)
diff --git a/slixmpp/plugins/xep_0202/time.py b/slixmpp/plugins/xep_0202/time.py
new file mode 100644
index 00000000..185200fc
--- /dev/null
+++ b/slixmpp/plugins/xep_0202/time.py
@@ -0,0 +1,99 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza.iq import Iq
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins import xep_0082
+from slixmpp.plugins.xep_0202 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0202(BasePlugin):
+
+ """
+ XEP-0202: Entity Time
+ """
+
+ name = 'xep_0202'
+ description = 'XEP-0202: Entity Time'
+ dependencies = set(['xep_0030', 'xep_0082'])
+ stanza = stanza
+ default_config = {
+ #: As a default, respond to time requests with the
+ #: local time returned by XEP-0082. However, a
+ #: custom function can be supplied which accepts
+ #: the JID of the entity to query for the time.
+ 'local_time': None,
+ 'tz_offset': 0
+ }
+
+ def plugin_init(self):
+ """Start the XEP-0203 plugin."""
+
+ if not self.local_time:
+ def default_local_time(jid):
+ return xep_0082.datetime(offset=self.tz_offset)
+
+ self.local_time = default_local_time
+
+ self.xmpp.register_handler(
+ Callback('Entity Time',
+ StanzaPath('iq/entity_time'),
+ self._handle_time_request))
+ register_stanza_plugin(Iq, stanza.EntityTime)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:time')
+ self.xmpp.remove_handler('Entity Time')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
+
+ def _handle_time_request(self, iq):
+ """
+ Respond to a request for the local time.
+
+ The time is taken from self.local_time(), which may be replaced
+ during plugin configuration with a function that maps JIDs to
+ times.
+
+ Arguments:
+ iq -- The Iq time request stanza.
+ """
+ iq = iq.reply()
+ iq['entity_time']['time'] = self.local_time(iq['to'])
+ iq.send()
+
+ def get_entity_time(self, to, ifrom=None, **iqargs):
+ """
+ Request the time from another entity.
+
+ Arguments:
+ to -- JID of the entity to query.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = to
+ iq['from'] = ifrom
+ iq.enable('entity_time')
+ return iq.send(**iqargs)
+
diff --git a/slixmpp/plugins/xep_0203/__init__.py b/slixmpp/plugins/xep_0203/__init__.py
new file mode 100644
index 00000000..59cb7af2
--- /dev/null
+++ b/slixmpp/plugins/xep_0203/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0203 import stanza
+from slixmpp.plugins.xep_0203.stanza import Delay
+from slixmpp.plugins.xep_0203.delay import XEP_0203
+
+
+register_plugin(XEP_0203)
+
+# Retain some backwards compatibility
+xep_0203 = XEP_0203
diff --git a/slixmpp/plugins/xep_0203/delay.py b/slixmpp/plugins/xep_0203/delay.py
new file mode 100644
index 00000000..725003e2
--- /dev/null
+++ b/slixmpp/plugins/xep_0203/delay.py
@@ -0,0 +1,37 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.stanza import Message, Presence
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0203 import stanza
+
+
+class XEP_0203(BasePlugin):
+
+ """
+ XEP-0203: Delayed Delivery
+
+ XMPP stanzas are sometimes withheld for delivery due to the recipient
+ being offline, or are resent in order to establish recent history as
+ is the case with MUCS. In any case, it is important to know when the
+ stanza was originally sent, not just when it was last received.
+
+ Also see <http://www.xmpp.org/extensions/xep-0203.html>.
+ """
+
+ name = 'xep_0203'
+ description = 'XEP-0203: Delayed Delivery'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ """Start the XEP-0203 plugin."""
+ register_stanza_plugin(Message, stanza.Delay)
+ register_stanza_plugin(Presence, stanza.Delay)
diff --git a/slixmpp/plugins/xep_0203/stanza.py b/slixmpp/plugins/xep_0203/stanza.py
new file mode 100644
index 00000000..de907c69
--- /dev/null
+++ b/slixmpp/plugins/xep_0203/stanza.py
@@ -0,0 +1,46 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase
+from slixmpp.plugins import xep_0082
+
+
+class Delay(ElementBase):
+
+ name = 'delay'
+ namespace = 'urn:xmpp:delay'
+ plugin_attrib = 'delay'
+ interfaces = set(('from', 'stamp', 'text'))
+
+ def get_from(self):
+ from_ = self._get_attr('from')
+ return JID(from_) if from_ else None
+
+ def set_from(self, value):
+ self._set_attr('from', str(value))
+
+ def get_stamp(self):
+ timestamp = self._get_attr('stamp')
+ return xep_0082.parse(timestamp) if timestamp else None
+
+ def set_stamp(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('stamp', value)
+
+ def get_text(self):
+ return self.xml.text
+
+ def set_text(self, value):
+ self.xml.text = value
+
+ def del_text(self):
+ self.xml.text = ''
diff --git a/slixmpp/plugins/xep_0221/__init__.py b/slixmpp/plugins/xep_0221/__init__.py
new file mode 100644
index 00000000..b977acc7
--- /dev/null
+++ b/slixmpp/plugins/xep_0221/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0221 import stanza
+from slixmpp.plugins.xep_0221.stanza import Media, URI
+from slixmpp.plugins.xep_0221.media import XEP_0221
+
+
+register_plugin(XEP_0221)
diff --git a/slixmpp/plugins/xep_0221/media.py b/slixmpp/plugins/xep_0221/media.py
new file mode 100644
index 00000000..4c34fbd2
--- /dev/null
+++ b/slixmpp/plugins/xep_0221/media.py
@@ -0,0 +1,27 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0221 import stanza, Media, URI
+from slixmpp.plugins.xep_0004 import FormField
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0221(BasePlugin):
+
+ name = 'xep_0221'
+ description = 'XEP-0221: Data Forms Media Element'
+ dependencies = set(['xep_0004'])
+
+ def plugin_init(self):
+ register_stanza_plugin(FormField, Media)
diff --git a/slixmpp/plugins/xep_0221/stanza.py b/slixmpp/plugins/xep_0221/stanza.py
new file mode 100644
index 00000000..2a2bbabd
--- /dev/null
+++ b/slixmpp/plugins/xep_0221/stanza.py
@@ -0,0 +1,42 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class Media(ElementBase):
+ name = 'media'
+ namespace = 'urn:xmpp:media-element'
+ plugin_attrib = 'media'
+ interfaces = set(['height', 'width', 'alt'])
+
+ def add_uri(self, value, itype):
+ uri = URI()
+ uri['value'] = value
+ uri['type'] = itype
+ self.append(uri)
+
+
+class URI(ElementBase):
+ name = 'uri'
+ namespace = 'urn:xmpp:media-element'
+ plugin_attrib = 'uri'
+ plugin_multi_attrib = 'uris'
+ interfaces = set(['type', 'value'])
+
+ def get_value(self):
+ return self.xml.text
+
+ def set_value(self, value):
+ self.xml.text = value
+
+ def del_value(self):
+ sel.xml.text = ''
+
+
+register_stanza_plugin(Media, URI, iterable=True)
diff --git a/slixmpp/plugins/xep_0222.py b/slixmpp/plugins/xep_0222.py
new file mode 100644
index 00000000..059f4c85
--- /dev/null
+++ b/slixmpp/plugins/xep_0222.py
@@ -0,0 +1,120 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin, register_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0222(BasePlugin):
+
+ """
+ XEP-0222: Persistent Storage of Public Data via PubSub
+ """
+
+ name = 'xep_0222'
+ description = 'XEP-0222: Persistent Storage of Public Data via PubSub'
+ dependencies = set(['xep_0163', 'xep_0060', 'xep_0004'])
+
+ profile = {'pubsub#persist_items': True,
+ 'pubsub#send_last_published_item': 'never'}
+
+ def configure(self, node):
+ """
+ Update a node's configuration to match the public storage profile.
+ """
+ config = self.xmpp['xep_0004'].Form()
+ config['type'] = 'submit'
+
+ for field, value in self.profile.items():
+ config.add_field(var=field, value=value)
+
+ return self.xmpp['xep_0060'].set_node_config(None, node, config,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout)
+
+ def store(self, stanza, node=None, id=None, ifrom=None, options=None,
+ callback=None, timeout=None):
+ """
+ Store public data via PEP.
+
+ This is just a (very) thin wrapper around the XEP-0060 publish()
+ method to set the defaults expected by PEP.
+
+ Arguments:
+ stanza -- The private content to store.
+ node -- The node to publish the content to. If not specified,
+ the stanza's namespace will be used.
+ id -- Optionally specify the ID of the item.
+ options -- Publish options to use, which will be modified to
+ fit the persistent storage option profile.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if not options:
+ options = self.xmpp['xep_0004'].stanza.Form()
+ options['type'] = 'submit'
+ options.add_field(
+ var='FORM_TYPE',
+ ftype='hidden',
+ value='http://jabber.org/protocol/pubsub#publish-options')
+
+ fields = options['fields']
+ for field, value in self.profile.items():
+ if field not in fields:
+ options.add_field(var=field)
+ options['fields'][field]['value'] = value
+
+ return self.xmpp['xep_0163'].publish(stanza, node,
+ options=options,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout)
+
+ def retrieve(self, node, id=None, item_ids=None, ifrom=None,
+ callback=None, timeout=None):
+ """
+ Retrieve public data via PEP.
+
+ This is just a (very) thin wrapper around the XEP-0060 publish()
+ method to set the defaults expected by PEP.
+
+ Arguments:
+ node -- The node to retrieve content from.
+ id -- Optionally specify the ID of the item.
+ item_ids -- Specify a group of IDs. If id is also specified, it
+ will be included in item_ids.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if item_ids is None:
+ item_ids = []
+ if id is not None:
+ item_ids.append(id)
+
+ return self.xmpp['xep_0060'].get_items(None, node,
+ item_ids=item_ids,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout)
+
+
+register_plugin(XEP_0222)
diff --git a/slixmpp/plugins/xep_0223.py b/slixmpp/plugins/xep_0223.py
new file mode 100644
index 00000000..2461bb20
--- /dev/null
+++ b/slixmpp/plugins/xep_0223.py
@@ -0,0 +1,119 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin, register_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0223(BasePlugin):
+
+ """
+ XEP-0223: Persistent Storage of Private Data via PubSub
+ """
+
+ name = 'xep_0223'
+ description = 'XEP-0223: Persistent Storage of Private Data via PubSub'
+ dependencies = set(['xep_0163', 'xep_0060', 'xep_0004'])
+
+ profile = {'pubsub#persist_items': True,
+ 'pubsub#send_last_published_item': 'never'}
+
+ def configure(self, node):
+ """
+ Update a node's configuration to match the public storage profile.
+ """
+ # TODO: that cannot possibly work, why is this here?
+ config = self.xmpp['xep_0004'].Form()
+ config['type'] = 'submit'
+
+ for field, value in self.profile.items():
+ config.add_field(var=field, value=value)
+
+ return self.xmpp['xep_0060'].set_node_config(None, node, config,
+ ifrom=ifrom,
+ callback=callback,
+ timeout=timeout)
+
+ def store(self, stanza, node=None, id=None, ifrom=None, options=None,
+ callback=None, timeout=None, timeout_callback=None):
+ """
+ Store private data via PEP.
+
+ This is just a (very) thin wrapper around the XEP-0060 publish()
+ method to set the defaults expected by PEP.
+
+ Arguments:
+ stanza -- The private content to store.
+ node -- The node to publish the content to. If not specified,
+ the stanza's namespace will be used.
+ id -- Optionally specify the ID of the item.
+ options -- Publish options to use, which will be modified to
+ fit the persistent storage option profile.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if not options:
+ options = self.xmpp['xep_0004'].stanza.Form()
+ options['type'] = 'submit'
+ options.add_field(
+ var='FORM_TYPE',
+ ftype='hidden',
+ value='http://jabber.org/protocol/pubsub#publish-options')
+
+ fields = options['fields']
+ for field, value in self.profile.items():
+ if field not in fields:
+ options.add_field(var=field)
+ options['fields'][field]['value'] = value
+
+ return self.xmpp['xep_0163'].publish(stanza, node, options=options,
+ ifrom=ifrom, callback=callback,
+ timeout=timeout,
+ timeout_callback=timeout_callback)
+
+ def retrieve(self, node, id=None, item_ids=None, ifrom=None,
+ callback=None, timeout=None, timeout_callback=None):
+ """
+ Retrieve private data via PEP.
+
+ This is just a (very) thin wrapper around the XEP-0060 publish()
+ method to set the defaults expected by PEP.
+
+ Arguments:
+ node -- The node to retrieve content from.
+ id -- Optionally specify the ID of the item.
+ item_ids -- Specify a group of IDs. If id is also specified, it
+ will be included in item_ids.
+ ifrom -- Specify the sender's JID.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if item_ids is None:
+ item_ids = []
+ if id is not None:
+ item_ids.append(id)
+
+ return self.xmpp['xep_0060'].get_items(None, node,
+ item_ids=item_ids, ifrom=ifrom,
+ callback=callback, timeout=timeout,
+ timeout_callback=timeout_callback)
+
+
+register_plugin(XEP_0223)
diff --git a/slixmpp/plugins/xep_0224/__init__.py b/slixmpp/plugins/xep_0224/__init__.py
new file mode 100644
index 00000000..4fadfd70
--- /dev/null
+++ b/slixmpp/plugins/xep_0224/__init__.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0224 import stanza
+from slixmpp.plugins.xep_0224.stanza import Attention
+from slixmpp.plugins.xep_0224.attention import XEP_0224
+
+
+register_plugin(XEP_0224)
+
+
+# Retain some backwards compatibility
+xep_0224 = XEP_0224
diff --git a/slixmpp/plugins/xep_0224/attention.py b/slixmpp/plugins/xep_0224/attention.py
new file mode 100644
index 00000000..2777e1b0
--- /dev/null
+++ b/slixmpp/plugins/xep_0224/attention.py
@@ -0,0 +1,75 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.stanza import Message
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0224 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0224(BasePlugin):
+
+ """
+ XEP-0224: Attention
+ """
+
+ name = 'xep_0224'
+ description = 'XEP-0224: Attention'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ """Start the XEP-0224 plugin."""
+ register_stanza_plugin(Message, stanza.Attention)
+
+ self.xmpp.register_handler(
+ Callback('Attention',
+ StanzaPath('message/attention'),
+ self._handle_attention))
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=stanza.Attention.namespace)
+ self.xmpp.remove_handler('Attention')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace)
+
+ def request_attention(self, to, mfrom=None, mbody=''):
+ """
+ Send an attention message with an optional body.
+
+ Arguments:
+ to -- The attention request recipient's JID.
+ mfrom -- Optionally specify the sender of the attention request.
+ mbody -- An optional message body to include in the request.
+ """
+ m = self.xmpp.Message()
+ m['to'] = to
+ m['type'] = 'headline'
+ m['attention'] = True
+ if mfrom:
+ m['from'] = mfrom
+ m['body'] = mbody
+ m.send()
+
+ def _handle_attention(self, msg):
+ """
+ Raise an event after receiving a message with an attention request.
+
+ Arguments:
+ msg -- A message stanza with an attention element.
+ """
+ log.debug("Received attention request from: %s", msg['from'])
+ self.xmpp.event('attention', msg)
diff --git a/slixmpp/plugins/xep_0224/stanza.py b/slixmpp/plugins/xep_0224/stanza.py
new file mode 100644
index 00000000..2b77dadd
--- /dev/null
+++ b/slixmpp/plugins/xep_0224/stanza.py
@@ -0,0 +1,40 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Attention(ElementBase):
+
+ """
+ """
+
+ name = 'attention'
+ namespace = 'urn:xmpp:attention:0'
+ plugin_attrib = 'attention'
+ interfaces = set(('attention',))
+ is_extension = True
+
+ def setup(self, xml):
+ return True
+
+ def set_attention(self, value):
+ if value:
+ xml = ET.Element(self.tag_name())
+ self.parent().xml.append(xml)
+ else:
+ self.del_attention()
+
+ def get_attention(self):
+ xml = self.parent().xml.find(self.tag_name())
+ return xml is not None
+
+ def del_attention(self):
+ xml = self.parent().xml.find(self.tag_name())
+ if xml is not None:
+ self.parent().xml.remove(xml)
diff --git a/slixmpp/plugins/xep_0231/__init__.py b/slixmpp/plugins/xep_0231/__init__.py
new file mode 100644
index 00000000..57d57846
--- /dev/null
+++ b/slixmpp/plugins/xep_0231/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz,
+ Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0231.stanza import BitsOfBinary
+from slixmpp.plugins.xep_0231.bob import XEP_0231
+
+
+register_plugin(XEP_0231)
diff --git a/slixmpp/plugins/xep_0231/bob.py b/slixmpp/plugins/xep_0231/bob.py
new file mode 100644
index 00000000..a3b5edd4
--- /dev/null
+++ b/slixmpp/plugins/xep_0231/bob.py
@@ -0,0 +1,141 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz,
+ Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import hashlib
+
+from slixmpp import future_wrapper
+from slixmpp.stanza import Iq, Message, Presence
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0231 import stanza, BitsOfBinary
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0231(BasePlugin):
+
+ """
+ XEP-0231 Bits of Binary
+ """
+
+ name = 'xep_0231'
+ description = 'XEP-0231: Bits of Binary'
+ dependencies = set(['xep_0030'])
+
+ def plugin_init(self):
+ self._cids = {}
+
+ register_stanza_plugin(Iq, BitsOfBinary)
+ register_stanza_plugin(Message, BitsOfBinary)
+ register_stanza_plugin(Presence, BitsOfBinary)
+
+ self.xmpp.register_handler(
+ Callback('Bits of Binary - Iq',
+ StanzaPath('iq/bob'),
+ self._handle_bob_iq))
+
+ self.xmpp.register_handler(
+ Callback('Bits of Binary - Message',
+ StanzaPath('message/bob'),
+ self._handle_bob))
+
+ self.xmpp.register_handler(
+ Callback('Bits of Binary - Presence',
+ StanzaPath('presence/bob'),
+ self._handle_bob))
+
+ self.api.register(self._get_bob, 'get_bob', default=True)
+ self.api.register(self._set_bob, 'set_bob', default=True)
+ self.api.register(self._del_bob, 'del_bob', default=True)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:bob')
+ self.xmpp.remove_handler('Bits of Binary - Iq')
+ self.xmpp.remove_handler('Bits of Binary - Message')
+ self.xmpp.remove_handler('Bits of Binary - Presence')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:bob')
+
+ def set_bob(self, data, mtype, cid=None, max_age=None):
+ if cid is None:
+ cid = 'sha1+%s@bob.xmpp.org' % hashlib.sha1(data).hexdigest()
+
+ bob = BitsOfBinary()
+ bob['data'] = data
+ bob['type'] = mtype
+ bob['cid'] = cid
+ bob['max_age'] = max_age
+
+ self.api['set_bob'](args=bob)
+
+ return cid
+
+ @future_wrapper
+ def get_bob(self, jid=None, cid=None, cached=True, ifrom=None,
+ timeout=None, callback=None):
+ if cached:
+ data = self.api['get_bob'](None, None, ifrom, args=cid)
+ if data is not None:
+ if not isinstance(data, Iq):
+ iq = self.xmpp.Iq()
+ iq.append(data)
+ return iq
+ return data
+
+ iq = self.xmpp.Iq()
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq['bob']['cid'] = cid
+ return iq.send(timeout=timeout, callback=callback)
+
+ def del_bob(self, cid):
+ self.api['del_bob'](args=cid)
+
+ def _handle_bob_iq(self, iq):
+ cid = iq['bob']['cid']
+
+ if iq['type'] == 'result':
+ self.api['set_bob'](iq['from'], None, iq['to'], args=iq['bob'])
+ self.xmpp.event('bob', iq)
+ elif iq['type'] == 'get':
+ data = self.api['get_bob'](iq['to'], None, iq['from'], args=cid)
+ if isinstance(data, Iq):
+ data['id'] = iq['id']
+ data.send()
+ return
+
+ iq = iq.reply()
+ iq.append(data)
+ iq.send()
+
+ def _handle_bob(self, stanza):
+ self.api['set_bob'](stanza['from'], None,
+ stanza['to'], args=stanza['bob'])
+ self.xmpp.event('bob', stanza)
+
+ # =================================================================
+
+ def _set_bob(self, jid, node, ifrom, bob):
+ self._cids[bob['cid']] = bob
+
+ def _get_bob(self, jid, node, ifrom, cid):
+ if cid in self._cids:
+ return self._cids[cid]
+ raise XMPPError('item-not-found')
+
+ def _del_bob(self, jid, node, ifrom, cid):
+ if cid in self._cids:
+ del self._cids[cid]
diff --git a/slixmpp/plugins/xep_0231/stanza.py b/slixmpp/plugins/xep_0231/stanza.py
new file mode 100644
index 00000000..b3b96eff
--- /dev/null
+++ b/slixmpp/plugins/xep_0231/stanza.py
@@ -0,0 +1,36 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz,
+ Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import ElementBase
+
+
+class BitsOfBinary(ElementBase):
+ name = 'data'
+ namespace = 'urn:xmpp:bob'
+ plugin_attrib = 'bob'
+ interfaces = set(('cid', 'max_age', 'type', 'data'))
+
+ def get_max_age(self):
+ return int(self._get_attr('max-age'))
+
+ def set_max_age(self, value):
+ self._set_attr('max-age', str(value))
+
+ def get_data(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_data(self, value):
+ self.xml.text = bytes(base64.b64encode(value)).decode('utf-8')
+
+ def del_data(self):
+ self.xml.text = ''
diff --git a/slixmpp/plugins/xep_0235/__init__.py b/slixmpp/plugins/xep_0235/__init__.py
new file mode 100644
index 00000000..8810a84d
--- /dev/null
+++ b/slixmpp/plugins/xep_0235/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0235 import stanza
+from slixmpp.plugins.xep_0235.stanza import OAuth
+from slixmpp.plugins.xep_0235.oauth import XEP_0235
+
+
+register_plugin(XEP_0235)
diff --git a/slixmpp/plugins/xep_0235/oauth.py b/slixmpp/plugins/xep_0235/oauth.py
new file mode 100644
index 00000000..bcd220b0
--- /dev/null
+++ b/slixmpp/plugins/xep_0235/oauth.py
@@ -0,0 +1,32 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+import logging
+
+from slixmpp import Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0235 import stanza, OAuth
+
+
+class XEP_0235(BasePlugin):
+
+ name = 'xep_0235'
+ description = 'XEP-0235: OAuth Over XMPP'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, OAuth)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:oauth:0')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:oauth:0')
diff --git a/slixmpp/plugins/xep_0235/stanza.py b/slixmpp/plugins/xep_0235/stanza.py
new file mode 100644
index 00000000..abb4a38d
--- /dev/null
+++ b/slixmpp/plugins/xep_0235/stanza.py
@@ -0,0 +1,80 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import hmac
+import hashlib
+import urllib
+import base64
+
+from slixmpp.xmlstream import ET, ElementBase, JID
+
+
+class OAuth(ElementBase):
+
+ name = 'oauth'
+ namespace = 'urn:xmpp:oauth:0'
+ plugin_attrib = 'oauth'
+ interfaces = set(['oauth_consumer_key', 'oauth_nonce', 'oauth_signature',
+ 'oauth_signature_method', 'oauth_timestamp',
+ 'oauth_token', 'oauth_version'])
+ sub_interfaces = interfaces
+
+ def generate_signature(self, stanza, sfrom, sto, consumer_secret,
+ token_secret, method='HMAC-SHA1'):
+ self['oauth_signature_method'] = method
+
+ request = urllib.quote('%s&%s' % (sfrom, sto), '')
+ parameters = urllib.quote('&'.join([
+ 'oauth_consumer_key=%s' % self['oauth_consumer_key'],
+ 'oauth_nonce=%s' % self['oauth_nonce'],
+ 'oauth_signature_method=%s' % self['oauth_signature_method'],
+ 'oauth_timestamp=%s' % self['oauth_timestamp'],
+ 'oauth_token=%s' % self['oauth_token'],
+ 'oauth_version=%s' % self['oauth_version']
+ ]), '')
+
+ sigbase = '%s&%s&%s' % (stanza, request, parameters)
+
+ consumer_secret = urllib.quote(consumer_secret, '')
+ token_secret = urllib.quote(token_secret, '')
+ key = '%s&%s' % (consumer_secret, token_secret)
+
+ if method == 'HMAC-SHA1':
+ sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest())
+ elif method == 'PLAINTEXT':
+ sig = key
+
+ self['oauth_signature'] = sig
+ return sig
+
+ def verify_signature(self, stanza, sfrom, sto, consumer_secret,
+ token_secret):
+ method = self['oauth_signature_method']
+
+ request = urllib.quote('%s&%s' % (sfrom, sto), '')
+ parameters = urllib.quote('&'.join([
+ 'oauth_consumer_key=%s' % self['oauth_consumer_key'],
+ 'oauth_nonce=%s' % self['oauth_nonce'],
+ 'oauth_signature_method=%s' % self['oauth_signature_method'],
+ 'oauth_timestamp=%s' % self['oauth_timestamp'],
+ 'oauth_token=%s' % self['oauth_token'],
+ 'oauth_version=%s' % self['oauth_version']
+ ]), '')
+
+ sigbase = '%s&%s&%s' % (stanza, request, parameters)
+
+ consumer_secret = urllib.quote(consumer_secret, '')
+ token_secret = urllib.quote(token_secret, '')
+ key = '%s&%s' % (consumer_secret, token_secret)
+
+ if method == 'HMAC-SHA1':
+ sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest())
+ elif method == 'PLAINTEXT':
+ sig = key
+
+ return self['oauth_signature'] == sig
diff --git a/slixmpp/plugins/xep_0242.py b/slixmpp/plugins/xep_0242.py
new file mode 100644
index 00000000..ea077a70
--- /dev/null
+++ b/slixmpp/plugins/xep_0242.py
@@ -0,0 +1,21 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins import BasePlugin, register_plugin
+
+
+class XEP_0242(BasePlugin):
+
+ name = 'xep_0242'
+ description = 'XEP-0242: XMPP Client Compliance 2009'
+ dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
+ 'xep_0045', 'xep_0085', 'xep_0016',
+ 'xep_0191'])
+
+
+register_plugin(XEP_0242)
diff --git a/slixmpp/plugins/xep_0249/__init__.py b/slixmpp/plugins/xep_0249/__init__.py
new file mode 100644
index 00000000..5d120ec6
--- /dev/null
+++ b/slixmpp/plugins/xep_0249/__init__.py
@@ -0,0 +1,19 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dalek
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0249.stanza import Invite
+from slixmpp.plugins.xep_0249.invite import XEP_0249
+
+
+register_plugin(XEP_0249)
+
+
+# Retain some backwards compatibility
+xep_0249 = XEP_0249
diff --git a/slixmpp/plugins/xep_0249/invite.py b/slixmpp/plugins/xep_0249/invite.py
new file mode 100644
index 00000000..fe5f5884
--- /dev/null
+++ b/slixmpp/plugins/xep_0249/invite.py
@@ -0,0 +1,83 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dalek
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp import Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.xep_0249 import Invite, stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0249(BasePlugin):
+
+ """
+ XEP-0249: Direct MUC Invitations
+ """
+
+ name = 'xep_0249'
+ description = 'XEP-0249: Direct MUC Invitations'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Direct MUC Invitations',
+ StanzaPath('message/groupchat_invite'),
+ self._handle_invite))
+
+ register_stanza_plugin(Message, Invite)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Invite.namespace)
+ self.xmpp.remove_handler('Direct MUC Invitations')
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Invite.namespace)
+
+ def _handle_invite(self, msg):
+ """
+ Raise an event for all invitations received.
+ """
+ log.debug("Received direct muc invitation from %s to room %s",
+ msg['from'], msg['groupchat_invite']['jid'])
+
+ self.xmpp.event('groupchat_direct_invite', msg)
+
+ def send_invitation(self, jid, roomjid, password=None,
+ reason=None, ifrom=None):
+ """
+ Send a direct MUC invitation to an XMPP entity.
+
+ Arguments:
+ jid -- The JID of the entity that will receive
+ the invitation
+ roomjid -- the address of the groupchat room to be joined
+ password -- a password needed for entry into a
+ password-protected room (OPTIONAL).
+ reason -- a human-readable purpose for the invitation
+ (OPTIONAL).
+ """
+
+ msg = self.xmpp.Message()
+ msg['to'] = jid
+ if ifrom is not None:
+ msg['from'] = ifrom
+ msg['groupchat_invite']['jid'] = roomjid
+ if password is not None:
+ msg['groupchat_invite']['password'] = password
+ if reason is not None:
+ msg['groupchat_invite']['reason'] = reason
+
+ return msg.send()
diff --git a/slixmpp/plugins/xep_0249/stanza.py b/slixmpp/plugins/xep_0249/stanza.py
new file mode 100644
index 00000000..5979f091
--- /dev/null
+++ b/slixmpp/plugins/xep_0249/stanza.py
@@ -0,0 +1,39 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dalek
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Invite(ElementBase):
+
+ """
+ XMPP allows for an agent in an MUC room to directly invite another
+ user to join the chat room (as opposed to a mediated invitation
+ done through the server).
+
+ Example invite stanza:
+ <message from='crone1@shakespeare.lit/desktop'
+ to='hecate@shakespeare.lit'>
+ <x xmlns='jabber:x:conference'
+ jid='darkcave@macbeth.shakespeare.lit'
+ password='cauldronburn'
+ reason='Hey Hecate, this is the place for all good witches!'/>
+ </message>
+
+ Stanza Interface:
+ jid -- The JID of the groupchat room
+ password -- The password used to gain entry in the room
+ (optional)
+ reason -- The reason for the invitation (optional)
+
+ """
+
+ name = "x"
+ namespace = "jabber:x:conference"
+ plugin_attrib = "groupchat_invite"
+ interfaces = ("jid", "password", "reason")
diff --git a/slixmpp/plugins/xep_0256.py b/slixmpp/plugins/xep_0256.py
new file mode 100644
index 00000000..4ad4f0ea
--- /dev/null
+++ b/slixmpp/plugins/xep_0256.py
@@ -0,0 +1,73 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Presence
+from slixmpp.exceptions import XMPPError
+from slixmpp.plugins import BasePlugin, register_plugin
+from slixmpp.xmlstream import register_stanza_plugin
+
+from slixmpp.plugins.xep_0012 import stanza, LastActivity
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0256(BasePlugin):
+
+ name = 'xep_0256'
+ description = 'XEP-0256: Last Activity in Presence'
+ dependencies = set(['xep_0012'])
+ stanza = stanza
+ default_config = {
+ 'auto_last_activity': False
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Presence, LastActivity)
+
+ self.xmpp.add_filter('out', self._initial_presence_activity)
+ self.xmpp.add_event_handler('connected', self._reset_presence_activity)
+
+ self._initial_presence = set()
+
+ def plugin_end(self):
+ self.xmpp.del_filter('out', self._initial_presence_activity)
+ self.xmpp.del_event_handler('connected', self._reset_presence_activity)
+
+ def _reset_presence_activity(self, e):
+ self._initial_presence = set()
+
+ def _initial_presence_activity(self, stanza):
+ if isinstance(stanza, Presence):
+ use_last_activity = False
+
+ if self.auto_last_activity and stanza['show'] in ('xa', 'away'):
+ use_last_activity = True
+
+ if stanza['from'] not in self._initial_presence:
+ self._initial_presence.add(stanza['from'])
+ use_last_activity = True
+
+ if use_last_activity:
+ plugin = self.xmpp['xep_0012']
+ try:
+ result = plugin.api['get_last_activity'](stanza['from'],
+ None,
+ stanza['to'])
+ seconds = result['last_activity']['seconds']
+ except XMPPError:
+ seconds = None
+
+ if seconds is not None:
+ stanza['last_activity']['seconds'] = seconds
+ return stanza
+
+
+register_plugin(XEP_0256)
diff --git a/slixmpp/plugins/xep_0257/__init__.py b/slixmpp/plugins/xep_0257/__init__.py
new file mode 100644
index 00000000..2621ad8f
--- /dev/null
+++ b/slixmpp/plugins/xep_0257/__init__.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0257 import stanza
+from slixmpp.plugins.xep_0257.stanza import Certs, AppendCert
+from slixmpp.plugins.xep_0257.stanza import DisableCert, RevokeCert
+from slixmpp.plugins.xep_0257.client_cert_management import XEP_0257
+
+
+register_plugin(XEP_0257)
diff --git a/slixmpp/plugins/xep_0257/client_cert_management.py b/slixmpp/plugins/xep_0257/client_cert_management.py
new file mode 100644
index 00000000..a6d07506
--- /dev/null
+++ b/slixmpp/plugins/xep_0257/client_cert_management.py
@@ -0,0 +1,70 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0257 import stanza, Certs
+from slixmpp.plugins.xep_0257 import AppendCert, DisableCert, RevokeCert
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0257(BasePlugin):
+
+ name = 'xep_0257'
+ description = 'XEP-0257: Client Certificate Management for SASL EXTERNAL'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Certs)
+ register_stanza_plugin(Iq, AppendCert)
+ register_stanza_plugin(Iq, DisableCert)
+ register_stanza_plugin(Iq, RevokeCert)
+
+ def get_certs(self, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq.enable('sasl_certs')
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def add_cert(self, name, cert, allow_management=True, ifrom=None,
+ timeout=None, callback=None, timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['sasl_cert_append']['name'] = name
+ iq['sasl_cert_append']['x509cert'] = cert
+ iq['sasl_cert_append']['cert_management'] = allow_management
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def disable_cert(self, name, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['sasl_cert_disable']['name'] = name
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def revoke_cert(self, name, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['sasl_cert_revoke']['name'] = name
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0257/stanza.py b/slixmpp/plugins/xep_0257/stanza.py
new file mode 100644
index 00000000..86b63451
--- /dev/null
+++ b/slixmpp/plugins/xep_0257/stanza.py
@@ -0,0 +1,87 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Certs(ElementBase):
+ name = 'items'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_certs'
+ interfaces = set()
+
+
+class CertItem(ElementBase):
+ name = 'item'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'item'
+ plugin_multi_attrib = 'items'
+ interfaces = set(['name', 'x509cert', 'users'])
+ sub_interfaces = set(['name', 'x509cert'])
+
+ def get_users(self):
+ resources = self.xml.findall('{%s}users/{%s}resource' % (
+ self.namespace, self.namespace))
+ return set([res.text for res in resources])
+
+ def set_users(self, values):
+ users = self.xml.find('{%s}users' % self.namespace)
+ if users is None:
+ users = ET.Element('{%s}users' % self.namespace)
+ self.xml.append(users)
+ for resource in values:
+ res = ET.Element('{%s}resource' % self.namespace)
+ res.text = resource
+ users.append(res)
+
+ def del_users(self):
+ users = self.xml.find('{%s}users' % self.namespace)
+ if users is not None:
+ self.xml.remove(users)
+
+
+class AppendCert(ElementBase):
+ name = 'append'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_cert_append'
+ interfaces = set(['name', 'x509cert', 'cert_management'])
+ sub_interfaces = set(['name', 'x509cert'])
+
+ def get_cert_management(self):
+ manage = self.xml.find('{%s}no-cert-management' % self.namespace)
+ return manage is None
+
+ def set_cert_management(self, value):
+ self.del_cert_management()
+ if not value:
+ manage = ET.Element('{%s}no-cert-management' % self.namespace)
+ self.xml.append(manage)
+
+ def del_cert_management(self):
+ manage = self.xml.find('{%s}no-cert-management' % self.namespace)
+ if manage is not None:
+ self.xml.remove(manage)
+
+
+class DisableCert(ElementBase):
+ name = 'disable'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_cert_disable'
+ interfaces = set(['name'])
+ sub_interfaces = interfaces
+
+
+class RevokeCert(ElementBase):
+ name = 'revoke'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_cert_revoke'
+ interfaces = set(['name'])
+ sub_interfaces = interfaces
+
+
+register_stanza_plugin(Certs, CertItem, iterable=True)
diff --git a/slixmpp/plugins/xep_0258/__init__.py b/slixmpp/plugins/xep_0258/__init__.py
new file mode 100644
index 00000000..7210072d
--- /dev/null
+++ b/slixmpp/plugins/xep_0258/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0258 import stanza
+from slixmpp.plugins.xep_0258.stanza import SecurityLabel, Label
+from slixmpp.plugins.xep_0258.stanza import DisplayMarking, EquivalentLabel
+from slixmpp.plugins.xep_0258.stanza import ESSLabel, Catalog, CatalogItem
+from slixmpp.plugins.xep_0258.security_labels import XEP_0258
+
+
+register_plugin(XEP_0258)
diff --git a/slixmpp/plugins/xep_0258/security_labels.py b/slixmpp/plugins/xep_0258/security_labels.py
new file mode 100644
index 00000000..2fb048c7
--- /dev/null
+++ b/slixmpp/plugins/xep_0258/security_labels.py
@@ -0,0 +1,44 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq, Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0258 import stanza, SecurityLabel, Catalog
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0258(BasePlugin):
+
+ name = 'xep_0258'
+ description = 'XEP-0258: Security Labels in XMPP'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, SecurityLabel)
+ register_stanza_plugin(Iq, Catalog)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=SecurityLabel.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(SecurityLabel.namespace)
+
+ def get_catalog(self, jid, ifrom=None,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq()
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq.enable('security_label_catalog')
+ return iq.send(callback=callback, timeout=timeout)
diff --git a/slixmpp/plugins/xep_0258/stanza.py b/slixmpp/plugins/xep_0258/stanza.py
new file mode 100644
index 00000000..e47bd34f
--- /dev/null
+++ b/slixmpp/plugins/xep_0258/stanza.py
@@ -0,0 +1,141 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from base64 import b64encode, b64decode
+
+from slixmpp.util import bytes
+from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class SecurityLabel(ElementBase):
+ name = 'securitylabel'
+ namespace = 'urn:xmpp:sec-label:0'
+ plugin_attrib = 'security_label'
+
+ def add_equivalent(self, label):
+ equiv = EquivalentLabel(parent=self)
+ equiv.append(label)
+ return equiv
+
+
+class Label(ElementBase):
+ name = 'label'
+ namespace = 'urn:xmpp:sec-label:0'
+ plugin_attrib = 'label'
+
+
+class DisplayMarking(ElementBase):
+ name = 'displaymarking'
+ namespace = 'urn:xmpp:sec-label:0'
+ plugin_attrib = 'display_marking'
+ interfaces = set(['fgcolor', 'bgcolor', 'value'])
+
+ def get_fgcolor(self):
+ return self._get_attr('fgcolor', 'black')
+
+ def get_bgcolor(self):
+ return self._get_attr('fgcolor', 'white')
+
+ def get_value(self):
+ return self.xml.text
+
+ def set_value(self, value):
+ self.xml.text = value
+
+ def del_value(self):
+ self.xml.text = ''
+
+
+class EquivalentLabel(ElementBase):
+ name = 'equivalentlabel'
+ namespace = 'urn:xmpp:sec-label:0'
+ plugin_attrib = 'equivalent_label'
+ plugin_multi_attrib = 'equivalent_labels'
+
+
+class Catalog(ElementBase):
+ name = 'catalog'
+ namespace = 'urn:xmpp:sec-label:catalog:2'
+ plugin_attrib = 'security_label_catalog'
+ interfaces = set(['to', 'from', 'name', 'desc', 'id', 'size', 'restrict'])
+
+ def get_to(self):
+ return JID(self._get_attr('to'))
+ pass
+
+ def set_to(self, value):
+ return self._set_attr('to', str(value))
+
+ def get_from(self):
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value):
+ return self._set_attr('from', str(value))
+
+ def get_restrict(self):
+ value = self._get_attr('restrict', '')
+ if value and value.lower() in ('true', '1'):
+ return True
+ return False
+
+ def set_restrict(self, value):
+ self._del_attr('restrict')
+ if value:
+ self._set_attr('restrict', 'true')
+ elif value is False:
+ self._set_attr('restrict', 'false')
+
+
+class CatalogItem(ElementBase):
+ name = 'catalog'
+ namespace = 'urn:xmpp:sec-label:catalog:2'
+ plugin_attrib = 'item'
+ plugin_multi_attrib = 'items'
+ interfaces = set(['selector', 'default'])
+
+ def get_default(self):
+ value = self._get_attr('default', '')
+ if value.lower() in ('true', '1'):
+ return True
+ return False
+
+ def set_default(self, value):
+ self._del_attr('default')
+ if value:
+ self._set_attr('default', 'true')
+ elif value is False:
+ self._set_attr('default', 'false')
+
+
+class ESSLabel(ElementBase):
+ name = 'esssecuritylabel'
+ namespace = 'urn:xmpp:sec-label:ess:0'
+ plugin_attrib = 'ess'
+ interfaces = set(['value'])
+
+ def get_value(self):
+ if self.xml.text:
+ return b64decode(bytes(self.xml.text))
+ return ''
+
+ def set_value(self, value):
+ self.xml.text = ''
+ if value:
+ self.xml.text = b64encode(bytes(value))
+
+ def del_value(self):
+ self.xml.text = ''
+
+
+register_stanza_plugin(Catalog, CatalogItem, iterable=True)
+register_stanza_plugin(CatalogItem, SecurityLabel)
+register_stanza_plugin(EquivalentLabel, ESSLabel)
+register_stanza_plugin(Label, ESSLabel)
+register_stanza_plugin(SecurityLabel, DisplayMarking)
+register_stanza_plugin(SecurityLabel, EquivalentLabel, iterable=True)
+register_stanza_plugin(SecurityLabel, Label)
diff --git a/slixmpp/plugins/xep_0270.py b/slixmpp/plugins/xep_0270.py
new file mode 100644
index 00000000..4d02a0a1
--- /dev/null
+++ b/slixmpp/plugins/xep_0270.py
@@ -0,0 +1,20 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins import BasePlugin, register_plugin
+
+
+class XEP_0270(BasePlugin):
+
+ name = 'xep_0270'
+ description = 'XEP-0270: XMPP Compliance Suites 2010'
+ dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
+ 'xep_0163', 'xep_0045', 'xep_0085'])
+
+
+register_plugin(XEP_0270)
diff --git a/slixmpp/plugins/xep_0279/__init__.py b/slixmpp/plugins/xep_0279/__init__.py
new file mode 100644
index 00000000..45fd3af0
--- /dev/null
+++ b/slixmpp/plugins/xep_0279/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0279 import stanza
+from slixmpp.plugins.xep_0279.stanza import IPCheck
+from slixmpp.plugins.xep_0279.ipcheck import XEP_0279
+
+
+register_plugin(XEP_0279)
diff --git a/slixmpp/plugins/xep_0279/ipcheck.py b/slixmpp/plugins/xep_0279/ipcheck.py
new file mode 100644
index 00000000..e8cea46f
--- /dev/null
+++ b/slixmpp/plugins/xep_0279/ipcheck.py
@@ -0,0 +1,41 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+import logging
+
+from slixmpp import Iq
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0279 import stanza, IPCheck
+
+
+class XEP_0279(BasePlugin):
+
+ name = 'xep_0279'
+ description = 'XEP-0279: Server IP Check'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, IPCheck)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:sic:0')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0')
+
+ def check_ip(self, ifrom=None, block=True, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq.enable('ip_check')
+ return iq.send(block=block, timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0279/stanza.py b/slixmpp/plugins/xep_0279/stanza.py
new file mode 100644
index 00000000..f80623cd
--- /dev/null
+++ b/slixmpp/plugins/xep_0279/stanza.py
@@ -0,0 +1,30 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class IPCheck(ElementBase):
+
+ name = 'ip'
+ namespace = 'urn:xmpp:sic:0'
+ plugin_attrib = 'ip_check'
+ interfaces = set(['ip_check'])
+ is_extension = True
+
+ def get_ip_check(self):
+ return self.xml.text
+
+ def set_ip_check(self, value):
+ if value:
+ self.xml.text = value
+ else:
+ self.xml.text = ''
+
+ def del_ip_check(self):
+ self.xml.text = ''
diff --git a/slixmpp/plugins/xep_0280/__init__.py b/slixmpp/plugins/xep_0280/__init__.py
new file mode 100644
index 00000000..ed9a19ef
--- /dev/null
+++ b/slixmpp/plugins/xep_0280/__init__.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0280.stanza import ReceivedCarbon, SentCarbon
+from slixmpp.plugins.xep_0280.stanza import PrivateCarbon
+from slixmpp.plugins.xep_0280.stanza import CarbonEnable, CarbonDisable
+from slixmpp.plugins.xep_0280.carbons import XEP_0280
+
+
+register_plugin(XEP_0280)
diff --git a/slixmpp/plugins/xep_0280/carbons.py b/slixmpp/plugins/xep_0280/carbons.py
new file mode 100644
index 00000000..a64ccbfd
--- /dev/null
+++ b/slixmpp/plugins/xep_0280/carbons.py
@@ -0,0 +1,85 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.stanza import Message, Iq
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0280 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0280(BasePlugin):
+
+ """
+ XEP-0280 Message Carbons
+ """
+
+ name = 'xep_0280'
+ description = 'XEP-0280: Message Carbons'
+ dependencies = set(['xep_0030', 'xep_0297'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Carbon Received',
+ StanzaPath('message/carbon_received'),
+ self._handle_carbon_received))
+ self.xmpp.register_handler(
+ Callback('Carbon Sent',
+ StanzaPath('message/carbon_sent'),
+ self._handle_carbon_sent))
+
+ register_stanza_plugin(Message, stanza.ReceivedCarbon)
+ register_stanza_plugin(Message, stanza.SentCarbon)
+ register_stanza_plugin(Message, stanza.PrivateCarbon)
+ register_stanza_plugin(Iq, stanza.CarbonEnable)
+ register_stanza_plugin(Iq, stanza.CarbonDisable)
+
+ register_stanza_plugin(stanza.ReceivedCarbon,
+ self.xmpp['xep_0297'].stanza.Forwarded)
+ register_stanza_plugin(stanza.SentCarbon,
+ self.xmpp['xep_0297'].stanza.Forwarded)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Carbon Received')
+ self.xmpp.remove_handler('Carbon Sent')
+ self.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:carbons:2')
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2')
+
+ def _handle_carbon_received(self, msg):
+ self.xmpp.event('carbon_received', msg)
+
+ def _handle_carbon_sent(self, msg):
+ self.xmpp.event('carbon_sent', msg)
+
+ def enable(self, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq.enable('carbon_enable')
+ return iq.send(timeout_callback=timeout_callback, timeout=timeout,
+ callback=callback)
+
+ def disable(self, ifrom=None, timeout=None, callback=None,
+ timeout_callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq.enable('carbon_disable')
+ return iq.send(timeout_callback=timeout_callback, timeout=timeout,
+ callback=callback)
diff --git a/slixmpp/plugins/xep_0280/stanza.py b/slixmpp/plugins/xep_0280/stanza.py
new file mode 100644
index 00000000..46276189
--- /dev/null
+++ b/slixmpp/plugins/xep_0280/stanza.py
@@ -0,0 +1,64 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class ReceivedCarbon(ElementBase):
+ name = 'received'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_received'
+ interfaces = set(['carbon_received'])
+ is_extension = True
+
+ def get_carbon_received(self):
+ return self['forwarded']['stanza']
+
+ def del_carbon_received(self):
+ del self['forwarded']['stanza']
+
+ def set_carbon_received(self, stanza):
+ self['forwarded']['stanza'] = stanza
+
+
+class SentCarbon(ElementBase):
+ name = 'sent'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_sent'
+ interfaces = set(['carbon_sent'])
+ is_extension = True
+
+ def get_carbon_sent(self):
+ return self['forwarded']['stanza']
+
+ def del_carbon_sent(self):
+ del self['forwarded']['stanza']
+
+ def set_carbon_sent(self, stanza):
+ self['forwarded']['stanza'] = stanza
+
+
+class PrivateCarbon(ElementBase):
+ name = 'private'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_private'
+ interfaces = set()
+
+
+class CarbonEnable(ElementBase):
+ name = 'enable'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_enable'
+ interfaces = set()
+
+
+class CarbonDisable(ElementBase):
+ name = 'disable'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_disable'
+ interfaces = set()
diff --git a/slixmpp/plugins/xep_0297/__init__.py b/slixmpp/plugins/xep_0297/__init__.py
new file mode 100644
index 00000000..8d7adb63
--- /dev/null
+++ b/slixmpp/plugins/xep_0297/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0297 import stanza
+from slixmpp.plugins.xep_0297.stanza import Forwarded
+from slixmpp.plugins.xep_0297.forwarded import XEP_0297
+
+
+register_plugin(XEP_0297)
diff --git a/slixmpp/plugins/xep_0297/forwarded.py b/slixmpp/plugins/xep_0297/forwarded.py
new file mode 100644
index 00000000..7c40bf30
--- /dev/null
+++ b/slixmpp/plugins/xep_0297/forwarded.py
@@ -0,0 +1,64 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+import logging
+
+from slixmpp import Iq, Message, Presence
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.xep_0297 import stanza, Forwarded
+
+
+class XEP_0297(BasePlugin):
+
+ name = 'xep_0297'
+ description = 'XEP-0297: Stanza Forwarding'
+ dependencies = set(['xep_0030', 'xep_0203'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, Forwarded)
+
+ # While these are marked as iterable, that is just for
+ # making it easier to extract the forwarded stanza. There
+ # still can be only a single forwarded stanza.
+ register_stanza_plugin(Forwarded, Message, iterable=True)
+ register_stanza_plugin(Forwarded, Presence, iterable=True)
+ register_stanza_plugin(Forwarded, Iq, iterable=True)
+
+ register_stanza_plugin(Forwarded, self.xmpp['xep_0203'].stanza.Delay)
+
+ self.xmpp.register_handler(
+ Callback('Forwarded Stanza',
+ StanzaPath('message/forwarded'),
+ self._handle_forwarded))
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:forward:0')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:forward:0')
+ self.xmpp.remove_handler('Forwarded Stanza')
+
+ def forward(self, stanza=None, mto=None, mbody=None, mfrom=None, delay=None):
+ stanza.stream = None
+
+ msg = self.xmpp.Message()
+ msg['to'] = mto
+ msg['from'] = mfrom
+ msg['body'] = mbody
+ msg['forwarded']['stanza'] = stanza
+ if delay is not None:
+ msg['forwarded']['delay']['stamp'] = delay
+ msg.send()
+
+ def _handle_forwarded(self, msg):
+ self.xmpp.event('forwarded_stanza', msg)
diff --git a/slixmpp/plugins/xep_0297/stanza.py b/slixmpp/plugins/xep_0297/stanza.py
new file mode 100644
index 00000000..233ad4f6
--- /dev/null
+++ b/slixmpp/plugins/xep_0297/stanza.py
@@ -0,0 +1,36 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Message, Presence, Iq
+from slixmpp.xmlstream import ElementBase
+
+
+class Forwarded(ElementBase):
+ name = 'forwarded'
+ namespace = 'urn:xmpp:forward:0'
+ plugin_attrib = 'forwarded'
+ interfaces = set(['stanza'])
+
+ def get_stanza(self):
+ for stanza in self:
+ if isinstance(stanza, (Message, Presence, Iq)):
+ return stanza
+ return ''
+
+ def set_stanza(self, value):
+ self.del_stanza()
+ self.append(value)
+
+ def del_stanza(self):
+ found_stanzas = []
+ for stanza in self:
+ if isinstance(stanza, (Message, Presence, Iq)):
+ found_stanzas.append(stanza)
+ for stanza in found_stanzas:
+ self.iterables.remove(stanza)
+ self.xml.remove(stanza.xml)
diff --git a/slixmpp/plugins/xep_0302.py b/slixmpp/plugins/xep_0302.py
new file mode 100644
index 00000000..6092b3e3
--- /dev/null
+++ b/slixmpp/plugins/xep_0302.py
@@ -0,0 +1,21 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins import BasePlugin, register_plugin
+
+
+class XEP_0302(BasePlugin):
+
+ name = 'xep_0302'
+ description = 'XEP-0302: XMPP Compliance Suites 2012'
+ dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
+ 'xep_0163', 'xep_0045', 'xep_0085',
+ 'xep_0184', 'xep_0198'])
+
+
+register_plugin(XEP_0302)
diff --git a/slixmpp/plugins/xep_0308/__init__.py b/slixmpp/plugins/xep_0308/__init__.py
new file mode 100644
index 00000000..147cbd4b
--- /dev/null
+++ b/slixmpp/plugins/xep_0308/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0308.stanza import Replace
+from slixmpp.plugins.xep_0308.correction import XEP_0308
+
+
+register_plugin(XEP_0308)
diff --git a/slixmpp/plugins/xep_0308/correction.py b/slixmpp/plugins/xep_0308/correction.py
new file mode 100644
index 00000000..3802b799
--- /dev/null
+++ b/slixmpp/plugins/xep_0308/correction.py
@@ -0,0 +1,52 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.stanza import Message
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0308 import stanza, Replace
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0308(BasePlugin):
+
+ """
+ XEP-0308 Last Message Correction
+ """
+
+ name = 'xep_0308'
+ description = 'XEP-0308: Last Message Correction'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Message Correction',
+ StanzaPath('message/replace'),
+ self._handle_correction))
+
+ register_stanza_plugin(Message, Replace)
+
+ self.xmpp.use_message_ids = True
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Message Correction')
+ self.xmpp.plugin['xep_0030'].del_feature(feature=Replace.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace)
+
+ def _handle_correction(self, msg):
+ self.xmpp.event('message_correction', msg)
diff --git a/slixmpp/plugins/xep_0308/stanza.py b/slixmpp/plugins/xep_0308/stanza.py
new file mode 100644
index 00000000..eb04d1ad
--- /dev/null
+++ b/slixmpp/plugins/xep_0308/stanza.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Replace(ElementBase):
+ name = 'replace'
+ namespace = 'urn:xmpp:message-correct:0'
+ plugin_attrib = 'replace'
+ interfaces = set(['id'])
diff --git a/slixmpp/plugins/xep_0313/__init__.py b/slixmpp/plugins/xep_0313/__init__.py
new file mode 100644
index 00000000..42d9025a
--- /dev/null
+++ b/slixmpp/plugins/xep_0313/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0313.stanza import Result, MAM, Preferences
+from slixmpp.plugins.xep_0313.mam import XEP_0313
+
+
+register_plugin(XEP_0313)
diff --git a/slixmpp/plugins/xep_0313/mam.py b/slixmpp/plugins/xep_0313/mam.py
new file mode 100644
index 00000000..d1c6b983
--- /dev/null
+++ b/slixmpp/plugins/xep_0313/mam.py
@@ -0,0 +1,85 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+import slixmpp
+from slixmpp.stanza import Message, Iq
+from slixmpp.exceptions import XMPPError
+from slixmpp.xmlstream.handler import Collector
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.xep_0313 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0313(BasePlugin):
+
+ """
+ XEP-0313 Message Archive Management
+ """
+
+ name = 'xep_0313'
+ description = 'XEP-0313: Message Archive Management'
+ dependencies = set(['xep_0030', 'xep_0050', 'xep_0059', 'xep_0297'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.MAM)
+ register_stanza_plugin(Iq, stanza.Preferences)
+ register_stanza_plugin(Message, stanza.Result)
+ register_stanza_plugin(Message, stanza.Archived, iterable=True)
+ register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded)
+ register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
+
+ def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None,
+ timeout=None, callback=None, iterator=False):
+ iq = self.xmpp.Iq()
+ query_id = iq['id']
+
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq['mam']['queryid'] = query_id
+ iq['mam']['start'] = start
+ iq['mam']['end'] = end
+ iq['mam']['with'] = with_jid
+
+ collector = Collector(
+ 'MAM_Results_%s' % query_id,
+ StanzaPath('message/mam_result@queryid=%s' % query_id))
+ self.xmpp.register_handler(collector)
+
+ if iterator:
+ return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results')
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['mam']['results'] = results
+ callback(iq)
+ return iq.send(timeout=timeout, callback=wrapped_cb)
+
+ def set_preferences(self, jid=None, default=None, always=None, never=None,
+ ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['mam_prefs']['default'] = default
+ iq['mam_prefs']['always'] = always
+ iq['mam_prefs']['never'] = never
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_configuration_commands(self, jid, **kwargs):
+ return self.xmpp['xep_0030'].get_items(
+ jid=jid,
+ node='urn:xmpp:mam#configure',
+ **kwargs)
diff --git a/slixmpp/plugins/xep_0313/stanza.py b/slixmpp/plugins/xep_0313/stanza.py
new file mode 100644
index 00000000..d7cfa222
--- /dev/null
+++ b/slixmpp/plugins/xep_0313/stanza.py
@@ -0,0 +1,139 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permissio
+"""
+
+import datetime as dt
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream import ElementBase, ET
+from slixmpp.plugins import xep_0082
+
+
+class MAM(ElementBase):
+ name = 'query'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam'
+ interfaces = set(['queryid', 'start', 'end', 'with', 'results'])
+ sub_interfaces = set(['start', 'end', 'with'])
+
+ def setup(self, xml=None):
+ ElementBase.setup(self, xml)
+ self._results = []
+
+ def get_start(self):
+ timestamp = self._get_sub_text('start')
+ return xep_0082.parse(timestamp)
+
+ def set_start(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_sub_text('start', value)
+
+ def get_end(self):
+ timestamp = self._get_sub_text('end')
+ return xep_0082.parse(timestamp)
+
+ def set_end(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_sub_text('end', value)
+
+ def get_with(self):
+ return JID(self._get_sub_text('with'))
+
+ def set_with(self, value):
+ self._set_sub_text('with', str(value))
+
+ # The results interface is meant only as an easy
+ # way to access the set of collected message responses
+ # from the query.
+
+ def get_results(self):
+ return self._results
+
+ def set_results(self, values):
+ self._results = values
+
+ def del_results(self):
+ self._results = []
+
+
+class Preferences(ElementBase):
+ name = 'prefs'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam_prefs'
+ interfaces = set(['default', 'always', 'never'])
+ sub_interfaces = set(['always', 'never'])
+
+ def get_always(self):
+ results = set()
+
+ jids = self.xml.findall('{%s}always/{%s}jid' % (
+ self.namespace, self.namespace))
+
+ for jid in jids:
+ results.add(JID(jid.text))
+
+ return results
+
+ def set_always(self, value):
+ self._set_sub_text('always', '', keep=True)
+ always = self.xml.find('{%s}always' % self.namespace)
+ always.clear()
+
+ if not isinstance(value, (list, set)):
+ value = [value]
+
+ for jid in value:
+ jid_xml = ET.Element('{%s}jid' % self.namespace)
+ jid_xml.text = str(jid)
+ always.append(jid_xml)
+
+ def get_never(self):
+ results = set()
+
+ jids = self.xml.findall('{%s}never/{%s}jid' % (
+ self.namespace, self.namespace))
+
+ for jid in jids:
+ results.add(JID(jid.text))
+
+ return results
+
+ def set_never(self, value):
+ self._set_sub_text('never', '', keep=True)
+ never = self.xml.find('{%s}never' % self.namespace)
+ never.clear()
+
+ if not isinstance(value, (list, set)):
+ value = [value]
+
+ for jid in value:
+ jid_xml = ET.Element('{%s}jid' % self.namespace)
+ jid_xml.text = str(jid)
+ never.append(jid_xml)
+
+
+class Result(ElementBase):
+ name = 'result'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam_result'
+ interfaces = set(['queryid', 'id'])
+
+
+class Archived(ElementBase):
+ name = 'archived'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam_archived'
+ plugin_multi_attrib = 'mam_archives'
+ interfaces = set(['by', 'id'])
+
+ def get_by(self):
+ return JID(self._get_attr('by'))
+
+ def set_by(self):
+ return self._set_attr('by', str(value))
diff --git a/slixmpp/plugins/xep_0319/__init__.py b/slixmpp/plugins/xep_0319/__init__.py
new file mode 100644
index 00000000..a9253b49
--- /dev/null
+++ b/slixmpp/plugins/xep_0319/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0319 import stanza
+from slixmpp.plugins.xep_0319.stanza import Idle
+from slixmpp.plugins.xep_0319.idle import XEP_0319
+
+
+register_plugin(XEP_0319)
diff --git a/slixmpp/plugins/xep_0319/idle.py b/slixmpp/plugins/xep_0319/idle.py
new file mode 100644
index 00000000..1fd980a5
--- /dev/null
+++ b/slixmpp/plugins/xep_0319/idle.py
@@ -0,0 +1,75 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from datetime import datetime, timedelta
+
+from slixmpp.stanza import Presence
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.xep_0319 import stanza
+
+
+class XEP_0319(BasePlugin):
+ name = 'xep_0319'
+ description = 'XEP-0319: Last User Interaction in Presence'
+ dependencies = set(['xep_0012'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self._idle_stamps = {}
+ register_stanza_plugin(Presence, stanza.Idle)
+ self.api.register(self._set_idle,
+ 'set_idle',
+ default=True)
+ self.api.register(self._get_idle,
+ 'get_idle',
+ default=True)
+ self.xmpp.register_handler(
+ Callback('Idle Presence',
+ StanzaPath('presence/idle'),
+ self._idle_presence))
+ self.xmpp.add_filter('out', self._stamp_idle_presence)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:idle:1')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:idle:1')
+ self.xmpp.del_filter('out', self._stamp_idle_presence)
+ self.xmpp.remove_handler('Idle Presence')
+
+ def idle(self, jid=None, since=None):
+ seconds = None
+ if since is None:
+ since = datetime.now()
+ else:
+ seconds = datetime.now() - since
+ self.api['set_idle'](jid, None, None, since)
+ self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds)
+
+ def active(self, jid=None):
+ self.api['set_idle'](jid, None, None, None)
+ self.xmpp['xep_0012'].del_last_activity(jid)
+
+ def _set_idle(self, jid, node, ifrom, data):
+ self._idle_stamps[jid] = data
+
+ def _get_idle(self, jid, node, ifrom, data):
+ return self._idle_stamps.get(jid, None)
+
+ def _idle_presence(self, pres):
+ self.xmpp.event('presence_idle', pres)
+
+ def _stamp_idle_presence(self, stanza):
+ if isinstance(stanza, Presence):
+ since = self.api['get_idle'](stanza['from'] or self.xmpp.boundjid)
+ if since:
+ stanza['idle']['since'] = since
+ return stanza
diff --git a/slixmpp/plugins/xep_0319/stanza.py b/slixmpp/plugins/xep_0319/stanza.py
new file mode 100644
index 00000000..ea70087b
--- /dev/null
+++ b/slixmpp/plugins/xep_0319/stanza.py
@@ -0,0 +1,28 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from slixmpp.xmlstream import ElementBase
+from slixmpp.plugins import xep_0082
+
+
+class Idle(ElementBase):
+ name = 'idle'
+ namespace = 'urn:xmpp:idle:1'
+ plugin_attrib = 'idle'
+ interfaces = set(['since'])
+
+ def get_since(self):
+ timestamp = self._get_attr('since')
+ return xep_0082.parse(timestamp)
+
+ def set_since(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('since', value)
diff --git a/slixmpp/plugins/xep_0323/__init__.py b/slixmpp/plugins/xep_0323/__init__.py
new file mode 100644
index 00000000..6bf42fd6
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0323.sensordata import XEP_0323
+from slixmpp.plugins.xep_0323 import stanza
+
+register_plugin(XEP_0323)
+
+xep_0323=XEP_0323
diff --git a/slixmpp/plugins/xep_0323/device.py b/slixmpp/plugins/xep_0323/device.py
new file mode 100644
index 00000000..994fc5ce
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/device.py
@@ -0,0 +1,258 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime
+import logging
+
+class Device(object):
+ """
+ Example implementation of a device readout object.
+ Is registered in the XEP_0323.register_node call
+ The device object may be any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_field
+ request_fields
+ """
+
+ def __init__(self, nodeId, fields=None):
+ if not fields:
+ fields = {}
+
+ self.nodeId = nodeId
+ self.fields = fields # see fields described below
+ # {'type':'numeric',
+ # 'name':'myname',
+ # 'value': 42,
+ # 'unit':'Z'}]
+ self.timestamp_data = {}
+ self.momentary_data = {}
+ self.momentary_timestamp = ""
+ logging.debug("Device object started nodeId %s",nodeId)
+
+ def has_field(self, field):
+ """
+ Returns true if the supplied field name exists in this device.
+
+ Arguments:
+ field -- The field name
+ """
+ if field in self.fields.keys():
+ return True
+ return False
+
+ def refresh(self, fields):
+ """
+ override method to do the refresh work
+ refresh values from hardware or other
+ """
+ pass
+
+
+ def request_fields(self, fields, flags, session, callback):
+ """
+ Starts a data readout. Verifies the requested fields,
+ refreshes the data (if needed) and calls the callback
+ with requested data.
+
+
+ Arguments:
+ fields -- List of field names to readout
+ flags -- [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ session -- Session id, only used in the callback as identifier
+ callback -- Callback function to call when data is available.
+
+ The callback function must support the following arguments:
+
+ session -- Session id, as supplied in the request_fields call
+ nodeId -- Identifier for this device
+ result -- The current result status of the readout. Valid values are:
+ "error" - Readout failed.
+ "fields" - Contains readout data.
+ "done" - Indicates that the readout is complete. May contain
+ readout data.
+ timestamp_block -- [optional] Only applies when result != "error"
+ The readout data. Structured as a dictionary:
+ {
+ timestamp: timestamp for this datablock,
+ fields: list of field dictionary (one per readout field).
+ readout field dictionary format:
+ {
+ type: The field type (numeric, boolean, dateTime, timeSpan, string, enum)
+ name: The field name
+ value: The field value
+ unit: The unit of the field. Only applies to type numeric.
+ dataType: The datatype of the field. Only applies to type enum.
+ flags: [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ }
+ }
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+
+ """
+ logging.debug("request_fields called looking for fields %s",fields)
+ if len(fields) > 0:
+ # Check availiability
+ for f in fields:
+ if f not in self.fields.keys():
+ self._send_reject(session, callback)
+ return False
+ else:
+ # Request all fields
+ fields = self.fields.keys()
+
+
+ # Refresh data from device
+ # ...
+ logging.debug("about to refresh device fields %s",fields)
+ self.refresh(fields)
+
+ if "momentary" in flags and flags['momentary'] == "true" or \
+ "all" in flags and flags['all'] == "true":
+ ts_block = {}
+ timestamp = ""
+
+ if len(self.momentary_timestamp) > 0:
+ timestamp = self.momentary_timestamp
+ else:
+ timestamp = self._get_timestamp()
+
+ field_block = []
+ for f in self.momentary_data:
+ if f in fields:
+ field_block.append({"name": f,
+ "type": self.fields[f]["type"],
+ "unit": self.fields[f]["unit"],
+ "dataType": self.fields[f]["dataType"],
+ "value": self.momentary_data[f]["value"],
+ "flags": self.momentary_data[f]["flags"]})
+ ts_block["timestamp"] = timestamp
+ ts_block["fields"] = field_block
+
+ callback(session, result="done", nodeId=self.nodeId, timestamp_block=ts_block)
+ return
+
+ from_flag = self._datetime_flag_parser(flags, 'from')
+ to_flag = self._datetime_flag_parser(flags, 'to')
+
+ for ts in sorted(self.timestamp_data.keys()):
+ tsdt = datetime.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S")
+ if not from_flag is None:
+ if tsdt < from_flag:
+ #print (str(tsdt) + " < " + str(from_flag))
+ continue
+ if not to_flag is None:
+ if tsdt > to_flag:
+ #print (str(tsdt) + " > " + str(to_flag))
+ continue
+
+ ts_block = {}
+ field_block = []
+
+ for f in self.timestamp_data[ts]:
+ if f in fields:
+ field_block.append({"name": f,
+ "type": self.fields[f]["type"],
+ "unit": self.fields[f]["unit"],
+ "dataType": self.fields[f]["dataType"],
+ "value": self.timestamp_data[ts][f]["value"],
+ "flags": self.timestamp_data[ts][f]["flags"]})
+
+ ts_block["timestamp"] = ts
+ ts_block["fields"] = field_block
+ callback(session, result="fields", nodeId=self.nodeId, timestamp_block=ts_block)
+ callback(session, result="done", nodeId=self.nodeId, timestamp_block=None)
+
+ def _datetime_flag_parser(self, flags, flagname):
+ if not flagname in flags:
+ return None
+
+ dt = None
+ try:
+ dt = datetime.datetime.strptime(flags[flagname], "%Y-%m-%dT%H:%M:%S")
+ except ValueError:
+ # Badly formatted datetime, ignore it
+ pass
+ return dt
+
+
+ def _get_timestamp(self):
+ """
+ Generates a properly formatted timestamp of current time
+ """
+ return datetime.datetime.now().replace(microsecond=0).isoformat()
+
+ def _send_reject(self, session, callback):
+ """
+ Sends a reject to the caller
+
+ Arguments:
+ session -- Session id, see definition in request_fields function
+ callback -- Callback function, see definition in request_fields function
+ """
+ callback(session, result="error", nodeId=self.nodeId, timestamp_block=None, error_msg="Reject")
+
+ def _add_field(self, name, typename, unit=None, dataType=None):
+ """
+ Adds a field to the device
+
+ Arguments:
+ name -- Name of the field
+ typename -- Type of the field (numeric, boolean, dateTime, timeSpan, string, enum)
+ unit -- [optional] only applies to "numeric". Unit for the field.
+ dataType -- [optional] only applies to "enum". Datatype for the field.
+ """
+ self.fields[name] = {"type": typename, "unit": unit, "dataType": dataType}
+
+ def _add_field_timestamp_data(self, name, timestamp, value, flags=None):
+ """
+ Adds timestamped data to a field
+
+ Arguments:
+ name -- Name of the field
+ timestamp -- Timestamp for the data (string)
+ value -- Field value at the timestamp
+ flags -- [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ if not name in self.fields.keys():
+ return False
+ if not timestamp in self.timestamp_data:
+ self.timestamp_data[timestamp] = {}
+
+ self.timestamp_data[timestamp][name] = {"value": value, "flags": flags}
+ return True
+
+ def _add_field_momentary_data(self, name, value, flags=None):
+ """
+ Sets momentary data to a field
+
+ Arguments:
+ name -- Name of the field
+ value -- Field value at the timestamp
+ flags -- [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ if name not in self.fields:
+ return False
+ if flags is None:
+ flags = {}
+
+ flags["momentary"] = "true"
+ self.momentary_data[name] = {"value": value, "flags": flags}
+ return True
+
+ def _set_momentary_timestamp(self, timestamp):
+ """
+ This function is only for unit testing to produce predictable results.
+ """
+ self.momentary_timestamp = timestamp
+
diff --git a/slixmpp/plugins/xep_0323/sensordata.py b/slixmpp/plugins/xep_0323/sensordata.py
new file mode 100644
index 00000000..c88deee9
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/sensordata.py
@@ -0,0 +1,712 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import time
+import datetime
+from threading import Thread, Lock, Timer
+
+from slixmpp.plugins.xep_0323.timerreset import TimerReset
+
+from slixmpp.xmlstream import JID
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0323 import stanza
+from slixmpp.plugins.xep_0323.stanza import Sensordata
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0323(BasePlugin):
+
+ """
+ XEP-0323: IoT Sensor Data
+
+
+ This XEP provides the underlying architecture, basic operations and data
+ structures for sensor data communication over XMPP networks. It includes
+ a hardware abstraction model, removing any technical detail implemented
+ in underlying technologies.
+
+ Also see <http://xmpp.org/extensions/xep-0323.html>
+
+ Configuration Values:
+ threaded -- Indicates if communication with sensors should be threaded.
+ Defaults to True.
+
+ Events:
+ Sensor side
+ -----------
+ Sensordata Event:Req -- Received a request for data
+ Sensordata Event:Cancel -- Received a cancellation for a request
+
+ Client side
+ -----------
+ Sensordata Event:Accepted -- Received a accept from sensor for a request
+ Sensordata Event:Rejected -- Received a reject from sensor for a request
+ Sensordata Event:Cancelled -- Received a cancel confirm from sensor
+ Sensordata Event:Fields -- Received fields from sensor for a request
+ This may be triggered multiple times since
+ the sensor can split up its response in
+ multiple messages.
+ Sensordata Event:Failure -- Received a failure indication from sensor
+ for a request. Typically a comm timeout.
+
+ Attributes:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+ sessions -- A dictionary or equivalent backend mapping
+ session IDs to dictionaries containing data
+ relevant to a request's session. This dictionary is used
+ both by the client and sensor side. On client side, seqnr
+ is used as key, while on sensor side, a session_id is used
+ as key. This ensures that the two will not collide, so
+ one instance can be both client and sensor.
+ Sensor side
+ -----------
+ nodes -- A dictionary mapping sensor nodes that are serviced through
+ this XMPP instance to their device handlers ("drivers").
+ Client side
+ -----------
+ last_seqnr -- The last used sequence number (integer). One sequence of
+ communication (e.g. -->request, <--accept, <--fields)
+ between client and sensor is identified by a unique
+ sequence number (unique between the client/sensor pair)
+
+ Methods:
+ plugin_init -- Overrides BasePlugin.plugin_init
+ post_init -- Overrides BasePlugin.post_init
+ plugin_end -- Overrides BasePlugin.plugin_end
+
+ Sensor side
+ -----------
+ register_node -- Register a sensor as available from this XMPP
+ instance.
+
+ Client side
+ -----------
+ request_data -- Initiates a request for data from one or more
+ sensors. Non-blocking, a callback function will
+ be called when data is available.
+
+ """
+
+ name = 'xep_0323'
+ description = 'XEP-0323 Internet of Things - Sensor Data'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+
+ default_config = {
+ 'threaded': True
+ }
+
+ def plugin_init(self):
+ """ Start the XEP-0323 plugin """
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Req',
+ StanzaPath('iq@type=get/req'),
+ self._handle_event_req))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Accepted',
+ StanzaPath('iq@type=result/accepted'),
+ self._handle_event_accepted))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Rejected',
+ StanzaPath('iq@type=error/rejected'),
+ self._handle_event_rejected))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Cancel',
+ StanzaPath('iq@type=get/cancel'),
+ self._handle_event_cancel))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Cancelled',
+ StanzaPath('iq@type=result/cancelled'),
+ self._handle_event_cancelled))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Fields',
+ StanzaPath('message/fields'),
+ self._handle_event_fields))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Failure',
+ StanzaPath('message/failure'),
+ self._handle_event_failure))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Started',
+ StanzaPath('message/started'),
+ self._handle_event_started))
+
+ # Server side dicts
+ self.nodes = {}
+ self.sessions = {}
+
+ self.last_seqnr = 0
+ self.seqnr_lock = Lock()
+
+ ## For testing only
+ self.test_authenticated_from = ""
+
+ def post_init(self):
+ """ Init complete. Register our features in Service discovery. """
+ BasePlugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Sensordata.namespace)
+ self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple())
+
+ def _new_session(self):
+ """ Return a new session ID. """
+ return str(time.time()) + '-' + self.xmpp.new_id()
+
+ def session_bind(self, jid):
+ logging.debug("setting the Disco discovery for %s" % Sensordata.namespace)
+ self.xmpp['xep_0030'].add_feature(Sensordata.namespace)
+ self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple())
+
+
+ def plugin_end(self):
+ """ Stop the XEP-0323 plugin """
+ self.sessions.clear()
+ self.xmpp.remove_handler('Sensordata Event:Req')
+ self.xmpp.remove_handler('Sensordata Event:Accepted')
+ self.xmpp.remove_handler('Sensordata Event:Rejected')
+ self.xmpp.remove_handler('Sensordata Event:Cancel')
+ self.xmpp.remove_handler('Sensordata Event:Cancelled')
+ self.xmpp.remove_handler('Sensordata Event:Fields')
+ self.xmpp['xep_0030'].del_feature(feature=Sensordata.namespace)
+
+
+ # =================================================================
+ # Sensor side (data provider) API
+
+ def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None):
+ """
+ Register a sensor/device as available for serving of data through this XMPP
+ instance.
+
+ The device object may by any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_field
+ request_fields
+ according to the interfaces shown in the example device.py file.
+
+ Arguments:
+ nodeId -- The identifier for the device
+ device -- The device object
+ commTimeout -- Time in seconds to wait between each callback from device during
+ a data readout. Float.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ self.nodes[nodeId] = {"device": device,
+ "commTimeout": commTimeout,
+ "sourceId": sourceId,
+ "cacheType": cacheType}
+
+ def _set_authenticated(self, auth=''):
+ """ Internal testing function """
+ self.test_authenticated_from = auth
+
+
+ def _handle_event_req(self, iq):
+ """
+ Event handler for reception of an Iq with req - this is a request.
+
+ Verifies that
+ - all the requested nodes are available
+ - at least one of the requested fields is available from at least
+ one of the nodes
+
+ If the request passes verification, an accept response is sent, and
+ the readout process is started in a separate thread.
+ If the verification fails, a reject message is sent.
+ """
+
+ seqnr = iq['req']['seqnr']
+ error_msg = ''
+ req_ok = True
+
+ # Authentication
+ if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from:
+ # Invalid authentication
+ req_ok = False
+ error_msg = "Access denied"
+
+ # Nodes
+ process_nodes = []
+ if len(iq['req']['nodes']) > 0:
+ for n in iq['req']['nodes']:
+ if not n['nodeId'] in self.nodes:
+ req_ok = False
+ error_msg = "Invalid nodeId " + n['nodeId']
+ process_nodes = [n['nodeId'] for n in iq['req']['nodes']]
+ else:
+ process_nodes = self.nodes.keys()
+
+ # Fields - if we just find one we are happy, otherwise we reject
+ process_fields = []
+ if len(iq['req']['fields']) > 0:
+ found = False
+ for f in iq['req']['fields']:
+ for node in self.nodes:
+ if self.nodes[node]["device"].has_field(f['name']):
+ found = True
+ break
+ if not found:
+ req_ok = False
+ error_msg = "Invalid field " + f['name']
+ process_fields = [f['name'] for n in iq['req']['fields']]
+
+ req_flags = iq['req']._get_flags()
+
+ request_delay_sec = None
+ if 'when' in req_flags:
+ # Timed request - requires datetime string in iso format
+ # ex. 2013-04-05T15:00:03
+ dt = None
+ try:
+ dt = datetime.datetime.strptime(req_flags['when'], "%Y-%m-%dT%H:%M:%S")
+ except ValueError:
+ req_ok = False
+ error_msg = "Invalid datetime in 'when' flag, please use ISO format (i.e. 2013-04-05T15:00:03)."
+
+ if not dt is None:
+ # Datetime properly formatted
+ dtnow = datetime.datetime.now()
+ dtdiff = dt - dtnow
+ request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600
+ if request_delay_sec <= 0:
+ req_ok = False
+ error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past. Current time: " + dtnow.isoformat()
+
+ if req_ok:
+ session = self._new_session()
+ self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr}
+ self.sessions[session]["commTimers"] = {}
+ self.sessions[session]["nodeDone"] = {}
+
+ iq = iq.reply()
+ iq['accepted']['seqnr'] = seqnr
+ if not request_delay_sec is None:
+ iq['accepted']['queued'] = "true"
+ iq.send()
+
+ self.sessions[session]["node_list"] = process_nodes
+
+ if not request_delay_sec is None:
+ # Delay request to requested time
+ timer = Timer(request_delay_sec, self._event_delayed_req, args=(session, process_fields, req_flags))
+ self.sessions[session]["commTimers"]["delaytimer"] = timer
+ timer.start()
+ return
+
+ if self.threaded:
+ tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags))
+ tr_req.start()
+ else:
+ self._threaded_node_request(session, process_fields, req_flags)
+
+ else:
+ iq = iq.reply()
+ iq['type'] = 'error'
+ iq['rejected']['seqnr'] = seqnr
+ iq['rejected']['error'] = error_msg
+ iq.send()
+
+ def _threaded_node_request(self, session, process_fields, flags):
+ """
+ Helper function to handle the device readouts in a separate thread.
+
+ Arguments:
+ session -- The request session id
+ process_fields -- The fields to request from the devices
+ flags -- [optional] flags to pass to the devices, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ for node in self.sessions[session]["node_list"]:
+ self.sessions[session]["nodeDone"][node] = False
+
+ for node in self.sessions[session]["node_list"]:
+ timer = TimerReset(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node))
+ self.sessions[session]["commTimers"][node] = timer
+ timer.start()
+ self.nodes[node]['device'].request_fields(process_fields, flags=flags, session=session, callback=self._device_field_request_callback)
+
+ def _event_comm_timeout(self, session, nodeId):
+ """
+ Triggered if any of the readout operations timeout.
+ Sends a failure message back to the client, stops communicating
+ with the failing device.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The id of the device which timed out
+ """
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['failure']['seqnr'] = self.sessions[session]['seqnr']
+ msg['failure']['error']['text'] = "Timeout"
+ msg['failure']['error']['nodeId'] = nodeId
+ msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat()
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ msg['failure']['done'] = 'true'
+ msg.send()
+ # The session is complete, delete it
+ del self.sessions[session]
+
+ def _event_delayed_req(self, session, process_fields, req_flags):
+ """
+ Triggered when the timer from a delayed request fires.
+
+ Arguments:
+ session -- The request session id
+ process_fields -- The fields to request from the devices
+ flags -- [optional] flags to pass to the devices, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['started']['seqnr'] = self.sessions[session]['seqnr']
+ msg.send()
+
+ if self.threaded:
+ tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags))
+ tr_req.start()
+ else:
+ self._threaded_node_request(session, process_fields, req_flags)
+
+ def _all_nodes_done(self, session):
+ """
+ Checks whether all devices are done replying to the readout.
+
+ Arguments:
+ session -- The request session id
+ """
+ for n in self.sessions[session]["nodeDone"]:
+ if not self.sessions[session]["nodeDone"][n]:
+ return False
+ return True
+
+ def _device_field_request_callback(self, session, nodeId, result, timestamp_block, error_msg=None):
+ """
+ Callback function called by the devices when they have any additional data.
+ Composes a message with the data and sends it back to the client, and resets
+ the timeout timer for the device.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The device id which initiated the callback
+ result -- The current result status of the readout. Valid values are:
+ "error" - Readout failed.
+ "fields" - Contains readout data.
+ "done" - Indicates that the readout is complete. May contain
+ readout data.
+ timestamp_block -- [optional] Only applies when result != "error"
+ The readout data. Structured as a dictionary:
+ {
+ timestamp: timestamp for this datablock,
+ fields: list of field dictionary (one per readout field).
+ readout field dictionary format:
+ {
+ type: The field type (numeric, boolean, dateTime, timeSpan, string, enum)
+ name: The field name
+ value: The field value
+ unit: The unit of the field. Only applies to type numeric.
+ dataType: The datatype of the field. Only applies to type enum.
+ flags: [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ }
+ }
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+ """
+ if not session in self.sessions:
+ # This can happen if a session was deleted, like in a cancellation. Just drop the data.
+ return
+
+ if result == "error":
+ self.sessions[session]["commTimers"][nodeId].cancel()
+
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['failure']['seqnr'] = self.sessions[session]['seqnr']
+ msg['failure']['error']['text'] = error_msg
+ msg['failure']['error']['nodeId'] = nodeId
+ msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat()
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ msg['failure']['done'] = 'true'
+ # The session is complete, delete it
+ del self.sessions[session]
+ msg.send()
+ else:
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['fields']['seqnr'] = self.sessions[session]['seqnr']
+
+ if timestamp_block is not None and len(timestamp_block) > 0:
+ node = msg['fields'].add_node(nodeId)
+ ts = node.add_timestamp(timestamp_block["timestamp"])
+
+ for f in timestamp_block["fields"]:
+ data = ts.add_data( typename=f['type'],
+ name=f['name'],
+ value=f['value'],
+ unit=f['unit'],
+ dataType=f['dataType'],
+ flags=f['flags'])
+
+ if result == "done":
+ self.sessions[session]["commTimers"][nodeId].cancel()
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ # The session is complete, delete it
+ del self.sessions[session]
+ msg['fields']['done'] = 'true'
+ else:
+ # Restart comm timer
+ self.sessions[session]["commTimers"][nodeId].reset()
+
+ msg.send()
+
+ def _handle_event_cancel(self, iq):
+ """ Received Iq with cancel - this is a cancel request.
+ Delete the session and confirm. """
+
+ seqnr = iq['cancel']['seqnr']
+ # Find the session
+ for s in self.sessions:
+ if self.sessions[s]['from'] == iq['from'] and self.sessions[s]['to'] == iq['to'] and self.sessions[s]['seqnr'] == seqnr:
+ # found it. Cancel all timers
+ for n in self.sessions[s]["commTimers"]:
+ self.sessions[s]["commTimers"][n].cancel()
+
+ # Confirm
+ iq = iq.reply()
+ iq['type'] = 'result'
+ iq['cancelled']['seqnr'] = seqnr
+ iq.send()
+
+ # Delete session
+ del self.sessions[s]
+ return
+
+ # Could not find session, send reject
+ iq = iq.reply()
+ iq['type'] = 'error'
+ iq['rejected']['seqnr'] = seqnr
+ iq['rejected']['error'] = "Cancel request received, no matching request is active."
+ iq.send()
+
+ # =================================================================
+ # Client side (data retriever) API
+
+ def request_data(self, from_jid, to_jid, callback, nodeIds=None, fields=None, flags=None):
+ """
+ Called on the client side to initiate a data readout.
+ Composes a message with the request and sends it to the device(s).
+ Does not block, the callback will be called when data is available.
+
+ Arguments:
+ from_jid -- The jid of the requester
+ to_jid -- The jid of the device(s)
+ callback -- The callback function to call when data is available.
+
+ The callback function must support the following arguments:
+
+ from_jid -- The jid of the responding device(s)
+ result -- The current result status of the readout. Valid values are:
+ "accepted" - Readout request accepted
+ "queued" - Readout request accepted and queued
+ "rejected" - Readout request rejected
+ "failure" - Readout failed.
+ "cancelled" - Confirmation of request cancellation.
+ "started" - Previously queued request is now started
+ "fields" - Contains readout data.
+ "done" - Indicates that the readout is complete.
+
+ nodeId -- [optional] Mandatory when result == "fields" or "failure".
+ The node Id of the responding device. One callback will only
+ contain data from one device.
+ timestamp -- [optional] Mandatory when result == "fields".
+ The timestamp of data in this callback. One callback will only
+ contain data from one timestamp.
+ fields -- [optional] Mandatory when result == "fields".
+ List of field dictionaries representing the readout data.
+ Dictionary format:
+ {
+ typename: The field type (numeric, boolean, dateTime, timeSpan, string, enum)
+ name: The field name
+ value: The field value
+ unit: The unit of the field. Only applies to type numeric.
+ dataType: The datatype of the field. Only applies to type enum.
+ flags: [optional] data classifier flags for the field, e.g. momentary.
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ }
+
+ error_msg -- [optional] Mandatory when result == "rejected" or "failure".
+ Details about why the request is rejected or failed.
+ "rejected" means that the request is stopped, but note that the
+ request will continue even after a "failure". "failure" only means
+ that communication was stopped to that specific device, other
+ device(s) (if any) will continue their readout.
+
+ nodeIds -- [optional] Limits the request to the node Ids in this list.
+ fields -- [optional] Limits the request to the field names in this list.
+ flags -- [optional] Limits the request according to the flags, or sets
+ readout conditions such as timing.
+
+ Return value:
+ session -- Session identifier. Client can use this as a reference to cancel
+ the request.
+ """
+ iq = self.xmpp.Iq()
+ iq['from'] = from_jid
+ iq['to'] = to_jid
+ iq['type'] = "get"
+ seqnr = self._get_new_seqnr()
+ iq['id'] = seqnr
+ iq['req']['seqnr'] = seqnr
+ if nodeIds is not None:
+ for nodeId in nodeIds:
+ iq['req'].add_node(nodeId)
+ if fields is not None:
+ for field in fields:
+ iq['req'].add_field(field)
+
+ iq['req']._set_flags(flags)
+
+ self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr, "callback": callback}
+ iq.send()
+
+ return seqnr
+
+ def cancel_request(self, session):
+ """
+ Called on the client side to cancel a request for data readout.
+ Composes a message with the cancellation and sends it to the device(s).
+ Does not block, the callback will be called when cancellation is
+ confirmed.
+
+ Arguments:
+ session -- The session id of the request to cancel
+ """
+ seqnr = session
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[seqnr]['from']
+ iq['to'] = self.sessions[seqnr]['to']
+ iq['type'] = "get"
+ iq['id'] = seqnr
+ iq['cancel']['seqnr'] = seqnr
+ iq.send()
+
+ def _get_new_seqnr(self):
+ """ Returns a unique sequence number (unique across threads) """
+ self.seqnr_lock.acquire()
+ self.last_seqnr += 1
+ self.seqnr_lock.release()
+ return str(self.last_seqnr)
+
+ def _handle_event_accepted(self, iq):
+ """ Received Iq with accepted - request was accepted """
+ seqnr = iq['accepted']['seqnr']
+ result = "accepted"
+ if iq['accepted']['queued'] == 'true':
+ result = "queued"
+
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=iq['from'], result=result)
+
+ def _handle_event_rejected(self, iq):
+ """ Received Iq with rejected - this is a reject.
+ Delete the session. """
+ seqnr = iq['rejected']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=iq['from'], result="rejected", error_msg=iq['rejected']['error'])
+ # Session terminated
+ del self.sessions[seqnr]
+
+ def _handle_event_cancelled(self, iq):
+ """
+ Received Iq with cancelled - this is a cancel confirm.
+ Delete the session.
+ """
+ seqnr = iq['cancelled']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=iq['from'], result="cancelled")
+ # Session cancelled
+ del self.sessions[seqnr]
+
+ def _handle_event_fields(self, msg):
+ """
+ Received Msg with fields - this is a data response to a request.
+ If this is the last data block, issue a "done" callback.
+ """
+ seqnr = msg['fields']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ for node in msg['fields']['nodes']:
+ for ts in node['timestamps']:
+ fields = []
+ for d in ts['datas']:
+ field_block = {}
+ field_block["name"] = d['name']
+ field_block["typename"] = d._get_typename()
+ field_block["value"] = d['value']
+ if not d['unit'] == "": field_block["unit"] = d['unit']
+ if not d['dataType'] == "": field_block["dataType"] = d['dataType']
+ flags = d._get_flags()
+ if not len(flags) == 0:
+ field_block["flags"] = flags
+ fields.append(field_block)
+
+ callback(from_jid=msg['from'], result="fields", nodeId=node['nodeId'], timestamp=ts['value'], fields=fields)
+
+ if msg['fields']['done'] == "true":
+ callback(from_jid=msg['from'], result="done")
+ # Session done
+ del self.sessions[seqnr]
+
+ def _handle_event_failure(self, msg):
+ """
+ Received Msg with failure - our request failed
+ Delete the session.
+ """
+ seqnr = msg['failure']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=msg['from'], result="failure", nodeId=msg['failure']['error']['nodeId'], timestamp=msg['failure']['error']['timestamp'], error_msg=msg['failure']['error']['text'])
+
+ # Session failed
+ del self.sessions[seqnr]
+
+ def _handle_event_started(self, msg):
+ """
+ Received Msg with started - our request was queued and is now started.
+ """
+ seqnr = msg['started']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=msg['from'], result="started")
+
+
diff --git a/slixmpp/plugins/xep_0323/stanza/__init__.py b/slixmpp/plugins/xep_0323/stanza/__init__.py
new file mode 100644
index 00000000..e1603e41
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/stanza/__init__.py
@@ -0,0 +1,12 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0323.stanza.sensordata import *
+
diff --git a/slixmpp/plugins/xep_0323/stanza/base.py b/slixmpp/plugins/xep_0323/stanza/base.py
new file mode 100644
index 00000000..7959b818
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/stanza/base.py
@@ -0,0 +1,13 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET
+
+pass
diff --git a/slixmpp/plugins/xep_0323/stanza/sensordata.py b/slixmpp/plugins/xep_0323/stanza/sensordata.py
new file mode 100644
index 00000000..aa223a62
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/stanza/sensordata.py
@@ -0,0 +1,792 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Iq, Message
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from re import match
+
+class Sensordata(ElementBase):
+ """ Placeholder for the namespace, not used as a stanza """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'sensordata'
+ plugin_attrib = name
+ interfaces = set(tuple())
+
+class FieldTypes():
+ """
+ All field types are optional booleans that default to False
+ """
+ field_types = set([ 'momentary','peak','status','computed','identity','historicalSecond','historicalMinute','historicalHour', \
+ 'historicalDay','historicalWeek','historicalMonth','historicalQuarter','historicalYear','historicalOther'])
+
+class FieldStatus():
+ """
+ All field statuses are optional booleans that default to False
+ """
+ field_status = set([ 'missing','automaticEstimate','manualEstimate','manualReadout','automaticReadout','timeOffset','warning','error', \
+ 'signed','invoiced','endOfSeries','powerFailure','invoiceConfirmed'])
+
+class Request(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'req'
+ plugin_attrib = name
+ interfaces = set(['seqnr','nodes','fields','serviceToken','deviceToken','userToken','from','to','when','historical','all'])
+ interfaces.update(FieldTypes.field_types)
+ _flags = set(['serviceToken','deviceToken','userToken','from','to','when','historical','all'])
+ _flags.update(FieldTypes.field_types)
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+ self._fields = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+ self._fields = set([field['name'] for field in self['fields']])
+
+ def _get_flags(self):
+ """
+ Helper function for getting of flags. Returns all flags in
+ dictionary format: { "flag name": "flag value" ... }
+ """
+ flags = {}
+ for f in self._flags:
+ if not self[f] == "":
+ flags[f] = self[f]
+ return flags
+
+ def _set_flags(self, flags):
+ """
+ Helper function for setting of flags.
+
+ Arguments:
+ flags -- Flags in dictionary format: { "flag name": "flag value" ... }
+ """
+ for f in self._flags:
+ if flags is not None and f in flags:
+ self[f] = flags[f]
+ else:
+ self[f] = None
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add((nodeId))
+ node = RequestNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, RequestNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, RequestNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+ def add_field(self, name):
+ """
+ Add a new field element. Each item is required to have a
+ name.
+
+ Arguments:
+ name -- The name of the field.
+ """
+ if name not in self._fields:
+ self._fields.add((name))
+ field = RequestField(parent=self)
+ field['name'] = name
+ self.iterables.append(field)
+ return field
+ return None
+
+ def del_field(self, name):
+ """
+ Remove a single field.
+
+ Arguments:
+ name -- name of field to remove.
+ """
+ if name in self._fields:
+ fields = [i for i in self.iterables if isinstance(i, RequestField)]
+ for field in fields:
+ if field['name'] == name:
+ self.xml.remove(field.xml)
+ self.iterables.remove(field)
+ return True
+ return False
+
+ def get_fields(self):
+ """Return all fields."""
+ fields = []
+ for field in self['substanzas']:
+ if isinstance(field, RequestField):
+ fields.append(field)
+ return fields
+
+ def set_fields(self, fields):
+ """
+ Set or replace all fields. The given fields must be in a
+ list or set where each item is RequestField or string
+
+ Arguments:
+ fields -- A series of fields in RequestField or string format.
+ """
+ self.del_fields()
+ for field in fields:
+ if isinstance(field, RequestField):
+ self.add_field(field['name'])
+ else:
+ self.add_field(field)
+
+ def del_fields(self):
+ """Remove all fields."""
+ self._fields = set()
+ fields = [i for i in self.iterables if isinstance(i, RequestField)]
+ for field in fields:
+ self.xml.remove(field.xml)
+ self.iterables.remove(field)
+
+
+class RequestNode(ElementBase):
+ """ Node element in a request """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'node'
+ plugin_attrib = name
+ interfaces = set(['nodeId','sourceId','cacheType'])
+
+class RequestField(ElementBase):
+ """ Field element in a request """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'field'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+class Accepted(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'accepted'
+ plugin_attrib = name
+ interfaces = set(['seqnr','queued'])
+
+class Started(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'started'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+class Failure(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'failure'
+ plugin_attrib = name
+ interfaces = set(['seqnr','done'])
+
+class Error(ElementBase):
+ """ Error element in a request failure """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'error'
+ plugin_attrib = name
+ interfaces = set(['nodeId','timestamp','sourceId','cacheType','text'])
+
+ def get_text(self):
+ """Return then contents inside the XML tag."""
+ return self.xml.text
+
+ def set_text(self, value):
+ """Set then contents inside the XML tag.
+
+ :param value: string
+ """
+
+ self.xml.text = value
+ return self
+
+ def del_text(self):
+ """Remove the contents inside the XML tag."""
+ self.xml.text = ""
+ return self
+
+class Rejected(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'rejected'
+ plugin_attrib = name
+ interfaces = set(['seqnr','error'])
+ sub_interfaces = set(['error'])
+
+class Fields(ElementBase):
+ """ Fields element, top level in a response message with data """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'fields'
+ plugin_attrib = name
+ interfaces = set(['seqnr','done','nodes'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None, substanzas=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add((nodeId))
+ node = FieldsNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ if substanzas is not None:
+ node.set_timestamps(substanzas)
+
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, FieldsNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, FieldsNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ #print(str(id(self)) + " set_nodes: got " + str(nodes))
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, FieldsNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'], substanzas=node['substanzas'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, FieldsNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+class FieldsNode(ElementBase):
+ """ Node element in response fields """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'node'
+ plugin_attrib = name
+ interfaces = set(['nodeId','sourceId','cacheType','timestamps'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._timestamps = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._timestamps = set([ts['value'] for ts in self['timestamps']])
+
+ def add_timestamp(self, timestamp, substanzas=None):
+ """
+ Add a new timestamp element.
+
+ Arguments:
+ timestamp -- The timestamp in ISO format.
+ """
+ #print(str(id(self)) + " add_timestamp: " + str(timestamp))
+
+ if timestamp not in self._timestamps:
+ self._timestamps.add((timestamp))
+ ts = Timestamp(parent=self)
+ ts['value'] = timestamp
+ if not substanzas is None:
+ ts.set_datas(substanzas)
+ #print("add_timestamp with substanzas: " + str(substanzas))
+ self.iterables.append(ts)
+ #print(str(id(self)) + " added_timestamp: " + str(id(ts)))
+ return ts
+ return None
+
+ def del_timestamp(self, timestamp):
+ """
+ Remove a single timestamp.
+
+ Arguments:
+ timestamp -- timestamp (in ISO format) of the item to remove.
+ """
+ #print("del_timestamp: ")
+ if timestamp in self._timestamps:
+ timestamps = [i for i in self.iterables if isinstance(i, Timestamp)]
+ for ts in timestamps:
+ if ts['value'] == timestamp:
+ self.xml.remove(ts.xml)
+ self.iterables.remove(ts)
+ return True
+ return False
+
+ def get_timestamps(self):
+ """Return all timestamps."""
+ #print(str(id(self)) + " get_timestamps: ")
+ timestamps = []
+ for timestamp in self['substanzas']:
+ if isinstance(timestamp, Timestamp):
+ timestamps.append(timestamp)
+ return timestamps
+
+ def set_timestamps(self, timestamps):
+ """
+ Set or replace all timestamps. The given timestamps must be in a
+ list or set where each item is a timestamp
+
+ Arguments:
+ timestamps -- A series of timestamps.
+ """
+ #print(str(id(self)) + " set_timestamps: got " + str(timestamps))
+ self.del_timestamps()
+ for timestamp in timestamps:
+ #print("set_timestamps: subset " + str(timestamp))
+ #print("set_timestamps: subset.substanzas " + str(timestamp['substanzas']))
+ if isinstance(timestamp, Timestamp):
+ self.add_timestamp(timestamp['value'], substanzas=timestamp['substanzas'])
+ else:
+ #print("set_timestamps: got " + str(timestamp))
+ self.add_timestamp(timestamp)
+
+ def del_timestamps(self):
+ """Remove all timestamps."""
+ #print(str(id(self)) + " del_timestamps: ")
+ self._timestamps = set()
+ timestamps = [i for i in self.iterables if isinstance(i, Timestamp)]
+ for timestamp in timestamps:
+ self.xml.remove(timestamp.xml)
+ self.iterables.remove(timestamp)
+
+class Field(ElementBase):
+ """
+ Field element in response Timestamp. This is a base class,
+ all instances of fields added to Timestamp must be of types:
+ DataNumeric
+ DataString
+ DataBoolean
+ DataDateTime
+ DataTimeSpan
+ DataEnum
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'field'
+ plugin_attrib = name
+ interfaces = set(['name','module','stringIds'])
+ interfaces.update(FieldTypes.field_types)
+ interfaces.update(FieldStatus.field_status)
+
+ _flags = set()
+ _flags.update(FieldTypes.field_types)
+ _flags.update(FieldStatus.field_status)
+
+ def set_stringIds(self, value):
+ """Verifies stringIds according to regexp from specification XMPP-0323.
+
+ :param value: string
+ """
+
+ pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
+ if pattern.match(value) is not None:
+ self.xml.stringIds = value
+ else:
+ # Bad content, add nothing
+ pass
+
+ return self
+
+ def _get_flags(self):
+ """
+ Helper function for getting of flags. Returns all flags in
+ dictionary format: { "flag name": "flag value" ... }
+ """
+ flags = {}
+ for f in self._flags:
+ if not self[f] == "":
+ flags[f] = self[f]
+ return flags
+
+ def _set_flags(self, flags):
+ """
+ Helper function for setting of flags.
+
+ Arguments:
+ flags -- Flags in dictionary format: { "flag name": "flag value" ... }
+ """
+ for f in self._flags:
+ if flags is not None and f in flags:
+ self[f] = flags[f]
+ else:
+ self[f] = None
+
+ def _get_typename(self):
+ return "invalid type, use subclasses!"
+
+
+class Timestamp(ElementBase):
+ """ Timestamp element in response Node """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'timestamp'
+ plugin_attrib = name
+ interfaces = set(['value','datas'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._datas = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._datas = set([data['name'] for data in self['datas']])
+
+ def add_data(self, typename, name, value, module=None, stringIds=None, unit=None, dataType=None, flags=None):
+ """
+ Add a new data element.
+
+ Arguments:
+ typename -- The type of data element (numeric, string, boolean, dateTime, timeSpan or enum)
+ value -- The value of the data element
+ module -- [optional] language module to use for the data element
+ stringIds -- [optional] The stringIds used to find associated text in the language module
+ unit -- [optional] The unit. Only applicable for type numeric
+ dataType -- [optional] The dataType. Only applicable for type enum
+ """
+ if name not in self._datas:
+ dataObj = None
+ if typename == "numeric":
+ dataObj = DataNumeric(parent=self)
+ dataObj['unit'] = unit
+ elif typename == "string":
+ dataObj = DataString(parent=self)
+ elif typename == "boolean":
+ dataObj = DataBoolean(parent=self)
+ elif typename == "dateTime":
+ dataObj = DataDateTime(parent=self)
+ elif typename == "timeSpan":
+ dataObj = DataTimeSpan(parent=self)
+ elif typename == "enum":
+ dataObj = DataEnum(parent=self)
+ dataObj['dataType'] = dataType
+
+ dataObj['name'] = name
+ dataObj['value'] = value
+ dataObj['module'] = module
+ dataObj['stringIds'] = stringIds
+
+ if flags is not None:
+ dataObj._set_flags(flags)
+
+ self._datas.add(name)
+ self.iterables.append(dataObj)
+ return dataObj
+ return None
+
+ def del_data(self, name):
+ """
+ Remove a single data element.
+
+ Arguments:
+ data_name -- The data element name to remove.
+ """
+ if name in self._datas:
+ datas = [i for i in self.iterables if isinstance(i, Field)]
+ for data in datas:
+ if data['name'] == name:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+ return True
+ return False
+
+ def get_datas(self):
+ """ Return all data elements. """
+ datas = []
+ for data in self['substanzas']:
+ if isinstance(data, Field):
+ datas.append(data)
+ return datas
+
+ def set_datas(self, datas):
+ """
+ Set or replace all data elements. The given elements must be in a
+ list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum)
+
+ Arguments:
+ datas -- A series of data elements.
+ """
+ self.del_datas()
+ for data in datas:
+ self.add_data(typename=data._get_typename(), name=data['name'], value=data['value'], module=data['module'], stringIds=data['stringIds'], unit=data['unit'], dataType=data['dataType'], flags=data._get_flags())
+
+ def del_datas(self):
+ """Remove all data elements."""
+ self._datas = set()
+ datas = [i for i in self.iterables if isinstance(i, Field)]
+ for data in datas:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+
+class DataNumeric(Field):
+ """
+ Field data of type numeric.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'numeric'
+ plugin_attrib = name
+ interfaces = set(['value', 'unit'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "numeric"
+
+class DataString(Field):
+ """
+ Field data of type string
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'string'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "string"
+
+class DataBoolean(Field):
+ """
+ Field data of type boolean.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'boolean'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "boolean"
+
+class DataDateTime(Field):
+ """
+ Field data of type dateTime.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'dateTime'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "dateTime"
+
+class DataTimeSpan(Field):
+ """
+ Field data of type timeSpan.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'timeSpan'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "timeSpan"
+
+class DataEnum(Field):
+ """
+ Field data of type enum.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'enum'
+ plugin_attrib = name
+ interfaces = set(['value', 'dataType'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "enum"
+
+class Done(ElementBase):
+ """ Done element used to signal that all data has been transferred """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'done'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+class Cancel(ElementBase):
+ """ Cancel element used to signal that a request shall be cancelled """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'cancel'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+class Cancelled(ElementBase):
+ """ Cancelled element used to signal that cancellation is confirmed """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'cancelled'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+
+register_stanza_plugin(Iq, Request)
+register_stanza_plugin(Request, RequestNode, iterable=True)
+register_stanza_plugin(Request, RequestField, iterable=True)
+
+register_stanza_plugin(Iq, Accepted)
+register_stanza_plugin(Message, Failure)
+register_stanza_plugin(Failure, Error)
+
+register_stanza_plugin(Iq, Rejected)
+
+register_stanza_plugin(Message, Fields)
+register_stanza_plugin(Fields, FieldsNode, iterable=True)
+register_stanza_plugin(FieldsNode, Timestamp, iterable=True)
+register_stanza_plugin(Timestamp, Field, iterable=True)
+register_stanza_plugin(Timestamp, DataNumeric, iterable=True)
+register_stanza_plugin(Timestamp, DataString, iterable=True)
+register_stanza_plugin(Timestamp, DataBoolean, iterable=True)
+register_stanza_plugin(Timestamp, DataDateTime, iterable=True)
+register_stanza_plugin(Timestamp, DataTimeSpan, iterable=True)
+register_stanza_plugin(Timestamp, DataEnum, iterable=True)
+
+register_stanza_plugin(Message, Started)
+
+register_stanza_plugin(Iq, Cancel)
+register_stanza_plugin(Iq, Cancelled)
diff --git a/slixmpp/plugins/xep_0323/timerreset.py b/slixmpp/plugins/xep_0323/timerreset.py
new file mode 100644
index 00000000..616380e7
--- /dev/null
+++ b/slixmpp/plugins/xep_0323/timerreset.py
@@ -0,0 +1,69 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+from threading import Thread, Event, Timer
+import time
+
+def TimerReset(*args, **kwargs):
+ """ Global function for Timer """
+ return _TimerReset(*args, **kwargs)
+
+
+class _TimerReset(Thread):
+ """Call a function after a specified number of seconds:
+
+ t = TimerReset(30.0, f, args=[], kwargs={})
+ t.start()
+ t.cancel() # stop the timer's action if it's still waiting
+ """
+
+ def __init__(self, interval, function, args=None, kwargs=None):
+ if not kwargs:
+ kwargs = {}
+ if not args:
+ args = []
+
+ Thread.__init__(self)
+ self.interval = interval
+ self.function = function
+ self.args = args
+ self.kwargs = kwargs
+ self.finished = Event()
+ self.resetted = True
+
+ def cancel(self):
+ """Stop the timer if it hasn't finished yet"""
+ self.finished.set()
+
+ def run(self):
+ #print "Time: %s - timer running..." % time.asctime()
+
+ while self.resetted:
+ #print "Time: %s - timer waiting for timeout in %.2f..." % (time.asctime(), self.interval)
+ self.resetted = False
+ self.finished.wait(self.interval)
+
+ if not self.finished.isSet():
+ self.function(*self.args, **self.kwargs)
+ self.finished.set()
+ #print "Time: %s - timer finished!" % time.asctime()
+
+ def reset(self, interval=None):
+ """ Reset the timer """
+
+ if interval:
+ #print "Time: %s - timer resetting to %.2f..." % (time.asctime(), interval)
+ self.interval = interval
+ else:
+ #print "Time: %s - timer resetting..." % time.asctime()
+ pass
+
+ self.resetted = True
+ self.finished.set()
+ self.finished.clear()
diff --git a/slixmpp/plugins/xep_0325/__init__.py b/slixmpp/plugins/xep_0325/__init__.py
new file mode 100644
index 00000000..ce8cb7ce
--- /dev/null
+++ b/slixmpp/plugins/xep_0325/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0325.control import XEP_0325
+from slixmpp.plugins.xep_0325 import stanza
+
+register_plugin(XEP_0325)
+
+xep_0325=XEP_0325
diff --git a/slixmpp/plugins/xep_0325/control.py b/slixmpp/plugins/xep_0325/control.py
new file mode 100644
index 00000000..9a493b02
--- /dev/null
+++ b/slixmpp/plugins/xep_0325/control.py
@@ -0,0 +1,548 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import time
+
+from slixmpp import asyncio
+from functools import partial
+from slixmpp.xmlstream import JID
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0325 import stanza
+from slixmpp.plugins.xep_0325.stanza import Control
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0325(BasePlugin):
+
+ """
+ XEP-0325: IoT Control
+
+
+ Actuators are devices in sensor networks that can be controlled through
+ the network and act with the outside world. In sensor networks and
+ Internet of Things applications, actuators make it possible to automate
+ real-world processes.
+ This plugin implements a mechanism whereby actuators can be controlled
+ in XMPP-based sensor networks, making it possible to integrate sensors
+ and actuators of different brands, makes and models into larger
+ Internet of Things applications.
+
+ Also see <http://xmpp.org/extensions/xep-0325.html>
+
+ Events:
+ Sensor side
+ -----------
+ Control Event:DirectSet -- Received a control message
+ Control Event:SetReq -- Received a control request
+
+ Client side
+ -----------
+ Control Event:SetResponse -- Received a response to a
+ control request, type result
+ Control Event:SetResponseError -- Received a response to a
+ control request, type error
+
+ Attributes:
+ sessions -- A dictionary or equivalent backend mapping
+ session IDs to dictionaries containing data
+ relevant to a request's session. This dictionary is used
+ both by the client and sensor side. On client side, seqnr
+ is used as key, while on sensor side, a session_id is used
+ as key. This ensures that the two will not collide, so
+ one instance can be both client and sensor.
+ Sensor side
+ -----------
+ nodes -- A dictionary mapping sensor nodes that are serviced through
+ this XMPP instance to their device handlers ("drivers").
+ Client side
+ -----------
+ last_seqnr -- The last used sequence number (integer). One sequence of
+ communication (e.g. -->request, <--accept, <--fields)
+ between client and sensor is identified by a unique
+ sequence number (unique between the client/sensor pair)
+
+ Methods:
+ plugin_init -- Overrides BasePlugin.plugin_init
+ post_init -- Overrides BasePlugin.post_init
+ plugin_end -- Overrides BasePlugin.plugin_end
+
+ Sensor side
+ -----------
+ register_node -- Register a sensor as available from this XMPP
+ instance.
+
+ Client side
+ -----------
+ set_request -- Initiates a control request to modify data in
+ sensor(s). Non-blocking, a callback function will
+ be called when the sensor has responded.
+ set_command -- Initiates a control command to modify data in
+ sensor(s). Non-blocking. The sensor(s) will not
+ respond regardless of the result of the command,
+ so no callback is made.
+
+ """
+
+ name = 'xep_0325'
+ description = 'XEP-0325 Internet of Things - Control'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+
+ default_config = {
+# 'session_db': None
+ }
+
+ def plugin_init(self):
+ """ Start the XEP-0325 plugin """
+
+ self.xmpp.register_handler(
+ Callback('Control Event:DirectSet',
+ StanzaPath('message/set'),
+ self._handle_direct_set))
+
+ self.xmpp.register_handler(
+ Callback('Control Event:SetReq',
+ StanzaPath('iq@type=set/set'),
+ self._handle_set_req))
+
+ self.xmpp.register_handler(
+ Callback('Control Event:SetResponse',
+ StanzaPath('iq@type=result/setResponse'),
+ self._handle_set_response))
+
+ self.xmpp.register_handler(
+ Callback('Control Event:SetResponseError',
+ StanzaPath('iq@type=error/setResponse'),
+ self._handle_set_response))
+
+ # Server side dicts
+ self.nodes = {}
+ self.sessions = {}
+
+ self.last_seqnr = 0
+
+ ## For testning only
+ self.test_authenticated_from = ""
+
+ def post_init(self):
+ """ Init complete. Register our features in Service discovery. """
+ BasePlugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Control.namespace)
+ self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple())
+
+ def _new_session(self):
+ """ Return a new session ID. """
+ return str(time.time()) + '-' + self.xmpp.new_id()
+
+ def plugin_end(self):
+ """ Stop the XEP-0325 plugin """
+ self.sessions.clear()
+ self.xmpp.remove_handler('Control Event:DirectSet')
+ self.xmpp.remove_handler('Control Event:SetReq')
+ self.xmpp.remove_handler('Control Event:SetResponse')
+ self.xmpp.remove_handler('Control Event:SetResponseError')
+ self.xmpp['xep_0030'].del_feature(feature=Control.namespace)
+ self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple())
+
+
+ # =================================================================
+ # Sensor side (data provider) API
+
+ def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None):
+ """
+ Register a sensor/device as available for control requests/commands
+ through this XMPP instance.
+
+ The device object may by any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_control_field
+ set_control_fields
+ according to the interfaces shown in the example device.py file.
+
+ Arguments:
+ nodeId -- The identifier for the device
+ device -- The device object
+ commTimeout -- Time in seconds to wait between each callback from device during
+ a data readout. Float.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ self.nodes[nodeId] = {"device": device,
+ "commTimeout": commTimeout,
+ "sourceId": sourceId,
+ "cacheType": cacheType}
+
+ def _set_authenticated(self, auth=''):
+ """ Internal testing function """
+ self.test_authenticated_from = auth
+
+ def _get_new_seqnr(self):
+ """ Returns a unique sequence number (unique across threads) """
+ self.last_seqnr = self.last_seqnr + 1
+ return str(self.last_seqnr)
+
+ def _handle_set_req(self, iq):
+ """
+ Event handler for reception of an Iq with set req - this is a
+ control request.
+
+ Verifies that
+ - all the requested nodes are available
+ (if no nodes are specified in the request, assume all nodes)
+ - all the control fields are available from all requested nodes
+ (if no nodes are specified in the request, assume all nodes)
+
+ If the request passes verification, the control request is passed
+ to the devices (in a separate thread).
+ If the verification fails, a setResponse with error indication
+ is sent.
+ """
+
+ error_msg = ''
+ req_ok = True
+ missing_node = None
+ missing_field = None
+
+ # Authentication
+ if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from:
+ # Invalid authentication
+ req_ok = False
+ error_msg = "Access denied"
+
+ # Nodes
+ if len(iq['set']['nodes']) > 0:
+ for n in iq['set']['nodes']:
+ if not n['nodeId'] in self.nodes:
+ req_ok = False
+ missing_node = n['nodeId']
+ error_msg = "Invalid nodeId " + n['nodeId']
+ process_nodes = [n['nodeId'] for n in iq['set']['nodes']]
+ else:
+ process_nodes = self.nodes.keys()
+
+ # Fields - for control we need to find all in all devices, otherwise we reject
+ process_fields = []
+ if len(iq['set']['datas']) > 0:
+ for f in iq['set']['datas']:
+ for node in self.nodes:
+ if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()):
+ req_ok = False
+ missing_field = f['name']
+ error_msg = "Invalid field " + f['name']
+ break
+ process_fields = [(f['name'], f._get_typename(), f['value']) for f in iq['set']['datas']]
+
+ if req_ok:
+ session = self._new_session()
+ self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": iq['id']}
+ self.sessions[session]["commTimers"] = {}
+ self.sessions[session]["nodeDone"] = {}
+ # Flag that a reply is exected when we are done
+ self.sessions[session]["reply"] = True
+
+ self.sessions[session]["node_list"] = process_nodes
+ self._node_request(session, process_fields)
+ else:
+ iq = iq.reply()
+ iq['type'] = 'error'
+ iq['setResponse']['responseCode'] = "NotFound"
+ if missing_node is not None:
+ iq['setResponse'].add_node(missing_node)
+ if missing_field is not None:
+ iq['setResponse'].add_data(missing_field)
+ iq['setResponse']['error']['var'] = "Output"
+ iq['setResponse']['error']['text'] = error_msg
+ iq.send()
+
+ def _handle_direct_set(self, msg):
+ """
+ Event handler for reception of a Message with set command - this is a
+ direct control command.
+
+ Verifies that
+ - all the requested nodes are available
+ (if no nodes are specified in the request, assume all nodes)
+ - all the control fields are available from all requested nodes
+ (if no nodes are specified in the request, assume all nodes)
+
+ If the request passes verification, the control request is passed
+ to the devices (in a separate thread).
+ If the verification fails, do nothing.
+ """
+ req_ok = True
+
+ # Nodes
+ if len(msg['set']['nodes']) > 0:
+ for n in msg['set']['nodes']:
+ if not n['nodeId'] in self.nodes:
+ req_ok = False
+ error_msg = "Invalid nodeId " + n['nodeId']
+ process_nodes = [n['nodeId'] for n in msg['set']['nodes']]
+ else:
+ process_nodes = self.nodes.keys()
+
+ # Fields - for control we need to find all in all devices, otherwise we reject
+ process_fields = []
+ if len(msg['set']['datas']) > 0:
+ for f in msg['set']['datas']:
+ for node in self.nodes:
+ if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()):
+ req_ok = False
+ missing_field = f['name']
+ error_msg = "Invalid field " + f['name']
+ break
+ process_fields = [(f['name'], f._get_typename(), f['value']) for f in msg['set']['datas']]
+
+ if req_ok:
+ session = self._new_session()
+ self.sessions[session] = {"from": msg['from'], "to": msg['to']}
+ self.sessions[session]["commTimers"] = {}
+ self.sessions[session]["nodeDone"] = {}
+ self.sessions[session]["reply"] = False
+
+ self.sessions[session]["node_list"] = process_nodes
+ self._node_request(session, process_fields)
+
+
+ def _node_request(self, session, process_fields):
+ """
+ Helper function to handle the device control in a separate thread.
+
+ Arguments:
+ session -- The request session id
+ process_fields -- The fields to set in the devices. List of tuple format:
+ (name, datatype, value)
+ """
+ for node in self.sessions[session]["node_list"]:
+ self.sessions[session]["nodeDone"][node] = False
+
+ for node in self.sessions[session]["node_list"]:
+ timer = self.xmpp.loop.call_later(self.nodes[node]['commTimeout'], partial(self._event_comm_timeout, args=(session, node)))
+ self.sessions[session]["commTimers"][node] = timer
+ self.nodes[node]['device'].set_control_fields(process_fields, session=session, callback=self._device_set_command_callback)
+
+ def _event_comm_timeout(self, session, nodeId):
+ """
+ Triggered if any of the control operations timeout.
+ Stop communicating with the failing device.
+ If the control command was an Iq request, sends a failure
+ message back to the client.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The id of the device which timed out
+ """
+
+ if self.sessions[session]["reply"]:
+ # Reply is exected when we are done
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[session]['to']
+ iq['to'] = self.sessions[session]['from']
+ iq['type'] = "error"
+ iq['id'] = self.sessions[session]['seqnr']
+ iq['setResponse']['responseCode'] = "OtherError"
+ iq['setResponse'].add_node(nodeId)
+ iq['setResponse']['error']['var'] = "Output"
+ iq['setResponse']['error']['text'] = "Timeout."
+ iq.send()
+
+ ## TODO - should we send one timeout per node??
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ # The session is complete, delete it
+ del self.sessions[session]
+
+ def _all_nodes_done(self, session):
+ """
+ Checks wheter all devices are done replying to the control command.
+
+ Arguments:
+ session -- The request session id
+ """
+ for n in self.sessions[session]["nodeDone"]:
+ if not self.sessions[session]["nodeDone"][n]:
+ return False
+ return True
+
+ def _device_set_command_callback(self, session, nodeId, result, error_field=None, error_msg=None):
+ """
+ Callback function called by the devices when the control command is
+ complete or failed.
+ If needed, composes a message with the result and sends it back to the
+ client.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The device id which initiated the callback
+ result -- The current result status of the control command. Valid values are:
+ "error" - Set fields failed.
+ "ok" - All fields were set.
+ error_field -- [optional] Only applies when result == "error"
+ The field name that failed (usually means it is missing)
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+ """
+
+ if not session in self.sessions:
+ # This can happend if a session was deleted, like in a timeout. Just drop the data.
+ return
+
+ if result == "error":
+ self.sessions[session]["commTimers"][nodeId].cancel()
+
+ if self.sessions[session]["reply"]:
+ # Reply is exected when we are done
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[session]['to']
+ iq['to'] = self.sessions[session]['from']
+ iq['type'] = "error"
+ iq['id'] = self.sessions[session]['seqnr']
+ iq['setResponse']['responseCode'] = "OtherError"
+ iq['setResponse'].add_node(nodeId)
+ if error_field is not None:
+ iq['setResponse'].add_data(error_field)
+ iq['setResponse']['error']['var'] = error_field
+ iq['setResponse']['error']['text'] = error_msg
+ iq.send()
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ # The session is complete, delete it
+ del self.sessions[session]
+ else:
+ self.sessions[session]["commTimers"][nodeId].cancel()
+
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ if self.sessions[session]["reply"]:
+ # Reply is exected when we are done
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[session]['to']
+ iq['to'] = self.sessions[session]['from']
+ iq['type'] = "result"
+ iq['id'] = self.sessions[session]['seqnr']
+ iq['setResponse']['responseCode'] = "OK"
+ iq.send()
+
+ # The session is complete, delete it
+ del self.sessions[session]
+
+
+ # =================================================================
+ # Client side (data controller) API
+
+ def set_request(self, from_jid, to_jid, callback, fields, nodeIds=None):
+ """
+ Called on the client side to initiade a control request.
+ Composes a message with the request and sends it to the device(s).
+ Does not block, the callback will be called when the device(s)
+ has responded.
+
+ Arguments:
+ from_jid -- The jid of the requester
+ to_jid -- The jid of the device(s)
+ callback -- The callback function to call when data is availble.
+
+ The callback function must support the following arguments:
+
+ from_jid -- The jid of the responding device(s)
+ result -- The result of the control request. Valid values are:
+ "OK" - Control request completed successfully
+ "NotFound" - One or more nodes or fields are missing
+ "InsufficientPrivileges" - Not authorized.
+ "Locked" - Field(s) is locked and cannot
+ be changed at the moment.
+ "NotImplemented" - Request feature not implemented.
+ "FormError" - Error while setting with
+ a form (not implemented).
+ "OtherError" - Indicates other types of
+ errors, such as timeout.
+ Details in the error_msg.
+
+
+ nodeId -- [optional] Only applicable when result == "error"
+ List of node Ids of failing device(s).
+
+ fields -- [optional] Only applicable when result == "error"
+ List of fields that failed.[optional] Mandatory when result == "rejected" or "failure".
+
+ error_msg -- Details about why the request failed.
+
+ fields -- Fields to set. List of tuple format: (name, typename, value).
+ nodeIds -- [optional] Limits the request to the node Ids in this list.
+ """
+ iq = self.xmpp.Iq()
+ iq['from'] = from_jid
+ iq['to'] = to_jid
+ seqnr = self._get_new_seqnr()
+ iq['id'] = seqnr
+ iq['type'] = "set"
+ if nodeIds is not None:
+ for nodeId in nodeIds:
+ iq['set'].add_node(nodeId)
+ if fields is not None:
+ for name, typename, value in fields:
+ iq['set'].add_data(name=name, typename=typename, value=value)
+
+ self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "callback": callback}
+ iq.send()
+
+ def set_command(self, from_jid, to_jid, fields, nodeIds=None):
+ """
+ Called on the client side to initiade a control command.
+ Composes a message with the set commandand sends it to the device(s).
+ Does not block. Device(s) will not respond, regardless of result.
+
+ Arguments:
+ from_jid -- The jid of the requester
+ to_jid -- The jid of the device(s)
+
+ fields -- Fields to set. List of tuple format: (name, typename, value).
+ nodeIds -- [optional] Limits the request to the node Ids in this list.
+ """
+ msg = self.xmpp.Message()
+ msg['from'] = from_jid
+ msg['to'] = to_jid
+ msg['type'] = "set"
+ if nodeIds is not None:
+ for nodeId in nodeIds:
+ msg['set'].add_node(nodeId)
+ if fields is not None:
+ for name, typename, value in fields:
+ msg['set'].add_data(name, typename, value)
+
+ # We won't get any reply, so don't create a session
+ msg.send()
+
+ def _handle_set_response(self, iq):
+ """ Received response from device(s) """
+ #print("ooh")
+ seqnr = iq['id']
+ from_jid = str(iq['from'])
+ result = iq['setResponse']['responseCode']
+ nodeIds = [n['name'] for n in iq['setResponse']['nodes']]
+ fields = [f['name'] for f in iq['setResponse']['datas']]
+ error_msg = None
+
+ if not iq['setResponse'].find('error') is None and not iq['setResponse']['error']['text'] == "":
+ error_msg = iq['setResponse']['error']['text']
+
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=from_jid, result=result, nodeIds=nodeIds, fields=fields, error_msg=error_msg)
+
diff --git a/slixmpp/plugins/xep_0325/device.py b/slixmpp/plugins/xep_0325/device.py
new file mode 100644
index 00000000..05275088
--- /dev/null
+++ b/slixmpp/plugins/xep_0325/device.py
@@ -0,0 +1,125 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime
+
+class Device(object):
+ """
+ Example implementation of a device control object.
+
+ The device object may by any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_control_field
+ set_control_fields
+ """
+
+ def __init__(self, nodeId):
+ self.nodeId = nodeId
+ self.control_fields = {}
+
+ def has_control_field(self, field, typename):
+ """
+ Returns true if the supplied field name exists
+ and the type matches for control in this device.
+
+ Arguments:
+ field -- The field name
+ typename -- The expected type
+ """
+ if field in self.control_fields and self.control_fields[field]["type"] == typename:
+ return True
+ return False
+
+ def set_control_fields(self, fields, session, callback):
+ """
+ Starts a control setting procedure. Verifies the fields,
+ sets the data and (if needed) and calls the callback.
+
+ Arguments:
+ fields -- List of control fields in tuple format:
+ (name, typename, value)
+ session -- Session id, only used in the callback as identifier
+ callback -- Callback function to call when control set is complete.
+
+ The callback function must support the following arguments:
+
+ session -- Session id, as supplied in the
+ request_fields call
+ nodeId -- Identifier for this device
+ result -- The current result status of the readout.
+ Valid values are:
+ "error" - Set fields failed.
+ "ok" - All fields were set.
+ error_field -- [optional] Only applies when result == "error"
+ The field name that failed
+ (usually means it is missing)
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+ """
+
+ if len(fields) > 0:
+ # Check availiability
+ for name, typename, value in fields:
+ if not self.has_control_field(name, typename):
+ self._send_control_reject(session, name, "NotFound", callback)
+ return False
+
+ for name, typename, value in fields:
+ self._set_field_value(name, value)
+
+ callback(session, result="ok", nodeId=self.nodeId)
+ return True
+
+ def _send_control_reject(self, session, field, message, callback):
+ """
+ Sends a reject to the caller
+
+ Arguments:
+ session -- Session id, see definition in
+ set_control_fields function
+ callback -- Callback function, see definition in
+ set_control_fields function
+ """
+ callback(session, result="error", nodeId=self.nodeId, error_field=field, error_msg=message)
+
+ def _add_control_field(self, name, typename, value):
+ """
+ Adds a control field to the device
+
+ Arguments:
+ name -- Name of the field
+ typename -- Type of the field, one of:
+ (boolean, color, string, date, dateTime,
+ double, duration, int, long, time)
+ value -- Field value
+ """
+ self.control_fields[name] = {"type": typename, "value": value}
+
+ def _set_field_value(self, name, value):
+ """
+ Set the value of a control field
+
+ Arguments:
+ name -- Name of the field
+ value -- New value for the field
+ """
+ if name in self.control_fields:
+ self.control_fields[name]["value"] = value
+
+ def _get_field_value(self, name):
+ """
+ Get the value of a control field. Only used for unit testing.
+
+ Arguments:
+ name -- Name of the field
+ """
+ if name in self.control_fields:
+ return self.control_fields[name]["value"]
+ return None
diff --git a/slixmpp/plugins/xep_0325/stanza/__init__.py b/slixmpp/plugins/xep_0325/stanza/__init__.py
new file mode 100644
index 00000000..1466db5d
--- /dev/null
+++ b/slixmpp/plugins/xep_0325/stanza/__init__.py
@@ -0,0 +1,12 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0325.stanza.control import *
+
diff --git a/slixmpp/plugins/xep_0325/stanza/base.py b/slixmpp/plugins/xep_0325/stanza/base.py
new file mode 100644
index 00000000..7959b818
--- /dev/null
+++ b/slixmpp/plugins/xep_0325/stanza/base.py
@@ -0,0 +1,13 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET
+
+pass
diff --git a/slixmpp/plugins/xep_0325/stanza/control.py b/slixmpp/plugins/xep_0325/stanza/control.py
new file mode 100644
index 00000000..c47f3a4e
--- /dev/null
+++ b/slixmpp/plugins/xep_0325/stanza/control.py
@@ -0,0 +1,526 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import Iq, Message
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from re import match
+
+class Control(ElementBase):
+ """ Placeholder for the namespace, not used as a stanza """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'control'
+ plugin_attrib = name
+ interfaces = set(tuple())
+
+class ControlSet(ElementBase):
+ namespace = 'urn:xmpp:iot:control'
+ name = 'set'
+ plugin_attrib = name
+ interfaces = set(['nodes','datas'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+ self._datas = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+ self._datas = set([data['name'] for data in self['datas']])
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add((nodeId))
+ node = RequestNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, RequestNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, RequestNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+ def add_data(self, name, typename, value):
+ """
+ Add a new data element.
+
+ Arguments:
+ name -- The name of the data element
+ typename -- The type of data element
+ (boolean, color, string, date, dateTime,
+ double, duration, int, long, time)
+ value -- The value of the data element
+ """
+ if name not in self._datas:
+ dataObj = None
+ if typename == "boolean":
+ dataObj = BooleanParameter(parent=self)
+ elif typename == "color":
+ dataObj = ColorParameter(parent=self)
+ elif typename == "string":
+ dataObj = StringParameter(parent=self)
+ elif typename == "date":
+ dataObj = DateParameter(parent=self)
+ elif typename == "dateTime":
+ dataObj = DateTimeParameter(parent=self)
+ elif typename == "double":
+ dataObj = DoubleParameter(parent=self)
+ elif typename == "duration":
+ dataObj = DurationParameter(parent=self)
+ elif typename == "int":
+ dataObj = IntParameter(parent=self)
+ elif typename == "long":
+ dataObj = LongParameter(parent=self)
+ elif typename == "time":
+ dataObj = TimeParameter(parent=self)
+
+ dataObj['name'] = name
+ dataObj['value'] = value
+
+ self._datas.add(name)
+ self.iterables.append(dataObj)
+ return dataObj
+ return None
+
+ def del_data(self, name):
+ """
+ Remove a single data element.
+
+ Arguments:
+ data_name -- The data element name to remove.
+ """
+ if name in self._datas:
+ datas = [i for i in self.iterables if isinstance(i, BaseParameter)]
+ for data in datas:
+ if data['name'] == name:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+ return True
+ return False
+
+ def get_datas(self):
+ """ Return all data elements. """
+ datas = []
+ for data in self['substanzas']:
+ if isinstance(data, BaseParameter):
+ datas.append(data)
+ return datas
+
+ def set_datas(self, datas):
+ """
+ Set or replace all data elements. The given elements must be in a
+ list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum)
+
+ Arguments:
+ datas -- A series of data elements.
+ """
+ self.del_datas()
+ for data in datas:
+ self.add_data(name=data['name'], typename=data._get_typename(), value=data['value'])
+
+ def del_datas(self):
+ """Remove all data elements."""
+ self._datas = set()
+ datas = [i for i in self.iterables if isinstance(i, BaseParameter)]
+ for data in datas:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+
+
+class RequestNode(ElementBase):
+ """ Node element in a request """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'node'
+ plugin_attrib = name
+ interfaces = set(['nodeId','sourceId','cacheType'])
+
+
+class ControlSetResponse(ElementBase):
+ namespace = 'urn:xmpp:iot:control'
+ name = 'setResponse'
+ plugin_attrib = name
+ interfaces = set(['responseCode'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+ self._datas = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+ self._datas = set([data['name'] for data in self['datas']])
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add(nodeId)
+ node = RequestNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, RequestNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, RequestNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+ def add_data(self, name):
+ """
+ Add a new ResponseParameter element.
+
+ Arguments:
+ name -- Name of the parameter
+ """
+ if name not in self._datas:
+ self._datas.add(name)
+ data = ResponseParameter(parent=self)
+ data['name'] = name
+ self.iterables.append(data)
+ return data
+ return None
+
+ def del_data(self, name):
+ """
+ Remove a single ResponseParameter element.
+
+ Arguments:
+ name -- The data element name to remove.
+ """
+ if name in self._datas:
+ datas = [i for i in self.iterables if isinstance(i, ResponseParameter)]
+ for data in datas:
+ if data['name'] == name:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+ return True
+ return False
+
+ def get_datas(self):
+ """ Return all ResponseParameter elements. """
+ datas = set()
+ for data in self['substanzas']:
+ if isinstance(data, ResponseParameter):
+ datas.add(data)
+ return datas
+
+ def set_datas(self, datas):
+ """
+ Set or replace all data elements. The given elements must be in a
+ list or set of ResponseParameter elements
+
+ Arguments:
+ datas -- A series of data element names.
+ """
+ self.del_datas()
+ for data in datas:
+ self.add_data(name=data['name'])
+
+ def del_datas(self):
+ """Remove all ResponseParameter elements."""
+ self._datas = set()
+ datas = [i for i in self.iterables if isinstance(i, ResponseParameter)]
+ for data in datas:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+
+
+class Error(ElementBase):
+ namespace = 'urn:xmpp:iot:control'
+ name = 'error'
+ plugin_attrib = name
+ interfaces = set(['var','text'])
+
+ def get_text(self):
+ """Return then contents inside the XML tag."""
+ return self.xml.text
+
+ def set_text(self, value):
+ """Set then contents inside the XML tag.
+
+ Arguments:
+ value -- string
+ """
+
+ self.xml.text = value
+ return self
+
+ def del_text(self):
+ """Remove the contents inside the XML tag."""
+ self.xml.text = ""
+ return self
+
+class ResponseParameter(ElementBase):
+ """
+ Parameter element in ControlSetResponse.
+ """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'parameter'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class BaseParameter(ElementBase):
+ """
+ Parameter element in SetCommand. This is a base class,
+ all instances of parameters added to SetCommand must be of types:
+ BooleanParameter
+ ColorParameter
+ StringParameter
+ DateParameter
+ DateTimeParameter
+ DoubleParameter
+ DurationParameter
+ IntParameter
+ LongParameter
+ TimeParameter
+ """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'baseParameter'
+ plugin_attrib = name
+ interfaces = set(['name','value'])
+
+ def _get_typename(self):
+ return self.name
+
+class BooleanParameter(BaseParameter):
+ """
+ Field data of type boolean.
+ Note that the value is expressed as a string.
+ """
+ name = 'boolean'
+ plugin_attrib = name
+
+class ColorParameter(BaseParameter):
+ """
+ Field data of type color.
+ Note that the value is expressed as a string.
+ """
+ name = 'color'
+ plugin_attrib = name
+
+class StringParameter(BaseParameter):
+ """
+ Field data of type string.
+ """
+ name = 'string'
+ plugin_attrib = name
+
+class DateParameter(BaseParameter):
+ """
+ Field data of type date.
+ Note that the value is expressed as a string.
+ """
+ name = 'date'
+ plugin_attrib = name
+
+class DateTimeParameter(BaseParameter):
+ """
+ Field data of type dateTime.
+ Note that the value is expressed as a string.
+ """
+ name = 'dateTime'
+ plugin_attrib = name
+
+class DoubleParameter(BaseParameter):
+ """
+ Field data of type double.
+ Note that the value is expressed as a string.
+ """
+ name = 'double'
+ plugin_attrib = name
+
+class DurationParameter(BaseParameter):
+ """
+ Field data of type duration.
+ Note that the value is expressed as a string.
+ """
+ name = 'duration'
+ plugin_attrib = name
+
+class IntParameter(BaseParameter):
+ """
+ Field data of type int.
+ Note that the value is expressed as a string.
+ """
+ name = 'int'
+ plugin_attrib = name
+
+class LongParameter(BaseParameter):
+ """
+ Field data of type long (64-bit int).
+ Note that the value is expressed as a string.
+ """
+ name = 'long'
+ plugin_attrib = name
+
+class TimeParameter(BaseParameter):
+ """
+ Field data of type time.
+ Note that the value is expressed as a string.
+ """
+ name = 'time'
+ plugin_attrib = name
+
+register_stanza_plugin(Iq, ControlSet)
+register_stanza_plugin(Message, ControlSet)
+
+register_stanza_plugin(ControlSet, RequestNode, iterable=True)
+
+register_stanza_plugin(ControlSet, BooleanParameter, iterable=True)
+register_stanza_plugin(ControlSet, ColorParameter, iterable=True)
+register_stanza_plugin(ControlSet, StringParameter, iterable=True)
+register_stanza_plugin(ControlSet, DateParameter, iterable=True)
+register_stanza_plugin(ControlSet, DateTimeParameter, iterable=True)
+register_stanza_plugin(ControlSet, DoubleParameter, iterable=True)
+register_stanza_plugin(ControlSet, DurationParameter, iterable=True)
+register_stanza_plugin(ControlSet, IntParameter, iterable=True)
+register_stanza_plugin(ControlSet, LongParameter, iterable=True)
+register_stanza_plugin(ControlSet, TimeParameter, iterable=True)
+
+register_stanza_plugin(Iq, ControlSetResponse)
+register_stanza_plugin(ControlSetResponse, Error)
+register_stanza_plugin(ControlSetResponse, RequestNode, iterable=True)
+register_stanza_plugin(ControlSetResponse, ResponseParameter, iterable=True)
+
diff --git a/slixmpp/plugins/xep_0332/__init__.py b/slixmpp/plugins/xep_0332/__init__.py
new file mode 100644
index 00000000..8bf6b369
--- /dev/null
+++ b/slixmpp/plugins/xep_0332/__init__.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of HTTP over XMPP transport
+ http://xmpp.org/extensions/xep-0332.html
+ Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0332 import stanza
+from slixmpp.plugins.xep_0332.http import XEP_0332
+
+
+register_plugin(XEP_0332)
diff --git a/slixmpp/plugins/xep_0332/http.py b/slixmpp/plugins/xep_0332/http.py
new file mode 100644
index 00000000..7ad14dc8
--- /dev/null
+++ b/slixmpp/plugins/xep_0332/http.py
@@ -0,0 +1,159 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of HTTP over XMPP transport
+ http://xmpp.org/extensions/xep-0332.html
+ Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp import Iq
+
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+
+from slixmpp.plugins.base import BasePlugin
+from slixmpp.plugins.xep_0332.stanza import (
+ HTTPRequest, HTTPResponse, HTTPData
+)
+from slixmpp.plugins.xep_0131.stanza import Headers
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0332(BasePlugin):
+ """
+ XEP-0332: HTTP over XMPP transport
+ """
+
+ name = 'xep_0332'
+ description = 'XEP-0332: HTTP over XMPP transport'
+
+ #: xep_0047 not included.
+ #: xep_0001, 0137 and 0166 are missing
+ dependencies = set(['xep_0030', 'xep_0131'])
+
+ #: TODO: Do we really need to mention the supported_headers?!
+ default_config = {
+ 'supported_headers': set([
+ 'Content-Length', 'Transfer-Encoding', 'DateTime',
+ 'Accept-Charset', 'Location', 'Content-ID', 'Description',
+ 'Content-Language', 'Content-Transfer-Encoding', 'Timestamp',
+ 'Expires', 'User-Agent', 'Host', 'Proxy-Authorization', 'Date',
+ 'WWW-Authenticate', 'Accept-Encoding', 'Server', 'Error-Info',
+ 'Identifier', 'Content-Location', 'Content-Encoding', 'Distribute',
+ 'Accept', 'Proxy-Authenticate', 'ETag', 'Expect', 'Content-Type'
+ ])
+ }
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback(
+ 'HTTP Request',
+ StanzaPath('iq/http-req'),
+ self._handle_request
+ )
+ )
+ self.xmpp.register_handler(
+ Callback(
+ 'HTTP Response',
+ StanzaPath('iq/http-resp'),
+ self._handle_response
+ )
+ )
+ register_stanza_plugin(Iq, HTTPRequest, iterable=True)
+ register_stanza_plugin(Iq, HTTPResponse, iterable=True)
+ register_stanza_plugin(HTTPRequest, Headers, iterable=True)
+ register_stanza_plugin(HTTPRequest, HTTPData, iterable=True)
+ register_stanza_plugin(HTTPResponse, Headers, iterable=True)
+ register_stanza_plugin(HTTPResponse, HTTPData, iterable=True)
+ # TODO: Should we register any api's here? self.api.register()
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('HTTP Request')
+ self.xmpp.remove_handler('HTTP Response')
+ self.xmpp['xep_0030'].del_feature('urn:xmpp:http')
+ for header in self.supported_headers:
+ self.xmpp['xep_0030'].del_feature(
+ feature='%s#%s' % (Headers.namespace, header)
+ )
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:http')
+ for header in self.supported_headers:
+ self.xmpp['xep_0030'].add_feature(
+ '%s#%s' % (Headers.namespace, header)
+ )
+ # TODO: Do we need to add the supported headers to xep_0131?
+ # self.xmpp['xep_0131'].supported_headers.add(header)
+
+ def _handle_request(self, iq):
+ self.xmpp.event('http_request', iq)
+
+ def _handle_response(self, iq):
+ self.xmpp.event('http_response', iq)
+
+ def send_request(self, to=None, method=None, resource=None, headers=None,
+ data=None, **kwargs):
+ iq = self.xmpp.Iq()
+ iq['from'] = self.xmpp.boundjid
+ iq['to'] = to
+ iq['type'] = 'set'
+ iq['http-req']['headers'] = headers
+ iq['http-req']['method'] = method
+ iq['http-req']['resource'] = resource
+ iq['http-req']['version'] = '1.1' # TODO: set this implicitly
+ if 'id' in kwargs:
+ iq['id'] = kwargs["id"]
+ if data is not None:
+ iq['http-req']['data'] = data
+ return iq.send(
+ timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None)
+ )
+
+ def send_response(self, to=None, code=None, message=None, headers=None,
+ data=None, **kwargs):
+ iq = self.xmpp.Iq()
+ iq['from'] = self.xmpp.boundjid
+ iq['to'] = to
+ iq['type'] = 'result'
+ iq['http-resp']['headers'] = headers
+ iq['http-resp']['code'] = code
+ iq['http-resp']['message'] = message
+ iq['http-resp']['version'] = '1.1' # TODO: set this implicitly
+ if 'id' in kwargs:
+ iq['id'] = kwargs["id"]
+ if data is not None:
+ iq['http-resp']['data'] = data
+ return iq.send(
+ timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None)
+ )
+
+ def send_error(self, to=None, ecode='500', etype='wait',
+ econd='internal-server-error', **kwargs):
+ iq = self.xmpp.Iq()
+ iq['from'] = self.xmpp.boundjid
+ iq['to'] = to
+ iq['type'] = 'error'
+ iq['error']['code'] = ecode
+ iq['error']['type'] = etype
+ iq['error']['condition'] = econd
+ if 'id' in kwargs:
+ iq['id'] = kwargs["id"]
+ return iq.send(
+ timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None)
+ )
diff --git a/slixmpp/plugins/xep_0332/stanza/__init__.py b/slixmpp/plugins/xep_0332/stanza/__init__.py
new file mode 100644
index 00000000..f98375c6
--- /dev/null
+++ b/slixmpp/plugins/xep_0332/stanza/__init__.py
@@ -0,0 +1,13 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of HTTP over XMPP transport
+ http://xmpp.org/extensions/xep-0332.html
+ Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.xep_0332.stanza.request import HTTPRequest
+from slixmpp.plugins.xep_0332.stanza.response import HTTPResponse
+from slixmpp.plugins.xep_0332.stanza.data import HTTPData
diff --git a/slixmpp/plugins/xep_0332/stanza/data.py b/slixmpp/plugins/xep_0332/stanza/data.py
new file mode 100644
index 00000000..a19c94f5
--- /dev/null
+++ b/slixmpp/plugins/xep_0332/stanza/data.py
@@ -0,0 +1,30 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of HTTP over XMPP transport
+ http://xmpp.org/extensions/xep-0332.html
+ Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class HTTPData(ElementBase):
+ """
+ The data element.
+ """
+ name = 'data'
+ namespace = 'urn:xmpp:http'
+ interfaces = set(['data'])
+ plugin_attrib = 'data'
+ is_extension = True
+
+ def get_data(self, encoding='text'):
+ data = self._get_sub_text(encoding, None)
+ return str(data) if data is not None else data
+
+ def set_data(self, data, encoding='text'):
+ self._set_sub_text(encoding, text=data)
+
diff --git a/slixmpp/plugins/xep_0332/stanza/request.py b/slixmpp/plugins/xep_0332/stanza/request.py
new file mode 100644
index 00000000..e3e46361
--- /dev/null
+++ b/slixmpp/plugins/xep_0332/stanza/request.py
@@ -0,0 +1,71 @@
+"""
+ slixmpp: The Slick XMPP Library
+ Implementation of HTTP over XMPP transport
+ http://xmpp.org/extensions/xep-0332.html
+ Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class HTTPRequest(ElementBase):
+
+ """
+ All HTTP communication is done using the `Request`/`Response` paradigm.
+ Each HTTP Request is made sending an `iq` stanza containing a `req`
+ element to the server. Each `iq` stanza sent is of type `set`.
+
+ Examples:
+ <iq type='set' from='a@b.com/browser' to='x@y.com' id='1'>
+ <req xmlns='urn:xmpp:http'
+ method='GET'
+ resource='/api/users'
+ version='1.1'>
+ <headers xmlns='http://jabber.org/protocol/shim'>
+ <header name='Host'>b.com</header>
+ </headers>
+ </req>
+ </iq>
+
+ <iq type='set' from='a@b.com/browser' to='x@y.com' id='2'>
+ <req xmlns='urn:xmpp:http'
+ method='PUT'
+ resource='/api/users'
+ version='1.1'>
+ <headers xmlns='http://jabber.org/protocol/shim'>
+ <header name='Host'>b.com</header>
+ <header name='Content-Type'>text/html</header>
+ <header name='Content-Length'>...</header>
+ </headers>
+ <data>
+ <text>...</text>
+ </data>
+ </req>
+ </iq>
+ """
+
+ name = 'request'
+ namespace = 'urn:xmpp:http'
+ interfaces = set(['method', 'resource', 'version'])
+ plugin_attrib = 'http-req'
+
+ def get_method(self):
+ return self._get_attr('method', None)
+
+ def set_method(self, method):
+ self._set_attr('method', method)
+
+ def get_resource(self):
+ return self._get_attr('resource', None)
+
+ def set_resource(self, resource):
+ self._set_attr('resource', resource)
+
+ def get_version(self):
+ return self._get_attr('version', None)
+
+ def set_version(self, version='1.1'):
+ self._set_attr('version', version)
diff --git a/slixmpp/plugins/xep_0332/stanza/response.py b/slixmpp/plugins/xep_0332/stanza/response.py
new file mode 100644
index 00000000..a0b8fe34
--- /dev/null
+++ b/slixmpp/plugins/xep_0332/stanza/response.py
@@ -0,0 +1,66 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Implementation of HTTP over XMPP transport
+ http://xmpp.org/extensions/xep-0332.html
+ Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
+ This file is part of slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+class HTTPResponse(ElementBase):
+
+ """
+ When the HTTP Server responds, it does so by sending an `iq` stanza
+ response (type=`result`) back to the client containing the `resp` element.
+ Since response are asynchronous, and since multiple requests may be active
+ at the same time, responses may be returned in a different order than the
+ in which the original requests were made.
+
+ Examples:
+ <iq type='result'
+ from='httpserver@clayster.com'
+ to='httpclient@clayster.com/browser' id='2'>
+ <resp xmlns='urn:xmpp:http'
+ version='1.1'
+ statusCode='200'
+ statusMessage='OK'>
+ <headers xmlns='http://jabber.org/protocol/shim'>
+ <header name='Date'>Fri, 03 May 2013 16:39:54GMT-4</header>
+ <header name='Server'>Clayster</header>
+ <header name='Content-Type'>text/turtle</header>
+ <header name='Content-Length'>...</header>
+ <header name='Connection'>Close</header>
+ </headers>
+ <data>
+ <text>
+ ...
+ </text>
+ </data>
+ </resp>
+ </iq>
+ """
+
+ name = 'response'
+ namespace = 'urn:xmpp:http'
+ interfaces = set(['code', 'message', 'version'])
+ plugin_attrib = 'http-resp'
+
+ def get_code(self):
+ code = self._get_attr('statusCode', None)
+ return int(code) if code is not None else code
+
+ def set_code(self, code):
+ self._set_attr('statusCode', str(code))
+
+ def get_message(self):
+ return self._get_attr('statusMessage', '')
+
+ def set_message(self, message):
+ self._set_attr('statusMessage', message)
+
+ def set_version(self, version='1.1'):
+ self._set_attr('version', version)
diff --git a/slixmpp/roster/__init__.py b/slixmpp/roster/__init__.py
new file mode 100644
index 00000000..9a9b69a1
--- /dev/null
+++ b/slixmpp/roster/__init__.py
@@ -0,0 +1,11 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.roster.item import RosterItem
+from slixmpp.roster.single import RosterNode
+from slixmpp.roster.multi import Roster
diff --git a/slixmpp/roster/item.py b/slixmpp/roster/item.py
new file mode 100644
index 00000000..c1eb574a
--- /dev/null
+++ b/slixmpp/roster/item.py
@@ -0,0 +1,497 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class RosterItem(object):
+
+ """
+ A RosterItem is a single entry in a roster node, and tracks
+ the subscription state and user annotations of a single JID.
+
+ Roster items may use an external datastore to persist roster data
+ across sessions. Client applications will not need to use this
+ functionality, but is intended for components that do not have their
+ roster persisted automatically by the XMPP server.
+
+ Roster items provide many methods for handling incoming presence
+ stanzas that ensure that response stanzas are sent according to
+ RFC 3921.
+
+ The external datastore is accessed through a provided interface
+ object which is stored in self.db. The interface object MUST
+ provide two methods: load and save, both of which are responsible
+ for working with a single roster item. A private dictionary,
+ self._db_state, is used to store any metadata needed by the
+ interface, such as the row ID of a roster item, etc.
+
+ Interface for self.db.load:
+ load(owner_jid, jid, db_state):
+ owner_jid -- The JID that owns the roster.
+ jid -- The JID of the roster item.
+ db_state -- A dictionary containing any data saved
+ by the interface object after a save()
+ call. Will typically have the equivalent
+ of a 'row_id' value.
+
+ Interface for self.db.save:
+ save(owner_jid, jid, item_state, db_state):
+ owner_jid -- The JID that owns the roster.
+ jid -- The JID of the roster item.
+ item_state -- A dictionary containing the fields:
+ 'from', 'to', 'pending_in', 'pending_out',
+ 'whitelisted', 'subscription', 'name',
+ and 'groups'.
+ db_state -- A dictionary provided for persisting
+ datastore specific information. Typically,
+ a value equivalent to 'row_id' will be
+ stored here.
+
+ State Fields:
+ from -- Indicates if a subscription of type 'from'
+ has been authorized.
+ to -- Indicates if a subscription of type 'to' has
+ been authorized.
+ pending_in -- Indicates if a subscription request has been
+ received from this JID and it has not been
+ authorized yet.
+ pending_out -- Indicates if a subscription request has been sent
+ to this JID and it has not been accepted yet.
+ subscription -- Returns one of: 'to', 'from', 'both', or 'none'
+ based on the states of from, to, pending_in,
+ and pending_out. Assignment to this value does
+ not affect the states of the other values.
+ whitelisted -- Indicates if a subscription request from this
+ JID should be automatically accepted.
+ name -- A user supplied alias for the JID.
+ groups -- A list of group names for the JID.
+
+ Attributes:
+ xmpp -- The main Slixmpp instance.
+ owner -- The JID that owns the roster.
+ jid -- The JID for the roster item.
+ db -- Optional datastore interface object.
+ last_status -- The last presence sent to this JID.
+ resources -- A dictionary of online resources for this JID.
+ Will contain the fields 'show', 'status',
+ and 'priority'.
+
+ Methods:
+ load -- Retrieve the roster item from an
+ external datastore, if one was provided.
+ save -- Save the roster item to an external
+ datastore, if one was provided.
+ remove -- Remove a subscription to the JID and revoke
+ its whitelisted status.
+ subscribe -- Subscribe to the JID.
+ authorize -- Accept a subscription from the JID.
+ unauthorize -- Deny a subscription from the JID.
+ unsubscribe -- Unsubscribe from the JID.
+ send_presence -- Send a directed presence to the JID.
+ send_last_presence -- Resend the last sent presence.
+ handle_available -- Update the JID's resource information.
+ handle_unavailable -- Update the JID's resource information.
+ handle_subscribe -- Handle a subscription request.
+ handle_subscribed -- Handle a notice that a subscription request
+ was authorized by the JID.
+ handle_unsubscribe -- Handle an unsubscribe request.
+ handle_unsubscribed -- Handle a notice that a subscription was
+ removed by the JID.
+ handle_probe -- Handle a presence probe query.
+ """
+
+ def __init__(self, xmpp, jid, owner=None,
+ state=None, db=None, roster=None):
+ """
+ Create a new roster item.
+
+ Arguments:
+ xmpp -- The main Slixmpp instance.
+ jid -- The item's JID.
+ owner -- The roster owner's JID. Defaults
+ so self.xmpp.boundjid.bare.
+ state -- A dictionary of initial state values.
+ db -- An optional interface to an external datastore.
+ roster -- The roster object containing this entry.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.owner = owner or self.xmpp.boundjid.bare
+ self.last_status = None
+ self.resources = {}
+ self.roster = roster
+ self.db = db
+ self._state = state or {
+ 'from': False,
+ 'to': False,
+ 'pending_in': False,
+ 'pending_out': False,
+ 'whitelisted': False,
+ 'subscription': 'none',
+ 'name': '',
+ 'groups': []}
+
+ self._db_state = {}
+ self.load()
+
+ def set_backend(self, db=None, save=True):
+ """
+ Set the datastore interface object for the roster item.
+
+ Arguments:
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
+ """
+ self.db = db
+ if save:
+ self.save()
+ self.load()
+
+ def load(self):
+ """
+ Load the item's state information from an external datastore,
+ if one has been provided.
+ """
+ if self.db:
+ item = self.db.load(self.owner, self.jid,
+ self._db_state)
+ if item:
+ self['name'] = item['name']
+ self['groups'] = item['groups']
+ self['from'] = item['from']
+ self['to'] = item['to']
+ self['whitelisted'] = item['whitelisted']
+ self['pending_out'] = item['pending_out']
+ self['pending_in'] = item['pending_in']
+ self['subscription'] = self._subscription()
+ return self._state
+ return None
+
+ def save(self, remove=False):
+ """
+ Save the item's state information to an external datastore,
+ if one has been provided.
+
+ Arguments:
+ remove -- If True, expunge the item from the datastore.
+ """
+ self['subscription'] = self._subscription()
+ if remove:
+ self._state['removed'] = True
+ if self.db:
+ self.db.save(self.owner, self.jid,
+ self._state, self._db_state)
+
+ # Finally, remove the in-memory copy if needed.
+ if remove:
+ del self.xmpp.roster[self.owner][self.jid]
+
+ def __getitem__(self, key):
+ """Return a state field's value."""
+ if key in self._state:
+ if key == 'subscription':
+ return self._subscription()
+ return self._state[key]
+ else:
+ raise KeyError
+
+ def __setitem__(self, key, value):
+ """
+ Set the value of a state field.
+
+ For boolean states, the values True, 'true', '1', 'on',
+ and 'yes' are accepted as True; all others are False.
+
+ Arguments:
+ key -- The state field to modify.
+ value -- The new value of the state field.
+ """
+ if key in self._state:
+ if key in ['name', 'subscription', 'groups']:
+ self._state[key] = value
+ else:
+ value = str(value).lower()
+ self._state[key] = value in ('true', '1', 'on', 'yes')
+ else:
+ raise KeyError
+
+ def _subscription(self):
+ """Return the proper subscription type based on current state."""
+ if self['to'] and self['from']:
+ return 'both'
+ elif self['from']:
+ return 'from'
+ elif self['to']:
+ return 'to'
+ else:
+ return 'none'
+
+ def remove(self):
+ """
+ Remove a JID's whitelisted status and unsubscribe if a
+ subscription exists.
+ """
+ if self['to']:
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+ self['to'] = False
+ self['whitelisted'] = False
+ self.save()
+
+ def subscribe(self):
+ """Send a subscription request to the JID."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'subscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self['pending_out'] = True
+ self.save()
+ p.send()
+
+ def authorize(self):
+ """Authorize a received subscription request from the JID."""
+ self['from'] = True
+ self['pending_in'] = False
+ self.save()
+ self._subscribed()
+ self.send_last_presence()
+
+ def unauthorize(self):
+ """Deny a received subscription request from the JID."""
+ self['from'] = False
+ self['pending_in'] = False
+ self.save()
+ self._unsubscribed()
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unavailable'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def _subscribed(self):
+ """Handle acknowledging a subscription."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'subscribed'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def unsubscribe(self):
+ """Unsubscribe from the JID."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self.save()
+ p.send()
+
+ def _unsubscribed(self):
+ """Handle acknowledging an unsubscribe request."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribed'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def send_presence(self, **kwargs):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of a directed presence, which should
+ be the owner JID plus resource.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ if self.xmpp.is_component and not kwargs.get('pfrom', ''):
+ kwargs['pfrom'] = self.owner
+ if not kwargs.get('pto', ''):
+ kwargs['pto'] = self.jid
+ self.xmpp.send_presence(**kwargs)
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ pres = self.roster.last_status
+ if pres is None:
+ self.send_presence()
+ else:
+ pres['to'] = self.jid
+ if self.xmpp.is_component:
+ pres['from'] = self.owner
+ else:
+ del pres['from']
+ pres.send()
+ else:
+ self.last_status.send()
+
+ def handle_available(self, presence):
+ resource = presence['from'].resource
+ data = {'status': presence['status'],
+ 'show': presence['show'],
+ 'priority': presence['priority']}
+ got_online = not self.resources
+ if resource not in self.resources:
+ self.resources[resource] = {}
+ old_status = self.resources[resource].get('status', '')
+ old_show = self.resources[resource].get('show', None)
+ self.resources[resource].update(data)
+ if got_online:
+ self.xmpp.event('got_online', presence)
+ if old_show != presence['show'] or old_status != presence['status']:
+ self.xmpp.event('changed_status', presence)
+
+ def handle_unavailable(self, presence):
+ resource = presence['from'].resource
+ if not self.resources:
+ return
+ if resource in self.resources:
+ del self.resources[resource]
+ self.xmpp.event('changed_status', presence)
+ if not self.resources:
+ self.xmpp.event('got_offline', presence)
+
+ def handle_subscribe(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | yes | "None + Pending In" |
+ | "None + Pending Out" | yes | "None + Pending Out/In" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | no | no state change |
+ | "To" | yes | "To + Pending In" |
+ | "To + Pending In" | no | no state change |
+ | "From" | no * | no state change |
+ | "From + Pending Out" | no * | no state change |
+ | "Both" | no * | no state change |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['from'] and not self['pending_in']:
+ self['pending_in'] = True
+ self.xmpp.event('roster_subscription_request', presence)
+ elif self['from']:
+ self._subscribed()
+ self.save()
+ else:
+ #server shouldn't send an invalid subscription request
+ self.xmpp.event('roster_subscription_request', presence)
+
+ def handle_subscribed(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | yes | "To" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | yes | "To + Pending In" |
+ | "To" | no | no state change |
+ | "To + Pending In" | no | no state change |
+ | "From" | no | no state change |
+ | "From + Pending Out" | yes | "Both" |
+ | "Both" | no | no state change |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['to'] and self['pending_out']:
+ self['pending_out'] = False
+ self['to'] = True
+ self.xmpp.event('roster_subscription_authorized', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_authorized', presence)
+
+ def handle_unsubscribe(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | no | no state change |
+ | "None + Pending In" | yes * | "None" |
+ | "None + Pending Out/In" | yes * | "None + Pending Out" |
+ | "To" | no | no state change |
+ | "To + Pending In" | yes * | "To" |
+ | "From" | yes * | "None" |
+ | "From + Pending Out" | yes * | "None + Pending Out |
+ | "Both" | yes * | "To" |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['from'] and self['pending_in']:
+ self['pending_in'] = False
+ self._unsubscribed()
+ elif self['from']:
+ self['from'] = False
+ self._unsubscribed()
+ self.xmpp.event('roster_subscription_remove', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_remove', presence)
+
+ def handle_unsubscribed(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | yes | "None" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | yes | "None + Pending In" |
+ | "To" | yes | "None" |
+ | "To + Pending In" | yes | "None + Pending In" |
+ | "From" | no | no state change |
+ | "From + Pending Out" | yes | "From" |
+ | "Both" | yes | "From" |
+ +------------------------------------------------------------------
+ """
+ if self.xmpp.is_component:
+ if not self['to'] and self['pending_out']:
+ self['pending_out'] = False
+ elif self['to'] and not self['pending_out']:
+ self['to'] = False
+ self.xmpp.event('roster_subscription_removed', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_removed', presence)
+
+ def handle_probe(self, presence):
+ if self['from']:
+ self.send_last_presence()
+ if self['pending_out']:
+ self.subscribe()
+ if not self['from']:
+ self._unsubscribed()
+
+ def reset(self):
+ """
+ Forgot current resource presence information as part of
+ a roster reset request.
+ """
+ self.resources = {}
+
+ def __repr__(self):
+ return repr(self._state)
diff --git a/slixmpp/roster/multi.py b/slixmpp/roster/multi.py
new file mode 100644
index 00000000..e1a44a08
--- /dev/null
+++ b/slixmpp/roster/multi.py
@@ -0,0 +1,224 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Presence
+from slixmpp.xmlstream import JID
+from slixmpp.roster import RosterNode
+
+
+class Roster(object):
+
+ """
+ Slixmpp's roster manager.
+
+ The roster is divided into "nodes", where each node is responsible
+ for a single JID. While the distinction is not strictly necessary
+ for client connections, it is a necessity for components that use
+ multiple JIDs.
+
+ Rosters may be stored and persisted in an external datastore. An
+ interface object to the datastore that loads and saves roster items may
+ be provided. See the documentation for the RosterItem class for the
+ methods that the datastore interface object must provide.
+
+ Attributes:
+ xmpp -- The main Slixmpp instance.
+ db -- Optional interface object to an external datastore.
+ auto_authorize -- Default auto_authorize value for new roster nodes.
+ Defaults to True.
+ auto_subscribe -- Default auto_subscribe value for new roster nodes.
+ Defaults to True.
+
+ Methods:
+ add -- Create a new roster node for a JID.
+ send_presence -- Shortcut for sending a presence stanza.
+ """
+
+ def __init__(self, xmpp, db=None):
+ """
+ Create a new roster.
+
+ Arguments:
+ xmpp -- The main Slixmpp instance.
+ db -- Optional interface object to a datastore.
+ """
+ self.xmpp = xmpp
+ self.db = db
+ self._auto_authorize = True
+ self._auto_subscribe = True
+ self._rosters = {}
+
+ if self.db:
+ for node in self.db.entries(None, {}):
+ self.add(node)
+
+ self.xmpp.add_filter('out', self._save_last_status)
+
+ def _save_last_status(self, stanza):
+
+ if isinstance(stanza, Presence):
+ sfrom = stanza['from'].full
+ sto = stanza['to'].full
+
+ if not sfrom:
+ sfrom = self.xmpp.boundjid
+
+ if stanza['type'] in stanza.showtypes or \
+ stanza['type'] in ('available', 'unavailable'):
+ if sto:
+ self[sfrom][sto].last_status = stanza
+ else:
+ self[sfrom].last_status = stanza
+ with self[sfrom]._last_status_lock:
+ for jid in self[sfrom]:
+ self[sfrom][jid].last_status = None
+
+ if not self.xmpp.sentpresence:
+ self.xmpp.event('sent_presence')
+ self.xmpp.sentpresence = True
+
+ return stanza
+
+ def __getitem__(self, key):
+ """
+ Return the roster node for a JID.
+
+ A new roster node will be created if one
+ does not already exist.
+
+ Arguments:
+ key -- Return the roster for this JID.
+ """
+ if key is None:
+ key = self.xmpp.boundjid
+ if not isinstance(key, JID):
+ key = JID(key)
+ key = key.bare
+
+ if key not in self._rosters:
+ self.add(key)
+ self._rosters[key].auto_authorize = self.auto_authorize
+ self._rosters[key].auto_subscribe = self.auto_subscribe
+ return self._rosters[key]
+
+ def keys(self):
+ """Return the JIDs managed by the roster."""
+ return self._rosters.keys()
+
+ def __iter__(self):
+ """Iterate over the roster nodes."""
+ return self._rosters.__iter__()
+
+ def add(self, node):
+ """
+ Add a new roster node for the given JID.
+
+ Arguments:
+ node -- The JID for the new roster node.
+ """
+ if not isinstance(node, JID):
+ node = JID(node)
+
+ node = node.bare
+ if node not in self._rosters:
+ self._rosters[node] = RosterNode(self.xmpp, node, self.db)
+
+ def set_backend(self, db=None, save=True):
+ """
+ Set the datastore interface object for the roster.
+
+ Arguments:
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
+ """
+ self.db = db
+ existing_entries = set(self._rosters)
+ new_entries = set(self.db.entries(None, {}))
+
+ for node in existing_entries:
+ self._rosters[node].set_backend(db, save)
+ for node in new_entries - existing_entries:
+ self.add(node)
+
+ def reset(self):
+ """
+ Reset the state of the roster to forget any current
+ presence information. Useful after a disconnection occurs.
+ """
+ for node in self:
+ self[node].reset()
+
+ def send_presence(self, **kwargs):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of a directed presence, which should
+ be the owner JID plus resource.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ if self.xmpp.is_component and not kwargs.get('pfrom', ''):
+ kwargs['pfrom'] = self.jid
+ self.xmpp.send_presence(**kwargs)
+
+ @property
+ def auto_authorize(self):
+ """
+ Auto accept or deny subscription requests.
+
+ If True, auto accept subscription requests.
+ If False, auto deny subscription requests.
+ If None, don't automatically respond.
+ """
+ return self._auto_authorize
+
+ @auto_authorize.setter
+ def auto_authorize(self, value):
+ """
+ Auto accept or deny subscription requests.
+
+ If True, auto accept subscription requests.
+ If False, auto deny subscription requests.
+ If None, don't automatically respond.
+ """
+ self._auto_authorize = value
+ for node in self._rosters:
+ self._rosters[node].auto_authorize = value
+
+ @property
+ def auto_subscribe(self):
+ """
+ Auto send requests for mutual subscriptions.
+
+ If True, auto send mutual subscription requests.
+ """
+ return self._auto_subscribe
+
+ @auto_subscribe.setter
+ def auto_subscribe(self, value):
+ """
+ Auto send requests for mutual subscriptions.
+
+ If True, auto send mutual subscription requests.
+ """
+ self._auto_subscribe = value
+ for node in self._rosters:
+ self._rosters[node].auto_subscribe = value
+
+ def __repr__(self):
+ return repr(self._rosters)
diff --git a/slixmpp/roster/single.py b/slixmpp/roster/single.py
new file mode 100644
index 00000000..62fbca41
--- /dev/null
+++ b/slixmpp/roster/single.py
@@ -0,0 +1,337 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import threading
+
+from slixmpp.xmlstream import JID
+from slixmpp.roster import RosterItem
+
+
+class RosterNode(object):
+
+ """
+ A roster node is a roster for a single JID.
+
+ Attributes:
+ xmpp -- The main Slixmpp instance.
+ jid -- The JID that owns the roster node.
+ db -- Optional interface to an external datastore.
+ auto_authorize -- Determines how authorizations are handled:
+ True -- Accept all subscriptions.
+ False -- Reject all subscriptions.
+ None -- Subscriptions must be
+ manually authorized.
+ Defaults to True.
+ auto_subscribe -- Determines if bi-directional subscriptions
+ are created after automatically authrorizing
+ a subscription request.
+ Defaults to True
+ last_status -- The last sent presence status that was broadcast
+ to all contact JIDs.
+
+ Methods:
+ add -- Add a JID to the roster.
+ update -- Update a JID's subscription information.
+ subscribe -- Subscribe to a JID.
+ unsubscribe -- Unsubscribe from a JID.
+ remove -- Remove a JID from the roster.
+ presence -- Return presence information for a JID's resources.
+ send_presence -- Shortcut for sending a presence stanza.
+ """
+
+ def __init__(self, xmpp, jid, db=None):
+ """
+ Create a roster node for a JID.
+
+ Arguments:
+ xmpp -- The main Slixmpp instance.
+ jid -- The JID that owns the roster.
+ db -- Optional interface to an external datastore.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.db = db
+ self.auto_authorize = True
+ self.auto_subscribe = True
+ self.last_status = None
+ self._version = ''
+ self._jids = {}
+ self._last_status_lock = threading.Lock()
+
+ if self.db:
+ if hasattr(self.db, 'version'):
+ self._version = self.db.version(self.jid)
+ for jid in self.db.entries(self.jid):
+ self.add(jid)
+
+ @property
+ def version(self):
+ """Retrieve the roster's version ID."""
+ if self.db and hasattr(self.db, 'version'):
+ self._version = self.db.version(self.jid)
+ return self._version
+
+ @version.setter
+ def version(self, version):
+ """Set the roster's version ID."""
+ self._version = version
+ if self.db and hasattr(self.db, 'set_version'):
+ self.db.set_version(self.jid, version)
+
+ def __getitem__(self, key):
+ """
+ Return the roster item for a subscribed JID.
+
+ A new item entry will be created if one does not already exist.
+ """
+ if key is None:
+ key = JID('')
+ if not isinstance(key, JID):
+ key = JID(key)
+ key = key.bare
+ if key not in self._jids:
+ self.add(key, save=True)
+ return self._jids[key]
+
+ def __delitem__(self, key):
+ """
+ Remove a roster item from the local storage.
+
+ To remove an item from the server, use the remove() method.
+ """
+ if key is None:
+ key = JID('')
+ if not isinstance(key, JID):
+ key = JID(key)
+ key = key.bare
+ if key in self._jids:
+ del self._jids[key]
+
+ def __len__(self):
+ """Return the number of JIDs referenced by the roster."""
+ return len(self._jids)
+
+ def keys(self):
+ """Return a list of all subscribed JIDs."""
+ return self._jids.keys()
+
+ def has_jid(self, jid):
+ """Returns whether the roster has a JID."""
+ return jid in self._jids
+
+ def groups(self):
+ """Return a dictionary mapping group names to JIDs."""
+ result = {}
+ for jid in self._jids:
+ groups = self._jids[jid]['groups']
+ if not groups:
+ if '' not in result:
+ result[''] = []
+ result[''].append(jid)
+ for group in groups:
+ if group not in result:
+ result[group] = []
+ result[group].append(jid)
+ return result
+
+ def __iter__(self):
+ """Iterate over the roster items."""
+ return self._jids.__iter__()
+
+ def set_backend(self, db=None, save=True):
+ """
+ Set the datastore interface object for the roster node.
+
+ Arguments:
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
+ """
+ self.db = db
+ existing_entries = set(self._jids)
+ new_entries = set(self.db.entries(self.jid, {}))
+
+ for jid in existing_entries:
+ self._jids[jid].set_backend(db, save)
+ for jid in new_entries - existing_entries:
+ self.add(jid)
+
+ def add(self, jid, name='', groups=None, afrom=False, ato=False,
+ pending_in=False, pending_out=False, whitelisted=False,
+ save=False):
+ """
+ Add a new roster item entry.
+
+ Arguments:
+ jid -- The JID for the roster item.
+ name -- An alias for the JID.
+ groups -- A list of group names.
+ afrom -- Indicates if the JID has a subscription state
+ of 'from'. Defaults to False.
+ ato -- Indicates if the JID has a subscription state
+ of 'to'. Defaults to False.
+ pending_in -- Indicates if the JID has sent a subscription
+ request to this connection's JID.
+ Defaults to False.
+ pending_out -- Indicates if a subscription request has been sent
+ to this JID.
+ Defaults to False.
+ whitelisted -- Indicates if a subscription request from this JID
+ should be automatically authorized.
+ Defaults to False.
+ save -- Indicates if the item should be persisted
+ immediately to an external datastore,
+ if one is used.
+ Defaults to False.
+ """
+ if isinstance(jid, JID):
+ key = jid.bare
+ else:
+ key = jid
+
+ state = {'name': name,
+ 'groups': groups or [],
+ 'from': afrom,
+ 'to': ato,
+ 'pending_in': pending_in,
+ 'pending_out': pending_out,
+ 'whitelisted': whitelisted,
+ 'subscription': 'none'}
+ self._jids[key] = RosterItem(self.xmpp, jid, self.jid,
+ state=state, db=self.db,
+ roster=self)
+ if save:
+ self._jids[key].save()
+
+ def subscribe(self, jid):
+ """
+ Subscribe to the given JID.
+
+ Arguments:
+ jid -- The JID to subscribe to.
+ """
+ self[jid].subscribe()
+
+ def unsubscribe(self, jid):
+ """
+ Unsubscribe from the given JID.
+
+ Arguments:
+ jid -- The JID to unsubscribe from.
+ """
+ self[jid].unsubscribe()
+
+ def remove(self, jid):
+ """
+ Remove a JID from the roster.
+
+ Arguments:
+ jid -- The JID to remove.
+ """
+ self[jid].remove()
+ if not self.xmpp.is_component:
+ return self.update(jid, subscription='remove')
+
+ def update(self, jid, name=None, subscription=None, groups=[],
+ timeout=None, callback=None, timeout_callback=None):
+ """
+ Update a JID's subscription information.
+
+ Arguments:
+ jid -- The JID to update.
+ name -- Optional alias for the JID.
+ subscription -- The subscription state. May be one of: 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names.
+ timeout -- The length of time (in seconds) to wait
+ for a response before continuing if blocking
+ is used. Defaults to self.response_timeout.
+ callback -- Optional reference to a stream handler function.
+ Will be executed when the roster is received.
+ """
+ if not groups:
+ groups = []
+
+ self[jid]['name'] = name
+ self[jid]['groups'] = groups
+ self[jid].save()
+
+ if not self.xmpp.is_component:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['roster']['items'] = {jid: {'name': name,
+ 'subscription': subscription,
+ 'groups': groups}}
+
+ return iq.send(timeout=timeout, callback=callback,
+ timeout_callback=timeout_callback)
+
+ def presence(self, jid, resource=None):
+ """
+ Retrieve the presence information of a JID.
+
+ May return either all online resources' status, or
+ a single resource's status.
+
+ Arguments:
+ jid -- The JID to lookup.
+ resource -- Optional resource for returning
+ only the status of a single connection.
+ """
+ if resource is None:
+ return self[jid].resources
+
+ default_presence = {'status': '',
+ 'priority': 0,
+ 'show': ''}
+ return self[jid].resources.get(resource,
+ default_presence)
+
+ def reset(self):
+ """
+ Reset the state of the roster to forget any current
+ presence information. Useful after a disconnection occurs.
+ """
+ for jid in self:
+ self[jid].reset()
+
+ def send_presence(self, **kwargs):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of a directed presence, which should
+ be the owner JID plus resource.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ if self.xmpp.is_component and not kwargs.get('pfrom', ''):
+ kwargs['pfrom'] = self.jid
+ self.xmpp.send_presence(**kwargs)
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ self.send_presence()
+ else:
+ pres = self.last_status
+ if self.xmpp.is_component:
+ pres['from'] = self.jid
+ else:
+ del pres['from']
+ pres.send()
+
+ def __repr__(self):
+ return repr(self._jids)
diff --git a/slixmpp/stanza/__init__.py b/slixmpp/stanza/__init__.py
new file mode 100644
index 00000000..6cd6a2c5
--- /dev/null
+++ b/slixmpp/stanza/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from slixmpp.stanza.error import Error
+from slixmpp.stanza.iq import Iq
+from slixmpp.stanza.message import Message
+from slixmpp.stanza.presence import Presence
+from slixmpp.stanza.stream_features import StreamFeatures
+from slixmpp.stanza.stream_error import StreamError
diff --git a/slixmpp/stanza/atom.py b/slixmpp/stanza/atom.py
new file mode 100644
index 00000000..ccded724
--- /dev/null
+++ b/slixmpp/stanza/atom.py
@@ -0,0 +1,43 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+class AtomEntry(ElementBase):
+
+ """
+ A simple Atom feed entry.
+
+ Stanza Interface:
+ title -- The title of the Atom feed entry.
+ summary -- The summary of the Atom feed entry.
+ """
+
+ namespace = 'http://www.w3.org/2005/Atom'
+ name = 'entry'
+ plugin_attrib = 'entry'
+ interfaces = set(('title', 'summary', 'id', 'published', 'updated'))
+ sub_interfaces = set(('title', 'summary', 'id', 'published',
+ 'updated'))
+
+class AtomAuthor(ElementBase):
+
+ """
+ An Atom author.
+
+ Stanza Interface:
+ name -- The printable author name
+ uri -- The bare jid of the author
+ """
+
+ name = 'author'
+ plugin_attrib = 'author'
+ interfaces = set(('name', 'uri'))
+ sub_interfaces = set(('name', 'uri'))
+
+register_stanza_plugin(AtomEntry, AtomAuthor)
diff --git a/slixmpp/stanza/error.py b/slixmpp/stanza/error.py
new file mode 100644
index 00000000..67f736c0
--- /dev/null
+++ b/slixmpp/stanza/error.py
@@ -0,0 +1,164 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase, ET
+
+
+class Error(ElementBase):
+
+ """
+ XMPP stanzas of type 'error' should include an <error> stanza that
+ describes the nature of the error and how it should be handled.
+
+ Use the 'XEP-0086: Error Condition Mappings' plugin to include error
+ codes used in older XMPP versions.
+
+ Example error stanza:
+ <error type="cancel" code="404">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ The item was not found.
+ </text>
+ </error>
+
+ Stanza Interface:
+ code -- The error code used in older XMPP versions.
+ condition -- The name of the condition element.
+ text -- Human readable description of the error.
+ type -- Error type indicating how the error should be handled.
+
+ Attributes:
+ conditions -- The set of allowable error condition elements.
+ condition_ns -- The namespace for the condition element.
+ types -- A set of values indicating how the error
+ should be treated.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_condition -- Retrieve the name of the condition element.
+ set_condition -- Add a condition element.
+ del_condition -- Remove the condition element.
+ get_text -- Retrieve the contents of the <text> element.
+ set_text -- Set the contents of the <text> element.
+ del_text -- Remove the <text> element.
+ """
+
+ namespace = 'jabber:client'
+ name = 'error'
+ plugin_attrib = 'error'
+ interfaces = set(('code', 'condition', 'text', 'type',
+ 'gone', 'redirect', 'by'))
+ sub_interfaces = set(('text',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ conditions = set(('bad-request', 'conflict', 'feature-not-implemented',
+ 'forbidden', 'gone', 'internal-server-error',
+ 'item-not-found', 'jid-malformed', 'not-acceptable',
+ 'not-allowed', 'not-authorized', 'payment-required',
+ 'recipient-unavailable', 'redirect',
+ 'registration-required', 'remote-server-not-found',
+ 'remote-server-timeout', 'resource-constraint',
+ 'service-unavailable', 'subscription-required',
+ 'undefined-condition', 'unexpected-request'))
+ condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
+ types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup.
+
+ Sets a default error type and condition, and changes the
+ parent stanza's type to 'error'.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ if ElementBase.setup(self, xml):
+ #If we had to generate XML then set default values.
+ self['type'] = 'cancel'
+ self['condition'] = 'feature-not-implemented'
+ if self.parent is not None:
+ self.parent()['type'] = 'error'
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.xml:
+ if "{%s}" % self.condition_ns in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return ''
+
+ def set_condition(self, value):
+ """
+ Set the tag name of the condition element.
+
+ Arguments:
+ value -- The tag name of the condition element.
+ """
+ if value in self.conditions:
+ del self['condition']
+ self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value)))
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.xml:
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.xml.remove(child)
+ return self
+
+ def get_text(self):
+ """Retrieve the contents of the <text> element."""
+ return self._get_sub_text('{%s}text' % self.condition_ns)
+
+ def set_text(self, value):
+ """
+ Set the contents of the <text> element.
+
+ Arguments:
+ value -- The new contents for the <text> element.
+ """
+ self._set_sub_text('{%s}text' % self.condition_ns, text=value)
+ return self
+
+ def del_text(self):
+ """Remove the <text> element."""
+ self._del_sub('{%s}text' % self.condition_ns)
+ return self
+
+ def get_gone(self):
+ return self._get_sub_text('{%s}gone' % self.condition_ns, '')
+
+ def get_redirect(self):
+ return self._get_sub_text('{%s}redirect' % self.condition_ns, '')
+
+ def set_gone(self, value):
+ if value:
+ del self['condition']
+ return self._set_sub_text('{%s}gone' % self.condition_ns, value)
+ elif self['condition'] == 'gone':
+ del self['condition']
+
+ def set_redirect(self, value):
+ if value:
+ del self['condition']
+ ns = self.condition_ns
+ return self._set_sub_text('{%s}redirect' % ns, value)
+ elif self['condition'] == 'redirect':
+ del self['condition']
+
+ def del_gone(self):
+ self._del_sub('{%s}gone' % self.condition_ns)
+
+ def del_redirect(self):
+ self._del_sub('{%s}redirect' % self.condition_ns)
diff --git a/slixmpp/stanza/htmlim.py b/slixmpp/stanza/htmlim.py
new file mode 100644
index 00000000..a5a3e5f3
--- /dev/null
+++ b/slixmpp/stanza/htmlim.py
@@ -0,0 +1,14 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Message
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0071 import XHTML_IM as HTMLIM
+
+
+register_stanza_plugin(Message, HTMLIM)
diff --git a/slixmpp/stanza/iq.py b/slixmpp/stanza/iq.py
new file mode 100644
index 00000000..a64dfa7f
--- /dev/null
+++ b/slixmpp/stanza/iq.py
@@ -0,0 +1,261 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza.rootstanza import RootStanza
+from slixmpp.xmlstream import StanzaBase, ET
+from slixmpp.xmlstream.handler import Waiter, Callback, CoroutineCallback
+from slixmpp.xmlstream.asyncio import asyncio
+from slixmpp.xmlstream.matcher import MatchIDSender, MatcherId
+from slixmpp.exceptions import IqTimeout, IqError
+
+
+class Iq(RootStanza):
+
+ """
+ XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of
+ requesting and modifying information, similar to HTTP's GET and
+ POST methods.
+
+ Each <iq> stanza must have an 'id' value which associates the
+ stanza with the response stanza. XMPP entities must always
+ be given a response <iq> stanza with a type of 'result' after
+ sending a stanza of type 'get' or 'set'.
+
+ Most uses cases for <iq> stanzas will involve adding a <query>
+ element whose namespace indicates the type of information
+ desired. However, some custom XMPP applications use <iq> stanzas
+ as a carrier stanza for an application-specific protocol instead.
+
+ Example <iq> Stanzas:
+
+ .. code-block:: xml
+
+ <iq to="user@example.com" type="get" id="314">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq to="user@localhost" type="result" id="17">
+ <query xmlns='jabber:iq:roster'>
+ <item jid='otheruser@example.net'
+ name='John Doe'
+ subscription='both'>
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ - **query**: The namespace of the <query> element if one exists.
+ Attributes:
+ - **types**: May be one of: get, set, result, or error.
+ """
+
+ namespace = 'jabber:client'
+ name = 'iq'
+ interfaces = set(('type', 'to', 'from', 'id', 'query'))
+ types = set(('get', 'result', 'set', 'error'))
+ plugin_attrib = name
+
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize a new <iq> stanza with an 'id' value.
+
+ Overrides StanzaBase.__init__.
+ """
+ StanzaBase.__init__(self, *args, **kwargs)
+ if self['id'] == '':
+ if self.stream is not None:
+ self['id'] = self.stream.new_id()
+ else:
+ self['id'] = '0'
+
+ def unhandled(self):
+ """
+ Send a feature-not-implemented error if the stanza is not handled.
+
+ Overrides StanzaBase.unhandled.
+ """
+ if self['type'] in ('get', 'set'):
+ reply = self.reply()
+ reply['error']['condition'] = 'feature-not-implemented'
+ reply['error']['text'] = 'No handlers registered for this request.'
+ reply.send()
+
+ def set_payload(self, value):
+ """
+ Set the XML contents of the <iq> stanza.
+
+ :param value: An XML object or a list of XML objects to use as the <iq>
+ stanza's contents
+ :type value: list or XML object
+ """
+ self.clear()
+ StanzaBase.set_payload(self, value)
+ return self
+
+ def set_query(self, value):
+ """
+ Add or modify a <query> element.
+
+ Query elements are differentiated by their namespace.
+
+ :param str value: The namespace of the <query> element.
+ """
+ query = self.xml.find("{%s}query" % value)
+ if query is None and value:
+ plugin = self.plugin_tag_map.get('{%s}query' % value, None)
+ if plugin:
+ self.enable(plugin.plugin_attrib)
+ else:
+ self.clear()
+ query = ET.Element("{%s}query" % value)
+ self.xml.append(query)
+ return self
+
+ def get_query(self):
+ """Return the namespace of the <query> element.
+
+ :rtype: str"""
+ for child in self.xml:
+ if child.tag.endswith('query'):
+ ns = child.tag.split('}')[0]
+ if '{' in ns:
+ ns = ns[1:]
+ return ns
+ return ''
+
+ def del_query(self):
+ """Remove the <query> element."""
+ for child in self.xml:
+ if child.tag.endswith('query'):
+ self.xml.remove(child)
+ return self
+
+ def reply(self, clear=True):
+ """
+ Create a new <iq> stanza replying to ``self``.
+
+ Overrides StanzaBase.reply
+
+ Sets the 'type' to 'result' in addition to the default
+ StanzaBase.reply behavior.
+
+ :param bool clear: Indicates if existing content should be
+ removed before replying. Defaults to True.
+ """
+ new_iq = StanzaBase.reply(self, clear=clear)
+ new_iq['type'] = 'result'
+ return new_iq
+
+ def send(self, callback=None, timeout=None, timeout_callback=None):
+ """Send an <iq> stanza over the XML stream.
+
+ A callback handler can be provided that will be executed when the Iq
+ stanza's result reply is received.
+
+ Returns a future which result will be set to the result Iq if it is of type 'get' or 'set'
+ (when it is received), or a future with the result set to None if it has another type.
+
+ Overrides StanzaBase.send
+
+ :param function callback: Optional reference to a stream handler
+ function. Will be executed when a reply
+ stanza is received.
+ :param int timeout: The length of time (in seconds) to wait for a
+ response before the timeout_callback is called,
+ instead of the regular callback
+ :param function timeout_callback: Optional reference to a stream handler
+ function. Will be executed when the
+ timeout expires before a response has
+ been received for the originally-sent
+ IQ stanza.
+ :rtype: asyncio.Future
+ """
+ if self.stream.session_bind_event.is_set():
+ matcher = MatchIDSender({
+ 'id': self['id'],
+ 'self': self.stream.boundjid,
+ 'peer': self['to']
+ })
+ else:
+ matcher = MatcherId(self['id'])
+
+ future = asyncio.Future()
+
+ def callback_success(result):
+ if result['type'] == 'error':
+ future.set_exception(IqError(result))
+ else:
+ future.set_result(result)
+
+ if timeout is not None:
+ self.stream.cancel_schedule('IqTimeout_%s' % self['id'])
+ if callback is not None:
+ callback(result)
+
+ def callback_timeout():
+ future.set_exception(IqTimeout(self))
+ self.stream.remove_handler('IqCallback_%s' % self['id'])
+ if timeout_callback is not None:
+ timeout_callback(self)
+
+ if self['type'] in ('get', 'set'):
+ handler_name = 'IqCallback_%s' % self['id']
+ if asyncio.iscoroutinefunction(callback):
+ constr = CoroutineCallback
+ else:
+ constr = Callback
+ if timeout is not None:
+ self.stream.schedule('IqTimeout_%s' % self['id'],
+ timeout,
+ callback_timeout,
+ repeat=False)
+ handler = constr(handler_name,
+ matcher,
+ callback_success,
+ once=True)
+ self.stream.register_handler(handler)
+ else:
+ future.set_result(None)
+ StanzaBase.send(self)
+ return future
+
+ def _handle_result(self, iq):
+ # we got the IQ, so don't fire the timeout
+ self.stream.cancel_schedule('IqTimeout_%s' % self['id'])
+ self.callback(iq)
+
+ def _fire_timeout(self):
+ # don't fire the handler for the IQ, if it finally does come in
+ self.stream.remove_handler('IqCallback_%s' % self['id'])
+ self.timeout_callback(self)
+
+ def _set_stanza_values(self, values):
+ """
+ Set multiple stanza interface values using a dictionary.
+
+ Stanza plugin values may be set usind nested dictionaries.
+
+ If the interface 'query' is given, then it will be set
+ last to avoid duplication of the <query /> element.
+
+ Overrides ElementBase._set_stanza_values.
+
+ Arguments:
+ values -- A dictionary mapping stanza interface with values.
+ Plugin interfaces may accept a nested dictionary that
+ will be used recursively.
+ """
+ query = values.get('query', '')
+ if query:
+ del values['query']
+ StanzaBase._set_stanza_values(self, values)
+ self['query'] = query
+ else:
+ StanzaBase._set_stanza_values(self, values)
+ return self
diff --git a/slixmpp/stanza/message.py b/slixmpp/stanza/message.py
new file mode 100644
index 00000000..cbb170fa
--- /dev/null
+++ b/slixmpp/stanza/message.py
@@ -0,0 +1,188 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza.rootstanza import RootStanza
+from slixmpp.xmlstream import StanzaBase, ET
+
+
+class Message(RootStanza):
+
+ """
+ XMPP's <message> stanzas are a "push" mechanism to send information
+ to other XMPP entities without requiring a response.
+
+ Chat clients will typically use <message> stanzas that have a type
+ of either "chat" or "groupchat".
+
+ When handling a message event, be sure to check if the message is
+ an error response.
+
+ Example <message> stanzas:
+
+ .. code-block:: xml
+
+ <message to="user1@example.com" from="user2@example.com">
+ <body>Hi!</body>
+ </message>
+
+ <message type="groupchat" to="room@conference.example.com">
+ <body>Hi everyone!</body>
+ </message>
+
+ Stanza Interface:
+ - **body**: The main contents of the message.
+ - **subject**: An optional description of the message's contents.
+ - **mucroom**: (Read-only) The name of the MUC room that sent the message.
+ - **mucnick**: (Read-only) The MUC nickname of message's sender.
+
+ Attributes:
+ - **types**: May be one of: normal, chat, headline, groupchat, or error.
+ """
+
+ name = 'message'
+ namespace = 'jabber:client'
+ plugin_attrib = name
+ interfaces = set(['type', 'to', 'from', 'id', 'body', 'subject',
+ 'thread', 'parent_thread', 'mucroom', 'mucnick'])
+ sub_interfaces = set(['body', 'subject', 'thread'])
+ lang_interfaces = sub_interfaces
+ types = set(['normal', 'chat', 'headline', 'error', 'groupchat'])
+
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize a new <message /> stanza with an optional 'id' value.
+
+ Overrides StanzaBase.__init__.
+ """
+ StanzaBase.__init__(self, *args, **kwargs)
+ if self['id'] == '':
+ if self.stream is not None and self.stream.use_message_ids:
+ self['id'] = self.stream.new_id()
+
+ def get_type(self):
+ """
+ Return the message type.
+
+ Overrides default stanza interface behavior.
+
+ Returns 'normal' if no type attribute is present.
+
+ :rtype: str
+ """
+ return self._get_attr('type', 'normal')
+
+ def get_parent_thread(self):
+ """Return the message thread's parent thread.
+
+ :rtype: str
+ """
+ thread = self.xml.find('{%s}thread' % self.namespace)
+ if thread is not None:
+ return thread.attrib.get('parent', '')
+ return ''
+
+ def set_parent_thread(self, value):
+ """Add or change the message thread's parent thread.
+
+ :param str value: identifier of the thread"""
+ thread = self.xml.find('{%s}thread' % self.namespace)
+ if value:
+ if thread is None:
+ thread = ET.Element('{%s}thread' % self.namespace)
+ self.xml.append(thread)
+ thread.attrib['parent'] = value
+ else:
+ if thread is not None and 'parent' in thread.attrib:
+ del thread.attrib['parent']
+
+ def del_parent_thread(self):
+ """Delete the message thread's parent reference."""
+ thread = self.xml.find('{%s}thread' % self.namespace)
+ if thread is not None and 'parent' in thread.attrib:
+ del thread.attrib['parent']
+
+ def chat(self):
+ """Set the message type to 'chat'."""
+ self['type'] = 'chat'
+ return self
+
+ def normal(self):
+ """Set the message type to 'normal'."""
+ self['type'] = 'normal'
+ return self
+
+ def reply(self, body=None, clear=True):
+ """
+ Create a message reply.
+
+ Overrides StanzaBase.reply.
+
+ Sets proper 'to' attribute if the message is from a MUC, and
+ adds a message body if one is given.
+
+ :param str body: Optional text content for the message.
+ :param bool clear: Indicates if existing content should be removed
+ before replying. Defaults to True.
+
+ :rtype: :class:`~.Message`
+ """
+ new_message = StanzaBase.reply(self, clear)
+
+ if self['type'] == 'groupchat':
+ new_message['to'] = new_message['to'].bare
+
+ new_message['thread'] = self['thread']
+ new_message['parent_thread'] = self['parent_thread']
+
+ del new_message['id']
+
+ if body is not None:
+ new_message['body'] = body
+ return new_message
+
+ def get_mucroom(self):
+ """
+ Return the name of the MUC room where the message originated.
+
+ Read-only stanza interface.
+
+ :rtype: str
+ """
+ if self['type'] == 'groupchat':
+ return self['from'].bare
+ else:
+ return ''
+
+ def get_mucnick(self):
+ """
+ Return the nickname of the MUC user that sent the message.
+
+ Read-only stanza interface.
+
+ :rtype: str
+ """
+ if self['type'] == 'groupchat':
+ return self['from'].resource
+ else:
+ return ''
+
+ def set_mucroom(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def del_mucroom(self):
+ """Dummy method to prevent deletion."""
+ pass
+
+ def set_mucnick(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def del_mucnick(self):
+ """Dummy method to prevent deletion."""
+ pass
diff --git a/slixmpp/stanza/nick.py b/slixmpp/stanza/nick.py
new file mode 100644
index 00000000..3bb7d63d
--- /dev/null
+++ b/slixmpp/stanza/nick.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+# The nickname stanza has been moved to its own plugin, but the existing
+# references are kept for backwards compatibility.
+
+from slixmpp.stanza import Message, Presence
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0172 import UserNick as Nick
+
+register_stanza_plugin(Message, Nick)
+register_stanza_plugin(Presence, Nick)
diff --git a/slixmpp/stanza/presence.py b/slixmpp/stanza/presence.py
new file mode 100644
index 00000000..1e8a940e
--- /dev/null
+++ b/slixmpp/stanza/presence.py
@@ -0,0 +1,173 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza.rootstanza import RootStanza
+from slixmpp.xmlstream import StanzaBase
+
+
+class Presence(RootStanza):
+
+ """
+ XMPP's <presence> stanza allows entities to know the status of other
+ clients and components. Since it is currently the only multi-cast
+ stanza in XMPP, many extensions add more information to <presence>
+ stanzas to broadcast to every entry in the roster, such as
+ capabilities, music choices, or locations (XEP-0115: Entity Capabilities
+ and XEP-0163: Personal Eventing Protocol).
+
+ Since <presence> stanzas are broadcast when an XMPP entity changes
+ its status, the bulk of the traffic in an XMPP network will be from
+ <presence> stanzas. Therefore, do not include more information than
+ necessary in a status message or within a <presence> stanza in order
+ to help keep the network running smoothly.
+
+ Example <presence> stanzas:
+
+ .. code-block:: xml
+
+ <presence />
+
+ <presence from="user@example.com">
+ <show>away</show>
+ <status>Getting lunch.</status>
+ <priority>5</priority>
+ </presence>
+
+ <presence type="unavailable" />
+
+ <presence to="user@otherhost.com" type="subscribe" />
+
+ Stanza Interface:
+ - **priority**: A value used by servers to determine message routing.
+ - **show**: The type of status, such as away or available for chat.
+ - **status**: Custom, human readable status message.
+
+ Attributes:
+ - **types**: One of: available, unavailable, error, probe,
+ subscribe, subscribed, unsubscribe, and unsubscribed.
+ - **showtypes**: One of: away, chat, dnd, and xa.
+ """
+
+ name = 'presence'
+ namespace = 'jabber:client'
+ plugin_attrib = name
+ interfaces = set(['type', 'to', 'from', 'id', 'show',
+ 'status', 'priority'])
+ sub_interfaces = set(['show', 'status', 'priority'])
+ lang_interfaces = set(['status'])
+
+ types = set(['available', 'unavailable', 'error', 'probe', 'subscribe',
+ 'subscribed', 'unsubscribe', 'unsubscribed'])
+ showtypes = set(['dnd', 'chat', 'xa', 'away'])
+
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize a new <presence /> stanza with an optional 'id' value.
+
+ Overrides StanzaBase.__init__.
+ """
+ StanzaBase.__init__(self, *args, **kwargs)
+ if self['id'] == '':
+ if self.stream is not None and self.stream.use_presence_ids:
+ self['id'] = self.stream.new_id()
+
+ def exception(self, e):
+ """
+ Override exception passback for presence.
+ """
+ pass
+
+ def set_show(self, show):
+ """
+ Set the value of the <show> element.
+
+ :param str show: Must be one of: away, chat, dnd, or xa.
+ """
+ if show is None:
+ self._del_sub('show')
+ elif show in self.showtypes:
+ self._set_sub_text('show', text=show)
+ return self
+
+ def get_type(self):
+ """
+ Return the value of the <presence> stanza's type attribute, or
+ the value of the <show> element.
+ """
+ out = self._get_attr('type')
+ if not out:
+ out = self['show']
+ if not out or out is None:
+ out = 'available'
+ return out
+
+ def set_type(self, value):
+ """
+ Set the type attribute's value, and the <show> element
+ if applicable.
+
+ :param str value: Must be in either self.types or self.showtypes.
+ """
+ if value in self.types:
+ self['show'] = None
+ if value == 'available':
+ value = ''
+ self._set_attr('type', value)
+ elif value in self.showtypes:
+ self['show'] = value
+ return self
+
+ def del_type(self):
+ """
+ Remove both the type attribute and the <show> element.
+ """
+ self._del_attr('type')
+ self._del_sub('show')
+
+ def set_priority(self, value):
+ """
+ Set the entity's priority value. Some server use priority to
+ determine message routing behavior.
+
+ Bot clients should typically use a priority of 0 if the same
+ JID is used elsewhere by a human-interacting client.
+
+ :param int value: An integer value greater than or equal to 0.
+ """
+ self._set_sub_text('priority', text=str(value))
+
+ def get_priority(self):
+ """
+ Return the value of the <presence> element as an integer.
+
+ :rtype: int
+ """
+ p = self._get_sub_text('priority')
+ if not p:
+ p = 0
+ try:
+ return int(p)
+ except ValueError:
+ # The priority is not a number: we consider it 0 as a default
+ return 0
+
+ def reply(self, clear=True):
+ """
+ Create a new reply <presence/> stanza from ``self``.
+
+ Overrides StanzaBase.reply.
+
+ :param bool clear: Indicates if the stanza contents should be removed
+ before replying. Defaults to True.
+ """
+ new_presence = StanzaBase.reply(self, clear)
+ if self['type'] == 'unsubscribe':
+ new_presence['type'] = 'unsubscribed'
+ elif self['type'] == 'subscribe':
+ new_presence['type'] = 'subscribed'
+ return new_presence
diff --git a/slixmpp/stanza/rootstanza.py b/slixmpp/stanza/rootstanza.py
new file mode 100644
index 00000000..a6dd958e
--- /dev/null
+++ b/slixmpp/stanza/rootstanza.py
@@ -0,0 +1,90 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from slixmpp.exceptions import XMPPError, IqError, IqTimeout
+from slixmpp.stanza import Error
+from slixmpp.xmlstream import ET, StanzaBase, register_stanza_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class RootStanza(StanzaBase):
+
+ """
+ A top-level XMPP stanza in an XMLStream.
+
+ The RootStanza class provides a more XMPP specific exception
+ handler than provided by the generic StanzaBase class.
+
+ Methods:
+ exception -- Overrides StanzaBase.exception
+ """
+
+ def exception(self, e):
+ """
+ Create and send an error reply.
+
+ Typically called when an event handler raises an exception.
+ The error's type and text content are based on the exception
+ object's type and content.
+
+ Overrides StanzaBase.exception.
+
+ Arguments:
+ e -- Exception object
+ """
+ if isinstance(e, IqError):
+ # We received an Iq error reply, but it wasn't caught
+ # locally. Using the condition/text from that error
+ # response could leak too much information, so we'll
+ # only use a generic error here.
+ reply = self.reply()
+ reply['error']['condition'] = 'undefined-condition'
+ reply['error']['text'] = 'External error'
+ reply['error']['type'] = 'cancel'
+ log.warning('You should catch IqError exceptions')
+ reply.send()
+ elif isinstance(e, IqTimeout):
+ reply = self.reply()
+ reply['error']['condition'] = 'remote-server-timeout'
+ reply['error']['type'] = 'wait'
+ log.warning('You should catch IqTimeout exceptions')
+ reply.send()
+ elif isinstance(e, XMPPError):
+ # We raised this deliberately
+ keep_id = self['id']
+ reply = self.reply(clear=e.clear)
+ reply['id'] = keep_id
+ reply['error']['condition'] = e.condition
+ reply['error']['text'] = e.text
+ reply['error']['type'] = e.etype
+ if e.extension is not None:
+ # Extended error tag
+ extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension),
+ e.extension_args)
+ reply['error'].append(extxml)
+ reply.send()
+ else:
+ # We probably didn't raise this on purpose, so send an error stanza
+ keep_id = self['id']
+ reply = self.reply()
+ reply['id'] = keep_id
+ reply['error']['condition'] = 'undefined-condition'
+ reply['error']['text'] = "Slixmpp got into trouble."
+ reply['error']['type'] = 'cancel'
+ reply.send()
+ # log the error
+ log.exception('Error handling {%s}%s stanza',
+ self.namespace, self.name)
+ # Finally raise the exception to a global exception handler
+ self.stream.exception(e)
+
+register_stanza_plugin(RootStanza, Error)
diff --git a/slixmpp/stanza/roster.py b/slixmpp/stanza/roster.py
new file mode 100644
index 00000000..c017c33f
--- /dev/null
+++ b/slixmpp/stanza/roster.py
@@ -0,0 +1,152 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Iq
+from slixmpp.xmlstream import JID
+from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Roster(ElementBase):
+
+ """
+ Example roster stanzas:
+ <iq type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" subscription="both" name="User">
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Inteface:
+ items -- A dictionary of roster entries contained
+ in the stanza.
+
+ Methods:
+ get_items -- Return a dictionary of roster entries.
+ set_items -- Add <item> elements.
+ del_items -- Remove all <item> elements.
+ """
+
+ namespace = 'jabber:iq:roster'
+ name = 'query'
+ plugin_attrib = 'roster'
+ interfaces = set(('items', 'ver'))
+
+ def get_ver(self):
+ """
+ Ensure handling an empty ver attribute propery.
+
+ The ver attribute is special in that the presence of the
+ attribute with an empty value is important for boostrapping
+ roster versioning.
+ """
+ return self.xml.attrib.get('ver', None)
+
+ def set_ver(self, ver):
+ """
+ Ensure handling an empty ver attribute propery.
+
+ The ver attribute is special in that the presence of the
+ attribute with an empty value is important for boostrapping
+ roster versioning.
+ """
+ if ver is not None:
+ self.xml.attrib['ver'] = ver
+ else:
+ del self.xml.attrib['ver']
+
+ def set_items(self, items):
+ """
+ Set the roster entries in the <roster> stanza.
+
+ Uses a dictionary using JIDs as keys, where each entry is itself
+ a dictionary that contains:
+ name -- An alias or nickname for the JID.
+ subscription -- The subscription type. Can be one of 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names to which the JID
+ has been assigned.
+
+ Arguments:
+ items -- A dictionary of roster entries.
+ """
+ self.del_items()
+ for jid in items:
+ item = RosterItem()
+ item.values = items[jid]
+ item['jid'] = jid
+ self.append(item)
+ return self
+
+ def get_items(self):
+ """
+ Return a dictionary of roster entries.
+
+ Each item is keyed using its JID, and contains:
+ name -- An assigned alias or nickname for the JID.
+ subscription -- The subscription type. Can be one of 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names to which the JID has
+ been assigned.
+ """
+ items = {}
+ for item in self['substanzas']:
+ if isinstance(item, RosterItem):
+ items[item['jid']] = item.values
+ # Remove extra JID reference to keep everything
+ # backward compatible
+ del items[item['jid']]['jid']
+ del items[item['jid']]['lang']
+ return items
+
+ def del_items(self):
+ """
+ Remove all <item> elements from the roster stanza.
+ """
+ for item in self['substanzas']:
+ if isinstance(item, RosterItem):
+ self.xml.remove(item.xml)
+
+
+class RosterItem(ElementBase):
+ namespace = 'jabber:iq:roster'
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = set(('jid', 'name', 'subscription', 'ask',
+ 'approved', 'groups'))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid', ''))
+
+ def set_jid(self, jid):
+ self._set_attr('jid', str(jid))
+
+ def get_groups(self):
+ groups = []
+ for group in self.xml.findall('{%s}group' % self.namespace):
+ if group.text:
+ groups.append(group.text)
+ else:
+ groups.append('')
+ return groups
+
+ def set_groups(self, values):
+ self.del_groups()
+ for group in values:
+ group_xml = ET.Element('{%s}group' % self.namespace)
+ group_xml.text = group
+ self.xml.append(group_xml)
+
+ def del_groups(self):
+ for group in self.xml.findall('{%s}group' % self.namespace):
+ self.xml.remove(group)
+
+
+register_stanza_plugin(Iq, Roster)
+register_stanza_plugin(Roster, RosterItem, iterable=True)
diff --git a/slixmpp/stanza/stream_error.py b/slixmpp/stanza/stream_error.py
new file mode 100644
index 00000000..d8b8bb5a
--- /dev/null
+++ b/slixmpp/stanza/stream_error.py
@@ -0,0 +1,83 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza.error import Error
+from slixmpp.xmlstream import StanzaBase
+
+
+class StreamError(Error, StanzaBase):
+
+ """
+ XMPP stanzas of type 'error' should include an <error> stanza that
+ describes the nature of the error and how it should be handled.
+
+ Use the 'XEP-0086: Error Condition Mappings' plugin to include error
+ codes used in older XMPP versions.
+
+ The stream:error stanza is used to provide more information for
+ error that occur with the underlying XML stream itself, and not
+ a particular stanza.
+
+ Note: The StreamError stanza is mostly the same as the normal
+ Error stanza, but with different namespaces and
+ condition names.
+
+ Example error stanza:
+ <stream:error>
+ <not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-streams">
+ XML was not well-formed.
+ </text>
+ </stream:error>
+
+ Stanza Interface:
+ condition -- The name of the condition element.
+ text -- Human readable description of the error.
+
+ Attributes:
+ conditions -- The set of allowable error condition elements.
+ condition_ns -- The namespace for the condition element.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_condition -- Retrieve the name of the condition element.
+ set_condition -- Add a condition element.
+ del_condition -- Remove the condition element.
+ get_text -- Retrieve the contents of the <text> element.
+ set_text -- Set the contents of the <text> element.
+ del_text -- Remove the <text> element.
+ """
+
+ namespace = 'http://etherx.jabber.org/streams'
+ interfaces = set(('condition', 'text', 'see_other_host'))
+ conditions = set((
+ 'bad-format', 'bad-namespace-prefix', 'conflict',
+ 'connection-timeout', 'host-gone', 'host-unknown',
+ 'improper-addressing', 'internal-server-error', 'invalid-from',
+ 'invalid-namespace', 'invalid-xml', 'not-authorized',
+ 'not-well-formed', 'policy-violation', 'remote-connection-failed',
+ 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host',
+ 'system-shutdown', 'undefined-condition', 'unsupported-encoding',
+ 'unsupported-feature', 'unsupported-stanza-type',
+ 'unsupported-version'))
+ condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams'
+
+ def get_see_other_host(self):
+ ns = self.condition_ns
+ return self._get_sub_text('{%s}see-other-host' % ns, '')
+
+ def set_see_other_host(self, value):
+ if value:
+ del self['condition']
+ ns = self.condition_ns
+ return self._set_sub_text('{%s}see-other-host' % ns, value)
+ elif self['condition'] == 'see-other-host':
+ del self['condition']
+
+ def del_see_other_host(self):
+ self._del_sub('{%s}see-other-host' % self.condition_ns)
diff --git a/slixmpp/stanza/stream_features.py b/slixmpp/stanza/stream_features.py
new file mode 100644
index 00000000..05788771
--- /dev/null
+++ b/slixmpp/stanza/stream_features.py
@@ -0,0 +1,57 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from collections import OrderedDict
+from slixmpp.xmlstream import StanzaBase
+
+
+class StreamFeatures(StanzaBase):
+
+ """
+ """
+
+ name = 'features'
+ namespace = 'http://etherx.jabber.org/streams'
+ interfaces = set(('features', 'required', 'optional'))
+ sub_interfaces = interfaces
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.values = self.values
+
+ def get_features(self):
+ """
+ """
+ features = OrderedDict()
+ for (name, lang), plugin in self.plugins.items():
+ features[name] = plugin
+ return features
+
+ def set_features(self, value):
+ """
+ """
+ pass
+
+ def del_features(self):
+ """
+ """
+ pass
+
+ def get_required(self):
+ """
+ """
+ features = self['features']
+ return [f for n, f in features.items() if f['required']]
+
+ def get_optional(self):
+ """
+ """
+ features = self['features']
+ return [f for n, f in features.items() if not f['required']]
diff --git a/slixmpp/stringprep.py b/slixmpp/stringprep.py
new file mode 100644
index 00000000..99506d78
--- /dev/null
+++ b/slixmpp/stringprep.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.stringprep
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module is a fallback using python’s stringprep instead of libidn’s.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2015 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+import stringprep
+from slixmpp.util import stringprep_profiles
+import encodings.idna
+
+class StringprepError(Exception):
+ pass
+
+#: These characters are not allowed to appear in a domain part.
+ILLEGAL_CHARS = ('\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r'
+ '\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19'
+ '\x1a\x1b\x1c\x1d\x1e\x1f'
+ ' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f')
+
+
+# pylint: disable=c0103
+#: The nodeprep profile of stringprep used to validate the local,
+#: or username, portion of a JID.
+_nodeprep = stringprep_profiles.create(
+ nfkc=True,
+ bidi=True,
+ mappings=[
+ stringprep_profiles.b1_mapping,
+ stringprep.map_table_b2],
+ prohibited=[
+ stringprep.in_table_c11,
+ stringprep.in_table_c12,
+ stringprep.in_table_c21,
+ stringprep.in_table_c22,
+ stringprep.in_table_c3,
+ stringprep.in_table_c4,
+ stringprep.in_table_c5,
+ stringprep.in_table_c6,
+ stringprep.in_table_c7,
+ stringprep.in_table_c8,
+ stringprep.in_table_c9,
+ lambda c: c in ' \'"&/:<>@'],
+ unassigned=[stringprep.in_table_a1])
+
+def nodeprep(node):
+ try:
+ return _nodeprep(node)
+ except stringprep_profiles.StringPrepError:
+ raise StringprepError
+
+# pylint: disable=c0103
+#: The resourceprep profile of stringprep, which is used to validate
+#: the resource portion of a JID.
+_resourceprep = stringprep_profiles.create(
+ nfkc=True,
+ bidi=True,
+ mappings=[stringprep_profiles.b1_mapping],
+ prohibited=[
+ stringprep.in_table_c12,
+ stringprep.in_table_c21,
+ stringprep.in_table_c22,
+ stringprep.in_table_c3,
+ stringprep.in_table_c4,
+ stringprep.in_table_c5,
+ stringprep.in_table_c6,
+ stringprep.in_table_c7,
+ stringprep.in_table_c8,
+ stringprep.in_table_c9],
+ unassigned=[stringprep.in_table_a1])
+
+def resourceprep(resource):
+ try:
+ return _resourceprep(resource)
+ except stringprep_profiles.StringPrepError:
+ raise StringprepError
+
+def idna(domain):
+ domain_parts = []
+ for label in domain.split('.'):
+ try:
+ label = encodings.idna.nameprep(label)
+ encodings.idna.ToASCII(label)
+ except UnicodeError:
+ raise StringprepError
+
+ if label.startswith('xn--'):
+ label = encodings.idna.ToUnicode(label)
+
+ for char in label:
+ if char in ILLEGAL_CHARS:
+ raise StringprepError
+
+ domain_parts.append(label)
+ return '.'.join(domain_parts)
+
+def punycode(domain):
+ domain_parts = []
+ for label in domain.split('.'):
+ try:
+ label = encodings.idna.nameprep(label)
+ encodings.idna.ToASCII(label)
+ except UnicodeError:
+ raise StringprepError
+
+ for char in label:
+ if char in ILLEGAL_CHARS:
+ raise StringprepError
+
+ domain_parts.append(label)
+ return b'.'.join(domain_parts)
+
+logging.getLogger(__name__).warning('Using slower stringprep, consider '
+ 'compiling the faster cython/libidn one.')
diff --git a/slixmpp/stringprep.pyx b/slixmpp/stringprep.pyx
new file mode 100644
index 00000000..e751c8ea
--- /dev/null
+++ b/slixmpp/stringprep.pyx
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# cython: language_level = 3
+# distutils: libraries = idn
+"""
+ slixmpp.stringprep
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module wraps libidn’s stringprep and idna functions using Cython.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2015 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ :license: MIT, see LICENSE for more details
+"""
+
+from libc.stdlib cimport free
+
+
+# Those are Cython declarations for the C function we’ll be using.
+
+cdef extern from "stringprep.h" nogil:
+ int stringprep_profile(const char* in_, char** out, const char* profile,
+ int flags)
+
+cdef extern from "idna.h" nogil:
+ int idna_to_ascii_8z(const char* in_, char** out, int flags)
+ int idna_to_unicode_8z8z(const char* in_, char** out, int flags)
+
+
+class StringprepError(Exception):
+ pass
+
+
+cdef str _stringprep(str in_, const char* profile):
+ """Python wrapper for libidn’s stringprep."""
+ cdef char* out
+ ret = stringprep_profile(in_.encode('utf-8'), &out, profile, 0)
+ if ret != 0:
+ raise StringprepError(ret)
+ unicode_out = out.decode('utf-8')
+ free(out)
+ return unicode_out
+
+
+def nodeprep(str node):
+ """The nodeprep profile of stringprep used to validate the local, or
+ username, portion of a JID."""
+ return _stringprep(node, 'Nodeprep')
+
+
+def resourceprep(str resource):
+ """The resourceprep profile of stringprep, which is used to validate the
+ resource portion of a JID."""
+ return _stringprep(resource, 'Resourceprep')
+
+
+def idna(str domain):
+ """The idna conversion functions, which are used to validate the domain
+ portion of a JID."""
+
+ cdef char* ascii_domain
+ cdef char* utf8_domain
+
+ ret = idna_to_ascii_8z(domain.encode('utf-8'), &ascii_domain, 0)
+ if ret != 0:
+ raise StringprepError(ret)
+
+ ret = idna_to_unicode_8z8z(ascii_domain, &utf8_domain, 0)
+ free(ascii_domain)
+ if ret != 0:
+ raise StringprepError(ret)
+
+ unicode_domain = utf8_domain.decode('utf-8')
+ free(utf8_domain)
+ return unicode_domain
+
+
+def punycode(str domain):
+ """Converts a domain name to its punycode representation."""
+
+ cdef char* ascii_domain
+ cdef bytes bytes_domain
+
+ ret = idna_to_ascii_8z(domain.encode('utf-8'), &ascii_domain, 0)
+ if ret != 0:
+ raise StringprepError(ret)
+ bytes_domain = ascii_domain
+ free(ascii_domain)
+ return bytes_domain
diff --git a/slixmpp/test/__init__.py b/slixmpp/test/__init__.py
new file mode 100644
index 00000000..0244afe3
--- /dev/null
+++ b/slixmpp/test/__init__.py
@@ -0,0 +1,11 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.test.mocksocket import TestSocket, TestTransport
+from slixmpp.test.livesocket import TestLiveSocket
+from slixmpp.test.slixtest import *
diff --git a/slixmpp/test/livesocket.py b/slixmpp/test/livesocket.py
new file mode 100644
index 00000000..e7deb617
--- /dev/null
+++ b/slixmpp/test/livesocket.py
@@ -0,0 +1,171 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import socket
+import threading
+from queue import Queue
+
+
+class TestLiveSocket(object):
+
+ """
+ A live test socket that reads and writes to queues in
+ addition to an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ next_recv -- Return the next received stanza.
+ recv_data -- Dummy method to have same interface as TestSocket.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a new, live test socket.
+
+ Arguments:
+ Same as arguments for socket.socket
+ """
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.recv_buffer = []
+ self.recv_queue = Queue()
+ self.send_queue = Queue()
+ self.send_queue_lock = threading.Lock()
+ self.recv_queue_lock = threading.Lock()
+ self.is_live = True
+
+ def __getattr__(self, name):
+ """
+ Return attribute values of internal, live socket.
+
+ Arguments:
+ name -- Name of the attribute requested.
+ """
+
+ return getattr(self.socket, name)
+
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def disconnect_errror(self):
+ """
+ Used to simulate a socket disconnection error.
+
+ Not used by live sockets.
+ """
+ try:
+ self.socket.shutdown()
+ self.socket.close()
+ except:
+ pass
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been sent.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ return self.send_queue.get(**args)
+ except:
+ return None
+
+ def next_recv(self, timeout=None):
+ """
+ Get the next stanza that has been received.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ if self.recv_buffer:
+ return self.recv_buffer.pop(0)
+ else:
+ return self.recv_queue.get(**args)
+ except:
+ return None
+
+ def recv_data(self, data):
+ """
+ Add data to a receive buffer for cases when more than a single stanza
+ was received.
+ """
+ self.recv_buffer.append(data)
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read data from the socket.
+
+ Store a copy in the receive queue.
+
+ Arguments:
+ Placeholders. Same as for socket.recv.
+ """
+ data = self.socket.recv(*args, **kwargs)
+ with self.recv_queue_lock:
+ self.recv_queue.put(data)
+ return data
+
+ def send(self, data):
+ """
+ Send data on the socket.
+
+ Store a copy in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ with self.send_queue_lock:
+ self.send_queue.put(data)
+ return self.socket.send(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.makefile()
+ """
+ return self
+
+ def read(self, *args, **kwargs):
+ """
+ Implement the file socket read interface.
+
+ Arguments:
+ Placeholders, same as socket.recv()
+ """
+ return self.recv(*args, **kwargs)
+
+ def clear(self):
+ """
+ Empty the send queue, typically done once the session has started to
+ remove the feature negotiation and log in stanzas.
+ """
+ with self.send_queue_lock:
+ for i in range(0, self.send_queue.qsize()):
+ self.send_queue.get(block=False)
+ with self.recv_queue_lock:
+ for i in range(0, self.recv_queue.qsize()):
+ self.recv_queue.get(block=False)
diff --git a/slixmpp/test/mocksocket.py b/slixmpp/test/mocksocket.py
new file mode 100644
index 00000000..149df29c
--- /dev/null
+++ b/slixmpp/test/mocksocket.py
@@ -0,0 +1,245 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import socket
+from queue import Queue
+
+
+class TestSocket(object):
+
+ """
+ A dummy socket that reads and writes to queues instead
+ of an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ recv_data -- Make a stanza available to read next.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a new test socket.
+
+ Arguments:
+ Same as arguments for socket.socket
+ """
+ self.socket = socket.socket(*args, **kwargs)
+ self.recv_queue = Queue()
+ self.send_queue = Queue()
+ self.is_live = False
+ self.disconnected = False
+
+ def __getattr__(self, name):
+ """
+ Return attribute values of internal, dummy socket.
+
+ Some attributes and methods are disabled to prevent the
+ socket from connecting to the network.
+
+ Arguments:
+ name -- Name of the attribute requested.
+ """
+
+ def dummy(*args):
+ """Method to do nothing and prevent actual socket connections."""
+ return None
+
+ overrides = {'connect': dummy,
+ 'close': dummy,
+ 'shutdown': dummy}
+
+ return overrides.get(name, getattr(self.socket, name))
+
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been 'sent'.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ return self.send_queue.get(**args)
+ except:
+ return None
+
+ def recv_data(self, data):
+ """
+ Add data to the receiving queue.
+
+ Arguments:
+ data -- String data to 'write' to the socket to be received
+ by the XMPP client.
+ """
+ self.recv_queue.put(data)
+
+ def disconnect_error(self):
+ """
+ Simulate a disconnect error by raising a socket.error exception
+ for any current or further socket operations.
+ """
+ self.disconnected = True
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read a value from the received queue.
+
+ Arguments:
+ Placeholders. Same as for socket.Socket.recv.
+ """
+ if self.disconnected:
+ raise socket.error
+ return self.read(block=True)
+
+ def send(self, data):
+ """
+ Send data by placing it in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ if self.disconnected:
+ raise socket.error
+ self.send_queue.put(data)
+ return len(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.Socket.makefile()
+ """
+ return self
+
+ def read(self, block=True, timeout=None, **kwargs):
+ """
+ Implement the file socket interface.
+
+ Arguments:
+ block -- Indicate if the read should block until a
+ value is ready.
+ timeout -- Time in seconds a block should last before
+ returning None.
+ """
+ if self.disconnected:
+ raise socket.error
+ if timeout is not None:
+ block = True
+ try:
+ return self.recv_queue.get(block, timeout)
+ except:
+ return None
+
+class TestTransport(object):
+
+ """
+ A transport socket that reads and writes to queues instead
+ of an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ recv_data -- Make a stanza available to read next.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, xmpp):
+ self.xmpp = xmpp
+ self.socket = TestSocket()
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been 'sent'.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ return self.socket.next_sent()
+
+ def disconnect_error(self):
+ """
+ Simulate a disconnect error by raising a socket.error exception
+ for any current or further socket operations.
+ """
+ self.socket.disconnect_error()
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read a value from the received queue.
+
+ Arguments:
+ Placeholders. Same as for socket.Socket.recv.
+ """
+ return
+
+ def write(self, data):
+ """
+ Send data by placing it in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ self.socket.send(data)
+ return len(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.Socket.makefile()
+ """
+ return self
+
+ def read(self, block=True, timeout=None, **kwargs):
+ """
+ Implement the file socket interface.
+
+ Arguments:
+ block -- Indicate if the read should block until a
+ value is ready.
+ timeout -- Time in seconds a block should last before
+ returning None.
+ """
+ return self.socket.recv(block, timeout, **kwargs)
+
+ def get_extra_info(self, *args, **kwargs):
+ return self.socket
+
+ def abort(self, *args, **kwargs):
+ return
+
+ def close(self, *args, **kwargs):
+ return
+
diff --git a/slixmpp/test/slixtest.py b/slixmpp/test/slixtest.py
new file mode 100644
index 00000000..f66cf6be
--- /dev/null
+++ b/slixmpp/test/slixtest.py
@@ -0,0 +1,710 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import unittest
+from queue import Queue
+from xml.parsers.expat import ExpatError
+
+from slixmpp.test import TestTransport
+from slixmpp import ClientXMPP, ComponentXMPP
+from slixmpp.stanza import Message, Iq, Presence
+from slixmpp.xmlstream import ET
+from slixmpp.xmlstream import ElementBase
+from slixmpp.xmlstream.tostring import tostring, highlight
+from slixmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender
+from slixmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
+
+import asyncio
+cls = asyncio.get_event_loop().__class__
+
+cls.idle_call = lambda self, callback: callback()
+
+class SlixTest(unittest.TestCase):
+
+ """
+ A Slixmpp specific TestCase class that provides
+ methods for comparing message, iq, and presence stanzas.
+
+ Methods:
+ Message -- Create a Message stanza object.
+ Iq -- Create an Iq stanza object.
+ Presence -- Create a Presence stanza object.
+ check_jid -- Check a JID and its component parts.
+ check -- Compare a stanza against an XML string.
+ stream_start -- Initialize a dummy XMPP client.
+ stream_close -- Disconnect the XMPP client.
+ make_header -- Create a stream header.
+ send_header -- Check that the given header has been sent.
+ send_feature -- Send a raw XML element.
+ send -- Check that the XMPP client sent the given
+ generic stanza.
+ recv -- Queue data for XMPP client to receive, or
+ verify the data that was received from a
+ live connection.
+ recv_header -- Check that a given stream header
+ was received.
+ recv_feature -- Check that a given, raw XML element
+ was recveived.
+ fix_namespaces -- Add top-level namespace to an XML object.
+ compare -- Compare XML objects against each other.
+ """
+
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ self.xmpp = None
+
+ def parse_xml(self, xml_string):
+ try:
+ xml = ET.fromstring(xml_string)
+ return xml
+ except (SyntaxError, ExpatError) as e:
+ msg = e.msg if hasattr(e, 'msg') else e.message
+ if 'unbound' in msg:
+ known_prefixes = {
+ 'stream': 'http://etherx.jabber.org/streams'}
+
+ prefix = xml_string.split('<')[1].split(':')[0]
+ if prefix in known_prefixes:
+ xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
+ prefix,
+ known_prefixes[prefix],
+ xml_string)
+ xml = self.parse_xml(xml_string)
+ xml = list(xml)[0]
+ return xml
+ else:
+ self.fail("XML data was mal-formed:\n%s" % xml_string)
+
+ # ------------------------------------------------------------------
+ # Shortcut methods for creating stanza objects
+
+ def Message(self, *args, **kwargs):
+ """
+ Create a Message stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Message's values.
+ """
+ return Message(self.xmpp, *args, **kwargs)
+
+ def Iq(self, *args, **kwargs):
+ """
+ Create an Iq stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Iq's values.
+ """
+ return Iq(self.xmpp, *args, **kwargs)
+
+ def Presence(self, *args, **kwargs):
+ """
+ Create a Presence stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Iq's values.
+ """
+ return Presence(self.xmpp, *args, **kwargs)
+
+ def check_jid(self, jid, user=None, domain=None, resource=None,
+ bare=None, full=None, string=None):
+ """
+ Verify the components of a JID.
+
+ Arguments:
+ jid -- The JID object to test.
+ user -- Optional. The user name portion of the JID.
+ domain -- Optional. The domain name portion of the JID.
+ resource -- Optional. The resource portion of the JID.
+ bare -- Optional. The bare JID.
+ full -- Optional. The full JID.
+ string -- Optional. The string version of the JID.
+ """
+ if user is not None:
+ self.assertEqual(jid.user, user,
+ "User does not match: %s" % jid.user)
+ if domain is not None:
+ self.assertEqual(jid.domain, domain,
+ "Domain does not match: %s" % jid.domain)
+ if resource is not None:
+ self.assertEqual(jid.resource, resource,
+ "Resource does not match: %s" % jid.resource)
+ if bare is not None:
+ self.assertEqual(jid.bare, bare,
+ "Bare JID does not match: %s" % jid.bare)
+ if full is not None:
+ self.assertEqual(jid.full, full,
+ "Full JID does not match: %s" % jid.full)
+ if string is not None:
+ self.assertEqual(str(jid), string,
+ "String does not match: %s" % str(jid))
+
+ def check_roster(self, owner, jid, name=None, subscription=None,
+ afrom=None, ato=None, pending_out=None, pending_in=None,
+ groups=None):
+ roster = self.xmpp.roster[owner][jid]
+ if name is not None:
+ self.assertEqual(roster['name'], name,
+ "Incorrect name value: %s" % roster['name'])
+ if subscription is not None:
+ self.assertEqual(roster['subscription'], subscription,
+ "Incorrect subscription: %s" % roster['subscription'])
+ if afrom is not None:
+ self.assertEqual(roster['from'], afrom,
+ "Incorrect from state: %s" % roster['from'])
+ if ato is not None:
+ self.assertEqual(roster['to'], ato,
+ "Incorrect to state: %s" % roster['to'])
+ if pending_out is not None:
+ self.assertEqual(roster['pending_out'], pending_out,
+ "Incorrect pending_out state: %s" % roster['pending_out'])
+ if pending_in is not None:
+ self.assertEqual(roster['pending_in'], pending_out,
+ "Incorrect pending_in state: %s" % roster['pending_in'])
+ if groups is not None:
+ self.assertEqual(roster['groups'], groups,
+ "Incorrect groups: %s" % roster['groups'])
+
+ # ------------------------------------------------------------------
+ # Methods for comparing stanza objects to XML strings
+
+ def check(self, stanza, criteria, method='exact',
+ defaults=None, use_values=True):
+ """
+ Create and compare several stanza objects to a correct XML string.
+
+ If use_values is False, tests using stanza.values will not be used.
+
+ Some stanzas provide default values for some interfaces, but
+ these defaults can be problematic for testing since they can easily
+ be forgotten when supplying the XML string. A list of interfaces that
+ use defaults may be provided and the generated stanzas will use the
+ default values for those interfaces if needed.
+
+ However, correcting the supplied XML is not possible for interfaces
+ that add or remove XML elements. Only interfaces that map to XML
+ attributes may be set using the defaults parameter. The supplied XML
+ must take into account any extra elements that are included by default.
+
+ Arguments:
+ stanza -- The stanza object to test.
+ criteria -- An expression the stanza must match against.
+ method -- The type of matching to use; one of:
+ 'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
+ Defaults to the value of self.match_method.
+ defaults -- A list of stanza interfaces that have default
+ values. These interfaces will be set to their
+ defaults for the given and generated stanzas to
+ prevent unexpected test failures.
+ use_values -- Indicates if testing using stanza.values should
+ be used. Defaults to True.
+ """
+ if method is None and hasattr(self, 'match_method'):
+ method = getattr(self, 'match_method')
+
+ if method != 'exact':
+ matchers = {'stanzapath': StanzaPath,
+ 'xpath': MatchXPath,
+ 'mask': MatchXMLMask,
+ 'idsender': MatchIDSender,
+ 'id': MatcherId}
+ Matcher = matchers.get(method, None)
+ if Matcher is None:
+ raise ValueError("Unknown matching method.")
+ test = Matcher(criteria)
+ self.failUnless(test.match(stanza),
+ "Stanza did not match using %s method:\n" % method + \
+ "Criteria:\n%s\n" % str(criteria) + \
+ "Stanza:\n%s" % str(stanza))
+ else:
+ stanza_class = stanza.__class__
+ if not isinstance(criteria, ElementBase):
+ xml = self.parse_xml(criteria)
+ else:
+ xml = criteria.xml
+
+ # Ensure that top level namespaces are used, even if they
+ # were not provided.
+ self.fix_namespaces(stanza.xml, 'jabber:client')
+ self.fix_namespaces(xml, 'jabber:client')
+
+ stanza2 = stanza_class(xml=xml)
+
+ if use_values:
+ # Using stanza.values will add XML for any interface that
+ # has a default value. We need to set those defaults on
+ # the existing stanzas and XML so that they will compare
+ # correctly.
+ default_stanza = stanza_class()
+ if defaults is None:
+ known_defaults = {
+ Message: ['type'],
+ Presence: ['priority']
+ }
+ defaults = known_defaults.get(stanza_class, [])
+ for interface in defaults:
+ stanza[interface] = stanza[interface]
+ stanza2[interface] = stanza2[interface]
+ # Can really only automatically add defaults for top
+ # level attribute values. Anything else must be accounted
+ # for in the provided XML string.
+ if interface not in xml.attrib:
+ if interface in default_stanza.xml.attrib:
+ value = default_stanza.xml.attrib[interface]
+ xml.attrib[interface] = value
+
+ values = stanza2.values
+ stanza3 = stanza_class()
+ stanza3.values = values
+
+ debug = "Three methods for creating stanzas do not match.\n"
+ debug += "Given XML:\n%s\n" % highlight(tostring(xml))
+ debug += "Given stanza:\n%s\n" % highlight(tostring(stanza.xml))
+ debug += "Generated stanza:\n%s\n" % highlight(tostring(stanza2.xml))
+ debug += "Second generated stanza:\n%s\n" % highlight(tostring(stanza3.xml))
+ result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
+ else:
+ debug = "Two methods for creating stanzas do not match.\n"
+ debug += "Given XML:\n%s\n" % highlight(tostring(xml))
+ debug += "Given stanza:\n%s\n" % highlight(tostring(stanza.xml))
+ debug += "Generated stanza:\n%s\n" % highlight(tostring(stanza2.xml))
+ result = self.compare(xml, stanza.xml, stanza2.xml)
+
+ self.failUnless(result, debug)
+
+ # ------------------------------------------------------------------
+ # Methods for simulating stanza streams.
+
+ def stream_disconnect(self):
+ """
+ Simulate a stream disconnection.
+ """
+ if self.xmpp:
+ self.xmpp.socket.disconnect_error()
+
+ def stream_start(self, mode='client', skip=True, header=None,
+ socket='mock', jid='tester@localhost/resource',
+ password='test', server='localhost',
+ port=5222, sasl_mech=None,
+ plugins=None, plugin_config={}):
+ """
+ Initialize an XMPP client or component using a dummy XML stream.
+
+ Arguments:
+ mode -- Either 'client' or 'component'. Defaults to 'client'.
+ skip -- Indicates if the first item in the sent queue (the
+ stream header) should be removed. Tests that wish
+ to test initializing the stream should set this to
+ False. Otherwise, the default of True should be used.
+ socket -- Either 'mock' or 'live' to indicate if the socket
+ should be a dummy, mock socket or a live, functioning
+ socket. Defaults to 'mock'.
+ jid -- The JID to use for the connection.
+ Defaults to 'tester@localhost/resource'.
+ password -- The password to use for the connection.
+ Defaults to 'test'.
+ server -- The name of the XMPP server. Defaults to 'localhost'.
+ port -- The port to use when connecting to the server.
+ Defaults to 5222.
+ plugins -- List of plugins to register. By default, all plugins
+ are loaded.
+ """
+ if not plugin_config:
+ plugin_config = {}
+
+ if mode == 'client':
+ self.xmpp = ClientXMPP(jid, password,
+ sasl_mech=sasl_mech,
+ plugin_config=plugin_config)
+ elif mode == 'component':
+ self.xmpp = ComponentXMPP(jid, password,
+ server, port,
+ plugin_config=plugin_config)
+ else:
+ raise ValueError("Unknown XMPP connection mode.")
+
+ self.xmpp.connection_made(TestTransport(self.xmpp))
+ self.xmpp.session_bind_event.set()
+ # Remove unique ID prefix to make it easier to test
+ self.xmpp._id_prefix = ''
+ self.xmpp.default_lang = None
+ self.xmpp.peer_default_lang = None
+
+ # Simulate connecting for mock sockets.
+ self.xmpp.auto_reconnect = False
+
+ # Must have the stream header ready for xmpp.process() to work.
+ if not header:
+ header = self.xmpp.stream_header
+
+ self.xmpp.data_received(header)
+
+ if skip:
+ self.xmpp.socket.next_sent()
+ if mode == 'component':
+ self.xmpp.socket.next_sent()
+
+
+ if plugins is None:
+ self.xmpp.register_plugins()
+ else:
+ for plugin in plugins:
+ self.xmpp.register_plugin(plugin)
+
+ # Some plugins require messages to have ID values. Set
+ # this to True in tests related to those plugins.
+ self.xmpp.use_message_ids = False
+
+ def make_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ default_lang="en",
+ version="1.0",
+ xml_header=True):
+ """
+ Create a stream header to be received by the test XMPP agent.
+
+ The header must be saved and passed to stream_start.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ """
+ header = '<stream:stream %s>'
+ parts = []
+ if xml_header:
+ header = '<?xml version="1.0"?>' + header
+ if sto:
+ parts.append('to="%s"' % sto)
+ if sfrom:
+ parts.append('from="%s"' % sfrom)
+ if sid:
+ parts.append('id="%s"' % sid)
+ if default_lang:
+ parts.append('xml:lang="%s"' % default_lang)
+ parts.append('version="%s"' % version)
+ parts.append('xmlns:stream="%s"' % stream_ns)
+ parts.append('xmlns="%s"' % default_ns)
+ return header % ' '.join(parts)
+
+ def recv(self, data, defaults=None, method='exact', use_values=True, timeout=1):
+ """
+ Pass data to the dummy XMPP client as if it came from an XMPP server.
+
+ If using a live connection, verify what the server has sent.
+
+ Arguments:
+ data -- If a dummy socket is being used, the XML that is to
+ be received next. Otherwise it is the criteria used
+ to match against live data that is received.
+ defaults -- A list of stanza interfaces with default values that
+ may interfere with comparisons.
+ method -- Select the type of comparison to use for
+ verifying the received stanza. Options are 'exact',
+ 'id', 'stanzapath', 'xpath', and 'mask'.
+ Defaults to the value of self.match_method.
+ use_values -- Indicates if stanza comparisons should test using
+ stanza.values. Defaults to True.
+ timeout -- Time to wait in seconds for data to be received by
+ a live connection.
+ """
+ self.xmpp.data_received(data)
+
+ def recv_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=False,
+ timeout=1):
+ """
+ Check that a given stream header was received.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id. Set to None to ignore.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ timeout -- Length of time to wait in seconds for a
+ response.
+ """
+ header = self.make_header(sto, sfrom, sid,
+ stream_ns=stream_ns,
+ default_ns=default_ns,
+ version=version,
+ xml_header=xml_header)
+ recv_header = self.xmpp.socket.next_recv(timeout)
+ if recv_header is None:
+ raise ValueError("Socket did not return data.")
+
+ # Apply closing elements so that we can construct
+ # XML objects for comparison.
+ header2 = header + '</stream:stream>'
+ recv_header2 = recv_header + '</stream:stream>'
+
+ xml = self.parse_xml(header2)
+ recv_xml = self.parse_xml(recv_header2)
+
+ if sid is None:
+ # Ignore the id sent by the server since
+ # we can't know in advance what it will be.
+ if 'id' in recv_xml.attrib:
+ del recv_xml.attrib['id']
+
+ # Ignore the xml:lang attribute for now.
+ if 'xml:lang' in recv_xml.attrib:
+ del recv_xml.attrib['xml:lang']
+ xml_ns = 'http://www.w3.org/XML/1998/namespace'
+ if '{%s}lang' % xml_ns in recv_xml.attrib:
+ del recv_xml.attrib['{%s}lang' % xml_ns]
+
+ if list(recv_xml):
+ # We received more than just the header
+ for xml in recv_xml:
+ self.xmpp.data_received(tostring(xml))
+
+ attrib = recv_xml.attrib
+ recv_xml.clear()
+ recv_xml.attrib = attrib
+
+ self.failUnless(
+ self.compare(xml, recv_xml),
+ "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
+ '%s %s' % (xml.tag, xml.attrib),
+ '%s %s' % (recv_xml.tag, recv_xml.attrib)))
+
+ def recv_feature(self, data, method='mask', use_values=True, timeout=1):
+ """
+ """
+ if method is None and hasattr(self, 'match_method'):
+ method = getattr(self, 'match_method')
+
+ self.xmpp.socket.data_received(data)
+
+ def send_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ default_lang="en",
+ version="1.0",
+ xml_header=False,
+ timeout=1):
+ """
+ Check that a given stream header was sent.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ timeout -- Length of time to wait in seconds for a
+ response.
+ """
+ header = self.make_header(sto, sfrom, sid,
+ stream_ns=stream_ns,
+ default_ns=default_ns,
+ default_lang=default_lang,
+ version=version,
+ xml_header=xml_header)
+ sent_header = self.xmpp.socket.next_sent(timeout)
+ if sent_header is None:
+ raise ValueError("Socket did not return data.")
+
+ # Apply closing elements so that we can construct
+ # XML objects for comparison.
+ header2 = header + '</stream:stream>'
+ sent_header2 = sent_header + b'</stream:stream>'
+
+ xml = self.parse_xml(header2)
+ sent_xml = self.parse_xml(sent_header2)
+
+ self.failUnless(
+ self.compare(xml, sent_xml),
+ "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
+ header, sent_header))
+
+ def send_feature(self, data, method='mask', use_values=True, timeout=1):
+ """
+ """
+ sent_data = self.xmpp.socket.next_sent(timeout)
+ xml = self.parse_xml(data)
+ sent_xml = self.parse_xml(sent_data)
+ if sent_data is None:
+ self.fail("No stanza was sent.")
+ if method == 'exact':
+ self.failUnless(self.compare(xml, sent_xml),
+ "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
+ highlight(tostring(xml)), highlight(tostring(sent_xml))))
+ elif method == 'mask':
+ matcher = MatchXMLMask(xml)
+ self.failUnless(matcher.match(sent_xml),
+ "Stanza did not match using %s method:\n" % method + \
+ "Criteria:\n%s\n" % highlight(tostring(xml)) + \
+ "Stanza:\n%s" % highlight(tostring(sent_xml)))
+ else:
+ raise ValueError("Uknown matching method: %s" % method)
+
+ def send(self, data, defaults=None, use_values=True,
+ timeout=.5, method='exact'):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using check.
+
+ Arguments:
+ stanza_class -- The class of the sent stanza object.
+ data -- The XML string of the expected Message stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by check_message.
+ defaults -- A list of stanza interfaces that have defaults
+ values which may interfere with comparisons.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ method -- Select the type of comparison to use for
+ verifying the sent stanza. Options are 'exact',
+ 'id', 'stanzapath', 'xpath', and 'mask'.
+ Defaults to the value of self.match_method.
+ """
+ sent = self.xmpp.socket.next_sent(timeout)
+ if data is None and sent is None:
+ return
+ if data is None and sent is not None:
+ self.fail("Stanza data was sent: %s" % sent)
+ if sent is None:
+ self.fail("No stanza was sent.")
+
+ xml = self.parse_xml(sent)
+ self.fix_namespaces(xml, 'jabber:client')
+ sent = self.xmpp._build_stanza(xml, 'jabber:client')
+ self.check(sent, data,
+ method=method,
+ defaults=defaults,
+ use_values=use_values)
+
+ def stream_close(self):
+ """
+ Disconnect the dummy XMPP client.
+
+ Can be safely called even if stream_start has not been called.
+
+ Must be placed in the tearDown method of a test class to ensure
+ that the XMPP client is disconnected after an error.
+ """
+ if hasattr(self, 'xmpp') and self.xmpp is not None:
+ self.xmpp.data_received(self.xmpp.stream_footer)
+ self.xmpp.disconnect()
+
+ # ------------------------------------------------------------------
+ # XML Comparison and Cleanup
+
+ def fix_namespaces(self, xml, ns):
+ """
+ Assign a namespace to an element and any children that
+ don't have a namespace.
+
+ Arguments:
+ xml -- The XML object to fix.
+ ns -- The namespace to add to the XML object.
+ """
+ if xml.tag.startswith('{'):
+ return
+ xml.tag = '{%s}%s' % (ns, xml.tag)
+ for child in xml:
+ self.fix_namespaces(child, ns)
+
+ def compare(self, xml, *other):
+ """
+ Compare XML objects.
+
+ Arguments:
+ xml -- The XML object to compare against.
+ *other -- The list of XML objects to compare.
+ """
+ if not other:
+ return False
+
+ # Compare multiple objects
+ if len(other) > 1:
+ for xml2 in other:
+ if not self.compare(xml, xml2):
+ return False
+ return True
+
+ other = other[0]
+
+ # Step 1: Check tags
+ if xml.tag != other.tag:
+ return False
+
+ # Step 2: Check attributes
+ if xml.attrib != other.attrib:
+ return False
+
+ # Step 3: Check text
+ if xml.text is None:
+ xml.text = ""
+ if other.text is None:
+ other.text = ""
+ xml.text = xml.text.strip()
+ other.text = other.text.strip()
+
+ if xml.text != other.text:
+ return False
+
+ # Step 4: Check children count
+ if len(list(xml)) != len(list(other)):
+ return False
+
+ # Step 5: Recursively check children
+ for child in xml:
+ child2s = other.findall("%s" % child.tag)
+ if child2s is None:
+ return False
+ for child2 in child2s:
+ if self.compare(child, child2):
+ break
+ else:
+ return False
+
+ # Step 6: Recursively check children the other way.
+ for child in other:
+ child2s = xml.findall("%s" % child.tag)
+ if child2s is None:
+ return False
+ for child2 in child2s:
+ if self.compare(child, child2):
+ break
+ else:
+ return False
+
+ # Everything matches
+ return True
diff --git a/slixmpp/thirdparty/__init__.py b/slixmpp/thirdparty/__init__.py
new file mode 100644
index 00000000..d950f4f9
--- /dev/null
+++ b/slixmpp/thirdparty/__init__.py
@@ -0,0 +1,7 @@
+try:
+ from gnupg import GPG
+except:
+ from slixmpp.thirdparty.gnupg import GPG
+
+from slixmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
+from slixmpp.thirdparty.orderedset import OrderedSet
diff --git a/slixmpp/thirdparty/gnupg.py b/slixmpp/thirdparty/gnupg.py
new file mode 100644
index 00000000..a89289fd
--- /dev/null
+++ b/slixmpp/thirdparty/gnupg.py
@@ -0,0 +1,1017 @@
+""" A wrapper for the 'gpg' command::
+
+Portions of this module are derived from A.M. Kuchling's well-designed
+GPG.py, using Richard Jones' updated version 1.3, which can be found
+in the pycrypto CVS repository on Sourceforge:
+
+http://pycrypto.cvs.sourceforge.net/viewvc/pycrypto/gpg/GPG.py
+
+This module is *not* forward-compatible with amk's; some of the
+old interface has changed. For instance, since I've added decrypt
+functionality, I elected to initialize with a 'gnupghome' argument
+instead of 'keyring', so that gpg can find both the public and secret
+keyrings. I've also altered some of the returned objects in order for
+the caller to not have to know as much about the internals of the
+result classes.
+
+While the rest of ISconf is released under the GPL, I am releasing
+this single file under the same terms that A.M. Kuchling used for
+pycrypto.
+
+Steve Traugott, stevegt@terraluna.org
+Thu Jun 23 21:27:20 PDT 2005
+
+This version of the module has been modified from Steve Traugott's version
+(see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by
+Vinay Sajip to make use of the subprocess module (Steve's version uses os.fork()
+and so does not work on Windows). Renamed to gnupg.py to avoid confusion with
+the previous versions.
+
+Modifications Copyright (C) 2008-2012 Vinay Sajip. All rights reserved.
+
+A unittest harness (test_gnupg.py) has also been added.
+"""
+import locale
+
+__version__ = "0.2.9"
+__author__ = "Vinay Sajip"
+__date__ = "$29-Mar-2012 21:12:58$"
+
+try:
+ from io import StringIO
+except ImportError:
+ from cStringIO import StringIO
+
+import codecs
+import locale
+import logging
+import os
+import socket
+from subprocess import Popen
+from subprocess import PIPE
+import sys
+import threading
+
+try:
+ import logging.NullHandler as NullHandler
+except ImportError:
+ class NullHandler(logging.Handler):
+ def handle(self, record):
+ pass
+try:
+ unicode
+ _py3k = False
+except NameError:
+ _py3k = True
+
+logger = logging.getLogger(__name__)
+if not logger.handlers:
+ logger.addHandler(NullHandler())
+
+def _copy_data(instream, outstream):
+ # Copy one stream to another
+ sent = 0
+ if hasattr(sys.stdin, 'encoding'):
+ enc = sys.stdin.encoding
+ else:
+ enc = 'ascii'
+ while True:
+ data = instream.read(1024)
+ if len(data) == 0:
+ break
+ sent += len(data)
+ logger.debug("sending chunk (%d): %r", sent, data[:256])
+ try:
+ outstream.write(data)
+ except UnicodeError:
+ outstream.write(data.encode(enc))
+ except:
+ # Can sometimes get 'broken pipe' errors even when the data has all
+ # been sent
+ logger.exception('Error sending data')
+ break
+ try:
+ outstream.close()
+ except IOError:
+ logger.warning('Exception occurred while closing: ignored', exc_info=1)
+ logger.debug("closed output, %d bytes sent", sent)
+
+def _threaded_copy_data(instream, outstream):
+ wr = threading.Thread(target=_copy_data, args=(instream, outstream))
+ wr.setDaemon(True)
+ logger.debug('data copier: %r, %r, %r', wr, instream, outstream)
+ wr.start()
+ return wr
+
+def _write_passphrase(stream, passphrase, encoding):
+ passphrase = '%s\n' % passphrase
+ passphrase = passphrase.encode(encoding)
+ stream.write(passphrase)
+ logger.debug("Wrote passphrase: %r", passphrase)
+
+def _is_sequence(instance):
+ return isinstance(instance,list) or isinstance(instance,tuple)
+
+def _make_binary_stream(s, encoding):
+ try:
+ if _py3k:
+ if isinstance(s, str):
+ s = s.encode(encoding)
+ else:
+ if type(s) is not str:
+ s = s.encode(encoding)
+ from io import BytesIO
+ rv = BytesIO(s)
+ except ImportError:
+ rv = StringIO(s)
+ return rv
+
+class Verify(object):
+ "Handle status messages for --verify"
+
+ def __init__(self, gpg):
+ self.gpg = gpg
+ self.valid = False
+ self.fingerprint = self.creation_date = self.timestamp = None
+ self.signature_id = self.key_id = None
+ self.username = None
+
+ def __nonzero__(self):
+ return self.valid
+
+ __bool__ = __nonzero__
+
+ def handle_status(self, key, value):
+ if key in ("TRUST_UNDEFINED", "TRUST_NEVER", "TRUST_MARGINAL",
+ "TRUST_FULLY", "TRUST_ULTIMATE", "RSA_OR_IDEA", "NODATA",
+ "IMPORT_RES", "PLAINTEXT", "PLAINTEXT_LENGTH",
+ "POLICY_URL", "DECRYPTION_INFO", "DECRYPTION_OKAY", "IMPORTED"):
+ pass
+ elif key == "BADSIG":
+ self.valid = False
+ self.status = 'signature bad'
+ self.key_id, self.username = value.split(None, 1)
+ elif key == "GOODSIG":
+ self.valid = True
+ self.status = 'signature good'
+ self.key_id, self.username = value.split(None, 1)
+ elif key == "VALIDSIG":
+ (self.fingerprint,
+ self.creation_date,
+ self.sig_timestamp,
+ self.expire_timestamp) = value.split()[:4]
+ # may be different if signature is made with a subkey
+ self.pubkey_fingerprint = value.split()[-1]
+ self.status = 'signature valid'
+ elif key == "SIG_ID":
+ (self.signature_id,
+ self.creation_date, self.timestamp) = value.split()
+ elif key == "ERRSIG":
+ self.valid = False
+ (self.key_id,
+ algo, hash_algo,
+ cls,
+ self.timestamp) = value.split()[:5]
+ self.status = 'signature error'
+ elif key == "DECRYPTION_FAILED":
+ self.valid = False
+ self.key_id = value
+ self.status = 'decryption failed'
+ elif key == "NO_PUBKEY":
+ self.valid = False
+ self.key_id = value
+ self.status = 'no public key'
+ elif key in ("KEYEXPIRED", "SIGEXPIRED"):
+ # these are useless in verify, since they are spit out for any
+ # pub/subkeys on the key, not just the one doing the signing.
+ # if we want to check for signatures with expired key,
+ # the relevant flag is EXPKEYSIG.
+ pass
+ elif key in ("EXPKEYSIG", "REVKEYSIG"):
+ # signed with expired or revoked key
+ self.valid = False
+ self.key_id = value.split()[0]
+ self.status = (('%s %s') % (key[:3], key[3:])).lower()
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+class ImportResult(object):
+ "Handle status messages for --import"
+
+ counts = '''count no_user_id imported imported_rsa unchanged
+ n_uids n_subk n_sigs n_revoc sec_read sec_imported
+ sec_dups not_imported'''.split()
+ def __init__(self, gpg):
+ self.gpg = gpg
+ self.imported = []
+ self.results = []
+ self.fingerprints = []
+ for result in self.counts:
+ setattr(self, result, None)
+
+ def __nonzero__(self):
+ if self.not_imported: return False
+ if not self.fingerprints: return False
+ return True
+
+ __bool__ = __nonzero__
+
+ ok_reason = {
+ '0': 'Not actually changed',
+ '1': 'Entirely new key',
+ '2': 'New user IDs',
+ '4': 'New signatures',
+ '8': 'New subkeys',
+ '16': 'Contains private key',
+ }
+
+ problem_reason = {
+ '0': 'No specific reason given',
+ '1': 'Invalid Certificate',
+ '2': 'Issuer Certificate missing',
+ '3': 'Certificate Chain too long',
+ '4': 'Error storing certificate',
+ }
+
+ def handle_status(self, key, value):
+ if key == "IMPORTED":
+ # this duplicates info we already see in import_ok & import_problem
+ pass
+ elif key == "NODATA":
+ self.results.append({'fingerprint': None,
+ 'problem': '0', 'text': 'No valid data found'})
+ elif key == "IMPORT_OK":
+ reason, fingerprint = value.split()
+ reasons = []
+ for code, text in list(self.ok_reason.items()):
+ if int(reason) | int(code) == int(reason):
+ reasons.append(text)
+ reasontext = '\n'.join(reasons) + "\n"
+ self.results.append({'fingerprint': fingerprint,
+ 'ok': reason, 'text': reasontext})
+ self.fingerprints.append(fingerprint)
+ elif key == "IMPORT_PROBLEM":
+ try:
+ reason, fingerprint = value.split()
+ except:
+ reason = value
+ fingerprint = '<unknown>'
+ self.results.append({'fingerprint': fingerprint,
+ 'problem': reason, 'text': self.problem_reason[reason]})
+ elif key == "IMPORT_RES":
+ import_res = value.split()
+ for i in range(len(self.counts)):
+ setattr(self, self.counts[i], int(import_res[i]))
+ elif key == "KEYEXPIRED":
+ self.results.append({'fingerprint': None,
+ 'problem': '0', 'text': 'Key expired'})
+ elif key == "SIGEXPIRED":
+ self.results.append({'fingerprint': None,
+ 'problem': '0', 'text': 'Signature expired'})
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+ def summary(self):
+ l = []
+ l.append('%d imported'%self.imported)
+ if self.not_imported:
+ l.append('%d not imported'%self.not_imported)
+ return ', '.join(l)
+
+class ListKeys(list):
+ ''' Handle status messages for --list-keys.
+
+ Handle pub and uid (relating the latter to the former).
+
+ Don't care about (info from src/DETAILS):
+
+ crt = X.509 certificate
+ crs = X.509 certificate and private key available
+ sub = subkey (secondary key)
+ ssb = secret subkey (secondary key)
+ uat = user attribute (same as user id except for field 10).
+ sig = signature
+ rev = revocation signature
+ pkd = public key data (special field format, see below)
+ grp = reserved for gpgsm
+ rvk = revocation key
+ '''
+ def __init__(self, gpg):
+ self.gpg = gpg
+ self.curkey = None
+ self.fingerprints = []
+ self.uids = []
+
+ def key(self, args):
+ vars = ("""
+ type trust length algo keyid date expires dummy ownertrust uid
+ """).split()
+ self.curkey = {}
+ for i in range(len(vars)):
+ self.curkey[vars[i]] = args[i]
+ self.curkey['uids'] = []
+ if self.curkey['uid']:
+ self.curkey['uids'].append(self.curkey['uid'])
+ del self.curkey['uid']
+ self.append(self.curkey)
+
+ pub = sec = key
+
+ def fpr(self, args):
+ self.curkey['fingerprint'] = args[9]
+ self.fingerprints.append(args[9])
+
+ def uid(self, args):
+ self.curkey['uids'].append(args[9])
+ self.uids.append(args[9])
+
+ def handle_status(self, key, value):
+ pass
+
+class Crypt(Verify):
+ "Handle status messages for --encrypt and --decrypt"
+ def __init__(self, gpg):
+ Verify.__init__(self, gpg)
+ self.data = ''
+ self.ok = False
+ self.status = ''
+
+ def __nonzero__(self):
+ if self.ok: return True
+ return False
+
+ __bool__ = __nonzero__
+
+ def __str__(self):
+ return self.data.decode(self.gpg.encoding, self.gpg.decode_errors)
+
+ def handle_status(self, key, value):
+ if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION",
+ "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA",
+ "CARDCTRL"):
+ # in the case of ERROR, this is because a more specific error
+ # message will have come first
+ pass
+ elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE",
+ "MISSING_PASSPHRASE", "DECRYPTION_FAILED",
+ "KEY_NOT_CREATED"):
+ self.status = key.replace("_", " ").lower()
+ elif key == "NEED_PASSPHRASE_SYM":
+ self.status = 'need symmetric passphrase'
+ elif key == "BEGIN_DECRYPTION":
+ self.status = 'decryption incomplete'
+ elif key == "BEGIN_ENCRYPTION":
+ self.status = 'encryption incomplete'
+ elif key == "DECRYPTION_OKAY":
+ self.status = 'decryption ok'
+ self.ok = True
+ elif key == "END_ENCRYPTION":
+ self.status = 'encryption ok'
+ self.ok = True
+ elif key == "INV_RECP":
+ self.status = 'invalid recipient'
+ elif key == "KEYEXPIRED":
+ self.status = 'key expired'
+ elif key == "SIG_CREATED":
+ self.status = 'sig created'
+ elif key == "SIGEXPIRED":
+ self.status = 'sig expired'
+ else:
+ Verify.handle_status(self, key, value)
+
+class GenKey(object):
+ "Handle status messages for --gen-key"
+ def __init__(self, gpg):
+ self.gpg = gpg
+ self.type = None
+ self.fingerprint = None
+
+ def __nonzero__(self):
+ if self.fingerprint: return True
+ return False
+
+ __bool__ = __nonzero__
+
+ def __str__(self):
+ return self.fingerprint or ''
+
+ def handle_status(self, key, value):
+ if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA"):
+ pass
+ elif key == "KEY_CREATED":
+ (self.type,self.fingerprint) = value.split()
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+class DeleteResult(object):
+ "Handle status messages for --delete-key and --delete-secret-key"
+ def __init__(self, gpg):
+ self.gpg = gpg
+ self.status = 'ok'
+
+ def __str__(self):
+ return self.status
+
+ problem_reason = {
+ '1': 'No such key',
+ '2': 'Must delete secret key first',
+ '3': 'Ambigious specification',
+ }
+
+ def handle_status(self, key, value):
+ if key == "DELETE_PROBLEM":
+ self.status = self.problem_reason.get(value,
+ "Unknown error: %r" % value)
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+class Sign(object):
+ "Handle status messages for --sign"
+ def __init__(self, gpg):
+ self.gpg = gpg
+ self.type = None
+ self.fingerprint = None
+
+ def __nonzero__(self):
+ return self.fingerprint is not None
+
+ __bool__ = __nonzero__
+
+ def __str__(self):
+ return self.data.decode(self.gpg.encoding, self.gpg.decode_errors)
+
+ def handle_status(self, key, value):
+ if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE",
+ "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL"):
+ pass
+ elif key == "SIG_CREATED":
+ (self.type,
+ algo, hashalgo, cls,
+ self.timestamp, self.fingerprint
+ ) = value.split()
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+
+class GPG(object):
+
+ decode_errors = 'strict'
+
+ result_map = {
+ 'crypt': Crypt,
+ 'delete': DeleteResult,
+ 'generate': GenKey,
+ 'import': ImportResult,
+ 'list': ListKeys,
+ 'sign': Sign,
+ 'verify': Verify,
+ }
+
+ "Encapsulate access to the gpg executable"
+ def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False,
+ use_agent=False, keyring=None):
+ """Initialize a GPG process wrapper. Options are:
+
+ gpgbinary -- full pathname for GPG binary.
+
+ gnupghome -- full pathname to where we can find the public and
+ private keyrings. Default is whatever gpg defaults to.
+ keyring -- name of alternative keyring file to use. If specified,
+ the default keyring is not used.
+ """
+ self.gpgbinary = gpgbinary
+ self.gnupghome = gnupghome
+ self.keyring = keyring
+ self.verbose = verbose
+ self.use_agent = use_agent
+ self.encoding = locale.getpreferredencoding()
+ if self.encoding is None: # This happens on Jython!
+ self.encoding = sys.stdin.encoding
+ if gnupghome and not os.path.isdir(self.gnupghome):
+ os.makedirs(self.gnupghome,0x1C0)
+ p = self._open_subprocess(["--version"])
+ result = self.result_map['verify'](self) # any result will do for this
+ self._collect_output(p, result, stdin=p.stdin)
+ if p.returncode != 0:
+ raise ValueError("Error invoking gpg: %s: %s" % (p.returncode,
+ result.stderr))
+
+ def _open_subprocess(self, args, passphrase=False):
+ # Internal method: open a pipe to a GPG subprocess and return
+ # the file objects for communicating with it.
+ cmd = [self.gpgbinary, '--status-fd 2 --no-tty']
+ if self.gnupghome:
+ cmd.append('--homedir "%s" ' % self.gnupghome)
+ if self.keyring:
+ cmd.append('--no-default-keyring --keyring "%s" ' % self.keyring)
+ if passphrase:
+ cmd.append('--batch --passphrase-fd 0')
+ if self.use_agent:
+ cmd.append('--use-agent')
+ cmd.extend(args)
+ cmd = ' '.join(cmd)
+ if self.verbose:
+ print(cmd)
+ logger.debug("%s", cmd)
+ return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+
+ def _read_response(self, stream, result):
+ # Internal method: reads all the stderr output from GPG, taking notice
+ # only of lines that begin with the magic [GNUPG:] prefix.
+ #
+ # Calls methods on the response object for each valid token found,
+ # with the arg being the remainder of the status line.
+ lines = []
+ while True:
+ line = stream.readline()
+ if len(line) == 0:
+ break
+ lines.append(line)
+ line = line.rstrip()
+ if self.verbose:
+ print(line)
+ logger.debug("%s", line)
+ if line[0:9] == '[GNUPG:] ':
+ # Chop off the prefix
+ line = line[9:]
+ L = line.split(None, 1)
+ keyword = L[0]
+ if len(L) > 1:
+ value = L[1]
+ else:
+ value = ""
+ result.handle_status(keyword, value)
+ result.stderr = ''.join(lines)
+
+ def _read_data(self, stream, result):
+ # Read the contents of the file from GPG's stdout
+ chunks = []
+ while True:
+ data = stream.read(1024)
+ if len(data) == 0:
+ break
+ logger.debug("chunk: %r" % data[:256])
+ chunks.append(data)
+ if _py3k:
+ # Join using b'' or '', as appropriate
+ result.data = type(data)().join(chunks)
+ else:
+ result.data = ''.join(chunks)
+
+ def _collect_output(self, process, result, writer=None, stdin=None):
+ """
+ Drain the subprocesses output streams, writing the collected output
+ to the result. If a writer thread (writing to the subprocess) is given,
+ make sure it's joined before returning. If a stdin stream is given,
+ close it before returning.
+ """
+ stderr = codecs.getreader(self.encoding)(process.stderr)
+ rr = threading.Thread(target=self._read_response, args=(stderr, result))
+ rr.setDaemon(True)
+ logger.debug('stderr reader: %r', rr)
+ rr.start()
+
+ stdout = process.stdout
+ dr = threading.Thread(target=self._read_data, args=(stdout, result))
+ dr.setDaemon(True)
+ logger.debug('stdout reader: %r', dr)
+ dr.start()
+
+ dr.join()
+ rr.join()
+ if writer is not None:
+ writer.join()
+ process.wait()
+ if stdin is not None:
+ try:
+ stdin.close()
+ except IOError:
+ pass
+ stderr.close()
+ stdout.close()
+
+ def _handle_io(self, args, file, result, passphrase=None, binary=False):
+ "Handle a call to GPG - pass input data, collect output data"
+ # Handle a basic data call - pass data to GPG, handle the output
+ # including status information. Garbage In, Garbage Out :)
+ p = self._open_subprocess(args, passphrase is not None)
+ if not binary:
+ stdin = codecs.getwriter(self.encoding)(p.stdin)
+ else:
+ stdin = p.stdin
+ if passphrase:
+ _write_passphrase(stdin, passphrase, self.encoding)
+ writer = _threaded_copy_data(file, stdin)
+ self._collect_output(p, result, writer, stdin)
+ return result
+
+ #
+ # SIGNATURE METHODS
+ #
+ def sign(self, message, **kwargs):
+ """sign message"""
+ f = _make_binary_stream(message, self.encoding)
+ result = self.sign_file(f, **kwargs)
+ f.close()
+ return result
+
+ def sign_file(self, file, keyid=None, passphrase=None, clearsign=True,
+ detach=False, binary=False):
+ """sign file"""
+ logger.debug("sign_file: %s", file)
+ if binary:
+ args = ['-s']
+ else:
+ args = ['-sa']
+ # You can't specify detach-sign and clearsign together: gpg ignores
+ # the detach-sign in that case.
+ if detach:
+ args.append("--detach-sign")
+ elif clearsign:
+ args.append("--clearsign")
+ if keyid:
+ args.append('--default-key "%s"' % keyid)
+ args.extend(['--no-version', "--comment ''"])
+ result = self.result_map['sign'](self)
+ #We could use _handle_io here except for the fact that if the
+ #passphrase is bad, gpg bails and you can't write the message.
+ p = self._open_subprocess(args, passphrase is not None)
+ try:
+ stdin = p.stdin
+ if passphrase:
+ _write_passphrase(stdin, passphrase, self.encoding)
+ writer = _threaded_copy_data(file, stdin)
+ except IOError:
+ logging.exception("error writing message")
+ writer = None
+ self._collect_output(p, result, writer, stdin)
+ return result
+
+ def verify(self, data):
+ """Verify the signature on the contents of the string 'data'
+
+ >>> gpg = GPG(gnupghome="keys")
+ >>> input = gpg.gen_key_input(Passphrase='foo')
+ >>> key = gpg.gen_key(input)
+ >>> assert key
+ >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='bar')
+ >>> assert not sig
+ >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='foo')
+ >>> assert sig
+ >>> verify = gpg.verify(sig.data)
+ >>> assert verify
+
+ """
+ f = _make_binary_stream(data, self.encoding)
+ result = self.verify_file(f)
+ f.close()
+ return result
+
+ def verify_file(self, file, data_filename=None):
+ "Verify the signature on the contents of the file-like object 'file'"
+ logger.debug('verify_file: %r, %r', file, data_filename)
+ result = self.result_map['verify'](self)
+ args = ['--verify']
+ if data_filename is None:
+ self._handle_io(args, file, result, binary=True)
+ else:
+ logger.debug('Handling detached verification')
+ import tempfile
+ fd, fn = tempfile.mkstemp(prefix='pygpg')
+ s = file.read()
+ file.close()
+ logger.debug('Wrote to temp file: %r', s)
+ os.write(fd, s)
+ os.close(fd)
+ args.append(fn)
+ args.append('"%s"' % data_filename)
+ try:
+ p = self._open_subprocess(args)
+ self._collect_output(p, result, stdin=p.stdin)
+ finally:
+ os.unlink(fn)
+ return result
+
+ #
+ # KEY MANAGEMENT
+ #
+
+ def import_keys(self, key_data):
+ """ import the key_data into our keyring
+
+ >>> import shutil
+ >>> shutil.rmtree("keys")
+ >>> gpg = GPG(gnupghome="keys")
+ >>> input = gpg.gen_key_input()
+ >>> result = gpg.gen_key(input)
+ >>> print1 = result.fingerprint
+ >>> result = gpg.gen_key(input)
+ >>> print2 = result.fingerprint
+ >>> pubkey1 = gpg.export_keys(print1)
+ >>> seckey1 = gpg.export_keys(print1,secret=True)
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> pubkeys = gpg.list_keys()
+ >>> assert print1 in seckeys.fingerprints
+ >>> assert print1 in pubkeys.fingerprints
+ >>> str(gpg.delete_keys(print1))
+ 'Must delete secret key first'
+ >>> str(gpg.delete_keys(print1,secret=True))
+ 'ok'
+ >>> str(gpg.delete_keys(print1))
+ 'ok'
+ >>> str(gpg.delete_keys("nosuchkey"))
+ 'No such key'
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> pubkeys = gpg.list_keys()
+ >>> assert not print1 in seckeys.fingerprints
+ >>> assert not print1 in pubkeys.fingerprints
+ >>> result = gpg.import_keys('foo')
+ >>> assert not result
+ >>> result = gpg.import_keys(pubkey1)
+ >>> pubkeys = gpg.list_keys()
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> assert not print1 in seckeys.fingerprints
+ >>> assert print1 in pubkeys.fingerprints
+ >>> result = gpg.import_keys(seckey1)
+ >>> assert result
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> pubkeys = gpg.list_keys()
+ >>> assert print1 in seckeys.fingerprints
+ >>> assert print1 in pubkeys.fingerprints
+ >>> assert print2 in pubkeys.fingerprints
+
+ """
+ result = self.result_map['import'](self)
+ logger.debug('import_keys: %r', key_data[:256])
+ data = _make_binary_stream(key_data, self.encoding)
+ self._handle_io(['--import'], data, result, binary=True)
+ logger.debug('import_keys result: %r', result.__dict__)
+ data.close()
+ return result
+
+ def recv_keys(self, keyserver, *keyids):
+ """Import a key from a keyserver
+
+ >>> import shutil
+ >>> shutil.rmtree("keys")
+ >>> gpg = GPG(gnupghome="keys")
+ >>> result = gpg.recv_keys('pgp.mit.edu', '3FF0DB166A7476EA')
+ >>> assert result
+
+ """
+ result = self.result_map['import'](self)
+ logger.debug('recv_keys: %r', keyids)
+ data = _make_binary_stream("", self.encoding)
+ #data = ""
+ args = ['--keyserver', keyserver, '--recv-keys']
+ args.extend(keyids)
+ self._handle_io(args, data, result, binary=True)
+ logger.debug('recv_keys result: %r', result.__dict__)
+ data.close()
+ return result
+
+ def delete_keys(self, fingerprints, secret=False):
+ which='key'
+ if secret:
+ which='secret-key'
+ if _is_sequence(fingerprints):
+ fingerprints = ' '.join(fingerprints)
+ args = ['--batch --delete-%s "%s"' % (which, fingerprints)]
+ result = self.result_map['delete'](self)
+ p = self._open_subprocess(args)
+ self._collect_output(p, result, stdin=p.stdin)
+ return result
+
+ def export_keys(self, keyids, secret=False):
+ "export the indicated keys. 'keyid' is anything gpg accepts"
+ which=''
+ if secret:
+ which='-secret-key'
+ if _is_sequence(keyids):
+ keyids = ' '.join(['"%s"' % k for k in keyids])
+ args = ["--armor --export%s %s" % (which, keyids)]
+ p = self._open_subprocess(args)
+ # gpg --export produces no status-fd output; stdout will be
+ # empty in case of failure
+ #stdout, stderr = p.communicate()
+ result = self.result_map['delete'](self) # any result will do
+ self._collect_output(p, result, stdin=p.stdin)
+ logger.debug('export_keys result: %r', result.data)
+ return result.data.decode(self.encoding, self.decode_errors)
+
+ def list_keys(self, secret=False):
+ """ list the keys currently in the keyring
+
+ >>> import shutil
+ >>> shutil.rmtree("keys")
+ >>> gpg = GPG(gnupghome="keys")
+ >>> input = gpg.gen_key_input()
+ >>> result = gpg.gen_key(input)
+ >>> print1 = result.fingerprint
+ >>> result = gpg.gen_key(input)
+ >>> print2 = result.fingerprint
+ >>> pubkeys = gpg.list_keys()
+ >>> assert print1 in pubkeys.fingerprints
+ >>> assert print2 in pubkeys.fingerprints
+
+ """
+
+ which='keys'
+ if secret:
+ which='secret-keys'
+ args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which,)
+ args = [args]
+ p = self._open_subprocess(args)
+
+ # there might be some status thingumy here I should handle... (amk)
+ # ...nope, unless you care about expired sigs or keys (stevegt)
+
+ # Get the response information
+ result = self.result_map['list'](self)
+ self._collect_output(p, result, stdin=p.stdin)
+ lines = result.data.decode(self.encoding,
+ self.decode_errors).splitlines()
+ valid_keywords = 'pub uid sec fpr'.split()
+ for line in lines:
+ if self.verbose:
+ print(line)
+ logger.debug("line: %r", line.rstrip())
+ if not line:
+ break
+ L = line.strip().split(':')
+ if not L:
+ continue
+ keyword = L[0]
+ if keyword in valid_keywords:
+ getattr(result, keyword)(L)
+ return result
+
+ def gen_key(self, input):
+ """Generate a key; you might use gen_key_input() to create the
+ control input.
+
+ >>> gpg = GPG(gnupghome="keys")
+ >>> input = gpg.gen_key_input()
+ >>> result = gpg.gen_key(input)
+ >>> assert result
+ >>> result = gpg.gen_key('foo')
+ >>> assert not result
+
+ """
+ args = ["--gen-key --batch"]
+ result = self.result_map['generate'](self)
+ f = _make_binary_stream(input, self.encoding)
+ self._handle_io(args, f, result, binary=True)
+ f.close()
+ return result
+
+ def gen_key_input(self, **kwargs):
+ """
+ Generate --gen-key input per gpg doc/DETAILS
+ """
+ parms = {}
+ for key, val in list(kwargs.items()):
+ key = key.replace('_','-').title()
+ parms[key] = val
+ parms.setdefault('Key-Type','RSA')
+ parms.setdefault('Key-Length',1024)
+ parms.setdefault('Name-Real', "Autogenerated Key")
+ parms.setdefault('Name-Comment', "Generated by gnupg.py")
+ try:
+ logname = os.environ['LOGNAME']
+ except KeyError:
+ logname = os.environ['USERNAME']
+ hostname = socket.gethostname()
+ parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'),
+ hostname))
+ out = "Key-Type: %s\n" % parms.pop('Key-Type')
+ for key, val in list(parms.items()):
+ out += "%s: %s\n" % (key, val)
+ out += "%commit\n"
+ return out
+
+ # Key-Type: RSA
+ # Key-Length: 1024
+ # Name-Real: ISdlink Server on %s
+ # Name-Comment: Created by %s
+ # Name-Email: isdlink@%s
+ # Expire-Date: 0
+ # %commit
+ #
+ #
+ # Key-Type: DSA
+ # Key-Length: 1024
+ # Subkey-Type: ELG-E
+ # Subkey-Length: 1024
+ # Name-Real: Joe Tester
+ # Name-Comment: with stupid passphrase
+ # Name-Email: joe@foo.bar
+ # Expire-Date: 0
+ # Passphrase: abc
+ # %pubring foo.pub
+ # %secring foo.sec
+ # %commit
+
+ #
+ # ENCRYPTION
+ #
+ def encrypt_file(self, file, recipients, sign=None,
+ always_trust=False, passphrase=None,
+ armor=True, output=None, symmetric=False):
+ "Encrypt the message read from the file-like object 'file'"
+ args = ['--no-version', "--comment ''"]
+ if symmetric:
+ args.append('--symmetric')
+ else:
+ args.append('--encrypt')
+ if not _is_sequence(recipients):
+ recipients = (recipients,)
+ for recipient in recipients:
+ args.append('--recipient "%s"' % recipient)
+ if armor: # create ascii-armored output - set to False for binary output
+ args.append('--armor')
+ if output: # write the output to a file with the specified name
+ if os.path.exists(output):
+ os.remove(output) # to avoid overwrite confirmation message
+ args.append('--output "%s"' % output)
+ if sign:
+ args.append('--sign --default-key "%s"' % sign)
+ if always_trust:
+ args.append("--always-trust")
+ result = self.result_map['crypt'](self)
+ self._handle_io(args, file, result, passphrase=passphrase, binary=True)
+ logger.debug('encrypt result: %r', result.data)
+ return result
+
+ def encrypt(self, data, recipients, **kwargs):
+ """Encrypt the message contained in the string 'data'
+
+ >>> import shutil
+ >>> if os.path.exists("keys"):
+ ... shutil.rmtree("keys")
+ >>> gpg = GPG(gnupghome="keys")
+ >>> input = gpg.gen_key_input(passphrase='foo')
+ >>> result = gpg.gen_key(input)
+ >>> print1 = result.fingerprint
+ >>> input = gpg.gen_key_input()
+ >>> result = gpg.gen_key(input)
+ >>> print2 = result.fingerprint
+ >>> result = gpg.encrypt("hello",print2)
+ >>> message = str(result)
+ >>> assert message != 'hello'
+ >>> result = gpg.decrypt(message)
+ >>> assert result
+ >>> str(result)
+ 'hello'
+ >>> result = gpg.encrypt("hello again",print1)
+ >>> message = str(result)
+ >>> result = gpg.decrypt(message)
+ >>> result.status == 'need passphrase'
+ True
+ >>> result = gpg.decrypt(message,passphrase='bar')
+ >>> result.status in ('decryption failed', 'bad passphrase')
+ True
+ >>> assert not result
+ >>> result = gpg.decrypt(message,passphrase='foo')
+ >>> result.status == 'decryption ok'
+ True
+ >>> str(result)
+ 'hello again'
+ >>> result = gpg.encrypt("signed hello",print2,sign=print1)
+ >>> result.status == 'need passphrase'
+ True
+ >>> result = gpg.encrypt("signed hello",print2,sign=print1,passphrase='foo')
+ >>> result.status == 'encryption ok'
+ True
+ >>> message = str(result)
+ >>> result = gpg.decrypt(message)
+ >>> result.status == 'decryption ok'
+ True
+ >>> assert result.fingerprint == print1
+
+ """
+ data = _make_binary_stream(data, self.encoding)
+ result = self.encrypt_file(data, recipients, **kwargs)
+ data.close()
+ return result
+
+ def decrypt(self, message, **kwargs):
+ data = _make_binary_stream(message, self.encoding)
+ result = self.decrypt_file(data, **kwargs)
+ data.close()
+ return result
+
+ def decrypt_file(self, file, always_trust=False, passphrase=None,
+ output=None):
+ args = ["--decrypt"]
+ if output: # write the output to a file with the specified name
+ if os.path.exists(output):
+ os.remove(output) # to avoid overwrite confirmation message
+ args.append('--output "%s"' % output)
+ if always_trust:
+ args.append("--always-trust")
+ result = self.result_map['crypt'](self)
+ self._handle_io(args, file, result, passphrase, binary=True)
+ logger.debug('decrypt result: %r', result.data)
+ return result
+
diff --git a/slixmpp/thirdparty/mini_dateutil.py b/slixmpp/thirdparty/mini_dateutil.py
new file mode 100644
index 00000000..e751a448
--- /dev/null
+++ b/slixmpp/thirdparty/mini_dateutil.py
@@ -0,0 +1,273 @@
+# This module is a very stripped down version of the dateutil
+# package for when dateutil has not been installed. As a replacement
+# for dateutil.parser.parse, the parsing methods from
+# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/
+
+#As such, the following copyrights and licenses applies:
+
+
+# dateutil - Extensions to the standard python 2.3+ datetime module.
+#
+# Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# fixed_dateime
+#
+# Copyright (c) 2008, Red Innovation Ltd., Finland
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Red Innovation nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+import re
+import math
+import datetime
+
+
+ZERO = datetime.timedelta(0)
+
+
+try:
+ from dateutil.parser import parse as parse_iso
+ from dateutil.tz import tzoffset, tzutc
+except:
+ # As a stopgap, define the two timezones here based
+ # on the dateutil code.
+
+ class tzutc(datetime.tzinfo):
+
+ def utcoffset(self, dt):
+ return ZERO
+
+ def dst(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def __eq__(self, other):
+ return (isinstance(other, tzutc) or
+ (isinstance(other, tzoffset) and other._offset == ZERO))
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "%s()" % self.__class__.__name__
+
+ __reduce__ = object.__reduce__
+
+ class tzoffset(datetime.tzinfo):
+
+ def __init__(self, name, offset):
+ self._name = name
+ self._offset = datetime.timedelta(minutes=offset)
+
+ def utcoffset(self, dt):
+ return self._offset
+
+ def dst(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return self._name
+
+ def __eq__(self, other):
+ return (isinstance(other, tzoffset) and
+ self._offset == other._offset)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "%s(%s, %s)" % (self.__class__.__name__,
+ repr(self._name),
+ self._offset.days*86400+self._offset.seconds)
+
+ __reduce__ = object.__reduce__
+
+
+ _fixed_offset_tzs = { }
+ UTC = tzutc()
+
+ def _get_fixed_offset_tz(offsetmins):
+ """For internal use only: Returns a tzinfo with
+ the given fixed offset. This creates only one instance
+ for each offset; the zones are kept in a dictionary"""
+
+ if offsetmins == 0:
+ return UTC
+
+ if not offsetmins in _fixed_offset_tzs:
+ if offsetmins < 0:
+ sign = '-'
+ absoff = -offsetmins
+ else:
+ sign = '+'
+ absoff = offsetmins
+
+ name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
+ inst = tzoffset(name,offsetmins)
+ _fixed_offset_tzs[offsetmins] = inst
+
+ return _fixed_offset_tzs[offsetmins]
+
+
+ _iso8601_parser = re.compile("""
+ ^
+ (?P<year> [0-9]{4})?(?P<ymdsep>-?)?
+ (?P<month>[0-9]{2})?(?P=ymdsep)?
+ (?P<day> [0-9]{2})?
+
+ (?P<time>
+ (?: # time part... optional... at least hour must be specified
+ (?:T|\s+)?
+ (?P<hour>[0-9]{2})
+ (?:
+ # minutes, separated with :, or none, from hours
+ (?P<hmssep>[:]?)
+ (?P<minute>[0-9]{2})
+ (?:
+ # same for seconds, separated with :, or none, from hours
+ (?P=hmssep)
+ (?P<second>[0-9]{2})
+ )?
+ )?
+
+ # fractions
+ (?: [,.] (?P<frac>[0-9]{1,10}))?
+
+ # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
+ (
+ (?P<tzempty>Z)
+ |
+ (?P<tzh>[+-][0-9]{2})
+ (?: :? # optional separator
+ (?P<tzm>[0-9]{2})
+ )?
+ )?
+ )
+ )?
+ $
+ """, re.X) # """
+
+ def parse_iso(timestamp):
+ """Internal function for parsing a timestamp in
+ ISO 8601 format"""
+
+ timestamp = timestamp.strip()
+
+ m = _iso8601_parser.match(timestamp)
+ if not m:
+ raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp)
+
+ vals = m.groupdict()
+ def_vals = {'year': 1970, 'month': 1, 'day': 1}
+ for key in vals:
+ if vals[key] is None:
+ vals[key] = def_vals.get(key, 0)
+ elif key not in ['time', 'ymdsep', 'hmssep', 'tzempty']:
+ vals[key] = int(vals[key])
+
+ year = vals['year']
+ month = vals['month']
+ day = vals['day']
+
+ if m.group('time') is None:
+ return datetime.date(year, month, day)
+
+ h, min, s, us = None, None, None, 0
+ frac = 0
+ if m.group('tzempty') == None and m.group('tzh') == None:
+ raise ValueError("Not a proper ISO 8601 timestamp: " +
+ "missing timezone (Z or +hh[:mm])!")
+
+ if m.group('frac'):
+ frac = m.group('frac')
+ power = len(frac)
+ frac = int(frac) / 10.0 ** power
+
+ if m.group('hour'):
+ h = vals['hour']
+
+ if m.group('minute'):
+ min = vals['minute']
+
+ if m.group('second'):
+ s = vals['second']
+
+ if frac != None:
+ # ok, fractions of hour?
+ if min == None:
+ frac, min = math.modf(frac * 60.0)
+ min = int(min)
+
+ # fractions of second?
+ if s == None:
+ frac, s = math.modf(frac * 60.0)
+ s = int(s)
+
+ # and extract microseconds...
+ us = int(frac * 1000000)
+
+ if m.group('tzempty') == 'Z':
+ offsetmins = 0
+ else:
+ # timezone: hour diff with sign
+ offsetmins = vals['tzh'] * 60
+ tzm = m.group('tzm')
+
+ # add optional minutes
+ if tzm != None:
+ tzm = int(tzm)
+ offsetmins += tzm if offsetmins > 0 else -tzm
+
+ tz = _get_fixed_offset_tz(offsetmins)
+ return datetime.datetime(year, month, day, h, min, s, us, tz)
diff --git a/slixmpp/thirdparty/orderedset.py b/slixmpp/thirdparty/orderedset.py
new file mode 100644
index 00000000..f6642db3
--- /dev/null
+++ b/slixmpp/thirdparty/orderedset.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2009 Raymond Hettinger
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+
+import collections
+
+class OrderedSet(collections.MutableSet):
+
+ def __init__(self, iterable=None):
+ self.end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.map = {} # key --> [key, prev, next]
+ if iterable is not None:
+ self |= iterable
+
+ def __len__(self):
+ return len(self.map)
+
+ def __contains__(self, key):
+ return key in self.map
+
+ def add(self, key):
+ if key not in self.map:
+ end = self.end
+ curr = end[1]
+ curr[2] = end[1] = self.map[key] = [key, curr, end]
+
+ def discard(self, key):
+ if key in self.map:
+ key, prev, next = self.map.pop(key)
+ prev[2] = next
+ next[1] = prev
+
+ def __iter__(self):
+ end = self.end
+ curr = end[2]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[2]
+
+ def __reversed__(self):
+ end = self.end
+ curr = end[1]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[1]
+
+ def pop(self, last=True):
+ if not self:
+ raise KeyError('set is empty')
+ key = self.end[1][0] if last else self.end[2][0]
+ self.discard(key)
+ return key
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, list(self))
+
+ def __eq__(self, other):
+ if isinstance(other, OrderedSet):
+ return len(self) == len(other) and list(self) == list(other)
+ return set(self) == set(other)
+
+
+if __name__ == '__main__':
+ s = OrderedSet('abracadaba')
+ t = OrderedSet('simsalabim')
+ print(s | t)
+ print(s & t)
+ print(s - t) \ No newline at end of file
diff --git a/slixmpp/util/__init__.py b/slixmpp/util/__init__.py
new file mode 100644
index 00000000..8f70b2bb
--- /dev/null
+++ b/slixmpp/util/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.util
+ ~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :license: MIT, see LICENSE for more details
+"""
+
+
+from slixmpp.util.misc_ops import bytes, unicode, hashes, hash, \
+ num_to_bytes, bytes_to_num, quote, \
+ XOR
diff --git a/slixmpp/util/misc_ops.py b/slixmpp/util/misc_ops.py
new file mode 100644
index 00000000..2e661045
--- /dev/null
+++ b/slixmpp/util/misc_ops.py
@@ -0,0 +1,143 @@
+import sys
+import hashlib
+
+
+def unicode(text):
+ if not isinstance(text, str):
+ return text.decode('utf-8')
+ else:
+ return text
+
+
+def bytes(text):
+ """
+ Convert Unicode text to UTF-8 encoded bytes.
+
+ Since Python 2.6+ and Python 3+ have similar but incompatible
+ signatures, this function unifies the two to keep code sane.
+
+ :param text: Unicode text to convert to bytes
+ :rtype: bytes (Python3), str (Python2.6+)
+ """
+ if text is None:
+ return b''
+
+ import builtins
+ if isinstance(text, builtins.bytes):
+ # We already have bytes, so do nothing
+ return text
+ if isinstance(text, list):
+ # Convert a list of integers to bytes
+ return builtins.bytes(text)
+ else:
+ # Convert UTF-8 text to bytes
+ return builtins.bytes(text, encoding='utf-8')
+
+
+def quote(text):
+ """
+ Enclose in quotes and escape internal slashes and double quotes.
+
+ :param text: A Unicode or byte string.
+ """
+ text = bytes(text)
+ return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"'
+
+
+def num_to_bytes(num):
+ """
+ Convert an integer into a four byte sequence.
+
+ :param integer num: An integer to convert to its byte representation.
+ """
+ bval = b''
+ bval += bytes(chr(0xFF & (num >> 24)))
+ bval += bytes(chr(0xFF & (num >> 16)))
+ bval += bytes(chr(0xFF & (num >> 8)))
+ bval += bytes(chr(0xFF & (num >> 0)))
+ return bval
+
+
+def bytes_to_num(bval):
+ """
+ Convert a four byte sequence to an integer.
+
+ :param bytes bval: A four byte sequence to turn into an integer.
+ """
+ num = 0
+ num += ord(bval[0] << 24)
+ num += ord(bval[1] << 16)
+ num += ord(bval[2] << 8)
+ num += ord(bval[3])
+ return num
+
+
+def XOR(x, y):
+ """
+ Return the results of an XOR operation on two equal length byte strings.
+
+ :param bytes x: A byte string
+ :param bytes y: A byte string
+ :rtype: bytes
+ """
+ result = b''
+ for a, b in zip(x, y):
+ result += bytes([a ^ b])
+ return result
+
+
+def hash(name):
+ """
+ Return a hash function implementing the given algorithm.
+
+ :param name: The name of the hashing algorithm to use.
+ :type name: string
+
+ :rtype: function
+ """
+ name = name.lower()
+ if name.startswith('sha-'):
+ name = 'sha' + name[4:]
+ if name in dir(hashlib):
+ return getattr(hashlib, name)
+ return None
+
+
+def hashes():
+ """
+ Return a list of available hashing algorithms.
+
+ :rtype: list of strings
+ """
+ t = []
+ if 'md5' in dir(hashlib):
+ t = ['MD5']
+ if 'md2' in dir(hashlib):
+ t += ['MD2']
+ hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')]
+ return t + hashes
+
+
+def setdefaultencoding(encoding):
+ """
+ Set the current default string encoding used by the Unicode implementation.
+
+ Actually calls sys.setdefaultencoding under the hood - see the docs for that
+ for more details. This method exists only as a way to call find/call it
+ even after it has been 'deleted' when the site module is executed.
+
+ :param string encoding: An encoding name, compatible with sys.setdefaultencoding
+ """
+ func = getattr(sys, 'setdefaultencoding', None)
+ if func is None:
+ import gc
+ import types
+ for obj in gc.get_objects():
+ if (isinstance(obj, types.BuiltinFunctionType)
+ and obj.__name__ == 'setdefaultencoding'):
+ func = obj
+ break
+ if func is None:
+ raise RuntimeError("Could not find setdefaultencoding")
+ sys.setdefaultencoding = func
+ return func(encoding)
diff --git a/slixmpp/util/sasl/__init__.py b/slixmpp/util/sasl/__init__.py
new file mode 100644
index 00000000..0e7e7fbd
--- /dev/null
+++ b/slixmpp/util/sasl/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.util.sasl
+ ~~~~~~~~~~~~~~~~~~~
+
+ This module was originally based on Dave Cridland's Suelta library.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copryight: (c) 2004-2013 David Alan Cridland
+ :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
+
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.util.sasl.client import *
+from slixmpp.util.sasl.mechanisms import *
diff --git a/slixmpp/util/sasl/client.py b/slixmpp/util/sasl/client.py
new file mode 100644
index 00000000..d5daf4be
--- /dev/null
+++ b/slixmpp/util/sasl/client.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.util.sasl.client
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module was originally based on Dave Cridland's Suelta library.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copryight: (c) 2004-2013 David Alan Cridland
+ :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
+
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+import stringprep
+
+from slixmpp.util import hashes, bytes, stringprep_profiles
+
+
+log = logging.getLogger(__name__)
+
+
+#: Global registry mapping mechanism names to implementation classes.
+MECHANISMS = {}
+
+
+#: Global registry mapping mechanism names to security scores.
+MECH_SEC_SCORES = {}
+
+
+#: The SASLprep profile of stringprep used to validate simple username
+#: and password credentials.
+saslprep = stringprep_profiles.create(
+ nfkc=True,
+ bidi=True,
+ mappings=[
+ stringprep_profiles.b1_mapping,
+ stringprep_profiles.c12_mapping],
+ prohibited=[
+ stringprep.in_table_c12,
+ stringprep.in_table_c21,
+ stringprep.in_table_c22,
+ stringprep.in_table_c3,
+ stringprep.in_table_c4,
+ stringprep.in_table_c5,
+ stringprep.in_table_c6,
+ stringprep.in_table_c7,
+ stringprep.in_table_c8,
+ stringprep.in_table_c9],
+ unassigned=[stringprep.in_table_a1])
+
+
+def sasl_mech(score):
+ sec_score = score
+ def register(mech):
+ n = 0
+ mech.score = sec_score
+ if mech.use_hashes:
+ for hashing_alg in hashes():
+ n += 1
+ score = mech.score + n
+ name = '%s-%s' % (mech.name, hashing_alg)
+ MECHANISMS[name] = mech
+ MECH_SEC_SCORES[name] = score
+
+ if mech.channel_binding:
+ name += '-PLUS'
+ score += 10
+ MECHANISMS[name] = mech
+ MECH_SEC_SCORES[name] = score
+ else:
+ MECHANISMS[mech.name] = mech
+ MECH_SEC_SCORES[mech.name] = mech.score
+ if mech.channel_binding:
+ MECHANISMS[mech.name + '-PLUS'] = mech
+ MECH_SEC_SCORES[name] = mech.score + 10
+ return mech
+ return register
+
+
+class SASLNoAppropriateMechanism(Exception):
+ def __init__(self, value=''):
+ self.message = value
+
+
+class SASLCancelled(Exception):
+ def __init__(self, value=''):
+ self.message = value
+
+
+class SASLFailed(Exception):
+ def __init__(self, value=''):
+ self.message = value
+
+
+class SASLMutualAuthFailed(SASLFailed):
+ def __init__(self, value=''):
+ self.message = value
+
+
+class Mech(object):
+
+ name = 'GENERIC'
+ score = -1
+ use_hashes = False
+ channel_binding = False
+ required_credentials = set()
+ optional_credentials = set()
+ security = set()
+
+ def __init__(self, name, credentials, security_settings):
+ self.credentials = credentials
+ self.security_settings = security_settings
+ self.values = {}
+ self.base_name = self.name
+ self.name = name
+ self.setup(name)
+
+ def setup(self, name):
+ pass
+
+ def process(self, challenge=b''):
+ return b''
+
+
+def choose(mech_list, credentials, security_settings, limit=None, min_mech=None):
+ available_mechs = set(MECHANISMS.keys())
+ if limit is None:
+ limit = set(mech_list)
+ if not isinstance(limit, set):
+ limit = set(limit)
+ if not isinstance(mech_list, set):
+ mech_list = set(mech_list)
+
+ mech_list = mech_list.intersection(limit)
+ available_mechs = available_mechs.intersection(mech_list)
+
+ best_score = MECH_SEC_SCORES.get(min_mech, -1)
+ best_mech = None
+ for name in available_mechs:
+ if name in MECH_SEC_SCORES:
+ if MECH_SEC_SCORES[name] > best_score:
+ best_score = MECH_SEC_SCORES[name]
+ best_mech = name
+ if best_mech is None:
+ raise SASLNoAppropriateMechanism()
+
+ mech_class = MECHANISMS[best_mech]
+
+ try:
+ creds = credentials(mech_class.required_credentials,
+ mech_class.optional_credentials)
+ for req in mech_class.required_credentials:
+ if req not in creds:
+ raise SASLCancelled('Missing credential: %s' % req)
+ for opt in mech_class.optional_credentials:
+ if opt not in creds:
+ creds[opt] = b''
+ for cred in creds:
+ if cred in ('username', 'password', 'authzid'):
+ creds[cred] = bytes(saslprep(creds[cred]))
+ else:
+ creds[cred] = bytes(creds[cred])
+ security_opts = security_settings(mech_class.security)
+
+ return mech_class(best_mech, creds, security_opts)
+ except SASLCancelled as e:
+ log.info('SASL: %s: %s', best_mech, e.message)
+ mech_list.remove(best_mech)
+ return choose(mech_list, credentials, security_settings,
+ limit=limit,
+ min_mech=min_mech)
diff --git a/slixmpp/util/sasl/mechanisms.py b/slixmpp/util/sasl/mechanisms.py
new file mode 100644
index 00000000..de0203c0
--- /dev/null
+++ b/slixmpp/util/sasl/mechanisms.py
@@ -0,0 +1,548 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.util.sasl.mechanisms
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ A collection of supported SASL mechanisms.
+
+ This module was originally based on Dave Cridland's Suelta library.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copryight: (c) 2004-2013 David Alan Cridland
+ :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
+
+ :license: MIT, see LICENSE for more details
+"""
+
+import hmac
+import random
+
+from base64 import b64encode, b64decode
+
+from slixmpp.util import bytes, hash, XOR, quote, num_to_bytes
+from slixmpp.util.sasl.client import sasl_mech, Mech, \
+ SASLCancelled, SASLFailed, \
+ SASLMutualAuthFailed
+
+
+@sasl_mech(0)
+class ANONYMOUS(Mech):
+
+ name = 'ANONYMOUS'
+
+ def process(self, challenge=b''):
+ return b'Anonymous, Suelta'
+
+
+@sasl_mech(1)
+class LOGIN(Mech):
+
+ name = 'LOGIN'
+ required_credentials = set(['username', 'password'])
+
+ def setup(self, name):
+ self.step = 0
+
+ def process(self, challenge=b''):
+ if not challenge:
+ return b''
+
+ if self.step == 0:
+ self.step = 1
+ return self.credentials['username']
+ else:
+ return self.credentials['password']
+
+
+@sasl_mech(2)
+class PLAIN(Mech):
+
+ name = 'PLAIN'
+ required_credentials = set(['username', 'password'])
+ optional_credentials = set(['authzid'])
+ security = set(['encrypted', 'encrypted_plain', 'unencrypted_plain'])
+
+ def setup(self, name):
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_plain']:
+ raise SASLCancelled('PLAIN without encryption')
+ else:
+ if not self.security_settings['encrypted_plain']:
+ raise SASLCancelled('PLAIN with encryption')
+
+ def process(self, challenge=b''):
+ authzid = self.credentials['authzid']
+ authcid = self.credentials['username']
+ password = self.credentials['password']
+ return authzid + b'\x00' + authcid + b'\x00' + password
+
+
+@sasl_mech(100)
+class EXTERNAL(Mech):
+
+ name = 'EXTERNAL'
+ optional_credentials = set(['authzid'])
+
+ def process(self, challenge=b''):
+ return self.credentials['authzid']
+
+
+@sasl_mech(31)
+class X_FACEBOOK_PLATFORM(Mech):
+
+ name = 'X-FACEBOOK-PLATFORM'
+ required_credentials = set(['api_key', 'access_token'])
+
+ def process(self, challenge=b''):
+ if challenge:
+ values = {}
+ for kv in challenge.split(b'&'):
+ key, value = kv.split(b'=')
+ values[key] = value
+
+ resp_data = {
+ b'method': values[b'method'],
+ b'v': b'1.0',
+ b'call_id': b'1.0',
+ b'nonce': values[b'nonce'],
+ b'access_token': self.credentials['access_token'],
+ b'api_key': self.credentials['api_key']
+ }
+
+ resp = '&'.join(['%s=%s' % (k.decode("utf-8"), v.decode("utf-8")) for k, v in resp_data.items()])
+ return bytes(resp)
+ return b''
+
+
+@sasl_mech(10)
+class X_MESSENGER_OAUTH2(Mech):
+
+ name = 'X-MESSENGER-OAUTH2'
+ required_credentials = set(['access_token'])
+
+ def process(self, challenge=b''):
+ return self.credentials['access_token']
+
+
+@sasl_mech(10)
+class X_OAUTH2(Mech):
+
+ name = 'X-OAUTH2'
+ required_credentials = set(['username', 'access_token'])
+
+ def process(self, challenge=b''):
+ return b'\x00' + self.credentials['username'] + \
+ b'\x00' + self.credentials['access_token']
+
+
+@sasl_mech(3)
+class X_GOOGLE_TOKEN(Mech):
+
+ name = 'X-GOOGLE-TOKEN'
+ required_credentials = set(['email', 'access_token'])
+
+ def process(self, challenge=b''):
+ email = self.credentials['email']
+ token = self.credentials['access_token']
+ return b'\x00' + email + b'\x00' + token
+
+
+@sasl_mech(20)
+class CRAM(Mech):
+
+ name = 'CRAM'
+ use_hashes = True
+ required_credentials = set(['username', 'password'])
+ security = set(['encrypted', 'unencrypted_cram'])
+
+ def setup(self, name):
+ self.hash_name = name[5:]
+ self.hash = hash(self.hash_name)
+ if self.hash is None:
+ raise SASLCancelled('Unknown hash: %s' % self.hash_name)
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_cram']:
+ raise SASLCancelled('Unecrypted CRAM-%s' % self.hash_name)
+
+ def process(self, challenge=b''):
+ if not challenge:
+ return None
+
+ username = self.credentials['username']
+ password = self.credentials['password']
+
+ mac = hmac.HMAC(key=password, digestmod=self.hash)
+ mac.update(challenge)
+
+ return username + b' ' + bytes(mac.hexdigest())
+
+
+@sasl_mech(60)
+class SCRAM(Mech):
+
+ name = 'SCRAM'
+ use_hashes = True
+ channel_binding = True
+ required_credentials = set(['username', 'password'])
+ optional_credentials = set(['authzid', 'channel_binding'])
+ security = set(['encrypted', 'unencrypted_scram'])
+
+ def setup(self, name):
+ self.use_channel_binding = False
+ if name[-5:] == '-PLUS':
+ name = name[:-5]
+ self.use_channel_binding = True
+
+ self.hash_name = name[6:]
+ self.hash = hash(self.hash_name)
+
+ if self.hash is None:
+ raise SASLCancelled('Unknown hash: %s' % self.hash_name)
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_scram']:
+ raise SASLCancelled('Unencrypted SCRAM')
+
+ self.step = 0
+ self._mutual_auth = False
+
+ def HMAC(self, key, msg):
+ return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest()
+
+ def Hi(self, text, salt, iterations):
+ text = bytes(text)
+ ui1 = self.HMAC(text, salt + b'\0\0\0\01')
+ ui = ui1
+ for i in range(iterations - 1):
+ ui1 = self.HMAC(text, ui1)
+ ui = XOR(ui, ui1)
+ return ui
+
+ def H(self, text):
+ return self.hash(text).digest()
+
+ def saslname(self, value):
+ value = value.decode("utf-8")
+ escaped = []
+ for char in value:
+ if char == ',':
+ escaped += b'=2C'
+ elif char == '=':
+ escaped += b'=3D'
+ else:
+ escaped += char
+ return "".join(escaped).encode("utf-8")
+
+ def parse(self, challenge):
+ items = {}
+ for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
+ items[key] = value
+ return items
+
+ def process(self, challenge=b''):
+ steps = [self.process_1, self.process_2, self.process_3]
+ return steps[self.step](challenge)
+
+ def process_1(self, challenge):
+ self.step = 1
+ data = {}
+
+ self.cnonce = bytes(('%s' % random.random())[2:])
+
+ gs2_cbind_flag = b'n'
+ if self.credentials['channel_binding']:
+ if self.use_channel_binding:
+ gs2_cbind_flag = b'p=tls-unique'
+ else:
+ gs2_cbind_flag = b'y'
+
+ authzid = b''
+ if self.credentials['authzid']:
+ authzid = b'a=' + self.saslname(self.credentials['authzid'])
+
+ self.gs2_header = gs2_cbind_flag + b',' + authzid + b','
+
+ nonce = b'r=' + self.cnonce
+ username = b'n=' + self.saslname(self.credentials['username'])
+
+ self.client_first_message_bare = username + b',' + nonce
+ self.client_first_message = self.gs2_header + \
+ self.client_first_message_bare
+
+ return self.client_first_message
+
+ def process_2(self, challenge):
+ self.step = 2
+
+ data = self.parse(challenge)
+ if b'm' in data:
+ raise SASLCancelled('Received reserved attribute.')
+
+ salt = b64decode(data[b's'])
+ iteration_count = int(data[b'i'])
+ nonce = data[b'r']
+
+ if nonce[:len(self.cnonce)] != self.cnonce:
+ raise SASLCancelled('Invalid nonce')
+
+ cbind_data = b''
+ if self.use_channel_binding:
+ cbind_data = self.credentials['channel_binding']
+ cbind_input = self.gs2_header + cbind_data
+ channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')
+
+ client_final_message_without_proof = channel_binding + b',' + \
+ b'r=' + nonce
+
+ salted_password = self.Hi(self.credentials['password'],
+ salt,
+ iteration_count)
+ client_key = self.HMAC(salted_password, b'Client Key')
+ stored_key = self.H(client_key)
+ auth_message = self.client_first_message_bare + b',' + \
+ challenge + b',' + \
+ client_final_message_without_proof
+ client_signature = self.HMAC(stored_key, auth_message)
+ client_proof = XOR(client_key, client_signature)
+ server_key = self.HMAC(salted_password, b'Server Key')
+
+ self.server_signature = self.HMAC(server_key, auth_message)
+
+ client_final_message = client_final_message_without_proof + \
+ b',p=' + b64encode(client_proof)
+
+ return client_final_message
+
+ def process_3(self, challenge):
+ data = self.parse(challenge)
+ verifier = data.get(b'v', None)
+ error = data.get(b'e', 'Unknown error')
+
+ if not verifier:
+ raise SASLFailed(error)
+
+ if b64decode(verifier) != self.server_signature:
+ raise SASLMutualAuthFailed()
+
+ self._mutual_auth = True
+
+ return b''
+
+
+@sasl_mech(30)
+class DIGEST(Mech):
+
+ name = 'DIGEST'
+ use_hashes = True
+ required_credentials = set(['username', 'password', 'realm', 'service', 'host'])
+ optional_credentials = set(['authzid', 'service-name'])
+ security = set(['encrypted', 'unencrypted_digest'])
+
+ def setup(self, name):
+ self.hash_name = name[7:]
+ self.hash = hash(self.hash_name)
+ if self.hash is None:
+ raise SASLCancelled('Unknown hash: %s' % self.hash_name)
+ if not self.security_settings['encrypted']:
+ if not self.security_settings['unencrypted_digest']:
+ raise SASLCancelled('Unencrypted DIGEST')
+
+ self.qops = [b'auth']
+ self.qop = b'auth'
+ self.maxbuf = b'65536'
+ self.nonce = b''
+ self.cnonce = b''
+ self.nonce_count = 1
+
+ def parse(self, challenge=b''):
+ data = {}
+ var_name = b''
+ var_value = b''
+
+ # States: var, new_var, end, quote, escaped_quote
+ state = 'var'
+
+
+ for char in challenge:
+ char = bytes([char])
+
+ if state == 'var':
+ if char.isspace():
+ continue
+ if char == b'=':
+ state = 'value'
+ else:
+ var_name += char
+ elif state == 'value':
+ if char == b'"':
+ state = 'quote'
+ elif char == b',':
+ if var_name:
+ data[var_name.decode('utf-8')] = var_value
+ var_name = b''
+ var_value = b''
+ state = 'var'
+ else:
+ var_value += char
+ elif state == 'escaped':
+ var_value += char
+ elif state == 'quote':
+ if char == b'\\':
+ state = 'escaped'
+ elif char == b'"':
+ state = 'end'
+ else:
+ var_value += char
+ else:
+ if char == b',':
+ if var_name:
+ data[var_name.decode('utf-8')] = var_value
+ var_name = b''
+ var_value = b''
+ state = 'var'
+ else:
+ var_value += char
+
+ if var_name:
+ data[var_name.decode('utf-8')] = var_value
+ var_name = b''
+ var_value = b''
+ state = 'var'
+ return data
+
+ def MAC(self, key, seq, msg):
+ mac = hmac.HMAC(key=key, digestmod=self.hash)
+ seqnum = num_to_bytes(seq)
+ mac.update(seqnum)
+ mac.update(msg)
+ return mac.digest()[:10] + b'\x00\x01' + seqnum
+
+ def A1(self):
+ username = self.credentials['username']
+ password = self.credentials['password']
+ authzid = self.credentials['authzid']
+ realm = self.credentials['realm']
+
+ a1 = self.hash()
+ a1.update(username + b':' + realm + b':' + password)
+ a1 = a1.digest()
+ a1 += b':' + self.nonce + b':' + self.cnonce
+ if authzid:
+ a1 += b':' + authzid
+
+ return bytes(a1)
+
+ def A2(self, prefix=b''):
+ a2 = prefix + b':' + self.digest_uri()
+ if self.qop in (b'auth-int', b'auth-conf'):
+ a2 += b':00000000000000000000000000000000'
+ return bytes(a2)
+
+ def response(self, prefix=b''):
+ nc = bytes('%08x' % self.nonce_count)
+
+ a1 = bytes(self.hash(self.A1()).hexdigest().lower())
+ a2 = bytes(self.hash(self.A2(prefix)).hexdigest().lower())
+ s = self.nonce + b':' + nc + b':' + self.cnonce + \
+ b':' + self.qop + b':' + a2
+
+ return bytes(self.hash(a1 + b':' + s).hexdigest().lower())
+
+ def digest_uri(self):
+ serv_type = self.credentials['service']
+ serv_name = self.credentials['service-name']
+ host = self.credentials['host']
+
+ uri = serv_type + b'/' + host
+ if serv_name and host != serv_name:
+ uri += b'/' + serv_name
+ return uri
+
+ def respond(self):
+ data = {
+ 'username': quote(self.credentials['username']),
+ 'authzid': quote(self.credentials['authzid']),
+ 'realm': quote(self.credentials['realm']),
+ 'nonce': quote(self.nonce),
+ 'cnonce': quote(self.cnonce),
+ 'nc': bytes('%08x' % self.nonce_count),
+ 'qop': self.qop,
+ 'digest-uri': quote(self.digest_uri()),
+ 'response': self.response(b'AUTHENTICATE'),
+ 'maxbuf': self.maxbuf,
+ 'charset': 'utf-8'
+ }
+ resp = b''
+ for key, value in data.items():
+ if value and value != b'""':
+ resp += b',' + bytes(key) + b'=' + bytes(value)
+ return resp[1:]
+
+ def process(self, challenge=b''):
+ if not challenge:
+ if self.cnonce and self.nonce and self.nonce_count and self.qop:
+ self.nonce_count += 1
+ return self.respond()
+ return None
+
+ data = self.parse(challenge)
+ if 'rspauth' in data:
+ if data['rspauth'] != self.response():
+ raise SASLMutualAuthFailed()
+ else:
+ self.nonce_count = 1
+ self.cnonce = bytes('%s' % random.random())[2:]
+ self.qops = data.get('qop', [b'auth'])
+ self.qop = b'auth'
+ if 'nonce' in data:
+ self.nonce = data['nonce']
+ if 'realm' in data and not self.credentials['realm']:
+ self.credentials['realm'] = data['realm']
+
+ return self.respond()
+
+
+try:
+ import kerberos
+except ImportError:
+ pass
+else:
+ @sasl_mech(75)
+ class GSSAPI(Mech):
+
+ name = 'GSSAPI'
+ required_credentials = set(['username', 'service-name'])
+ optional_credentials = set(['authzid'])
+
+ def setup(self, name):
+ authzid = self.credentials['authzid']
+ if not authzid:
+ authzid = 'xmpp@%s' % self.credentials['service-name']
+
+ _, self.gss = kerberos.authGSSClientInit(authzid)
+ self.step = 0
+
+ def process(self, challenge=b''):
+ b64_challenge = b64encode(challenge)
+ try:
+ if self.step == 0:
+ result = kerberos.authGSSClientStep(self.gss, b64_challenge)
+ if result != kerberos.AUTH_GSS_CONTINUE:
+ self.step = 1
+ elif not challenge:
+ kerberos.authGSSClientClean(self.gss)
+ return b''
+ elif self.step == 1:
+ username = self.credentials['username']
+
+ kerberos.authGSSClientUnwrap(self.gss, b64_challenge)
+ resp = kerberos.authGSSClientResponse(self.gss)
+ kerberos.authGSSClientWrap(self.gss, resp, username)
+
+ resp = kerberos.authGSSClientResponse(self.gss)
+ except kerberos.GSSError as e:
+ raise SASLCancelled('Kerberos error: %s' % e)
+ if not resp:
+ return b''
+ else:
+ return b64decode(resp)
diff --git a/slixmpp/util/stringprep_profiles.py b/slixmpp/util/stringprep_profiles.py
new file mode 100644
index 00000000..5fb0b4b7
--- /dev/null
+++ b/slixmpp/util/stringprep_profiles.py
@@ -0,0 +1,151 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.util.stringprep_profiles
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module makes it easier to define profiles of stringprep,
+ such as nodeprep and resourceprep for JID validation, and
+ SASLprep for SASL.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :license: MIT, see LICENSE for more details
+"""
+
+
+from __future__ import unicode_literals
+
+import stringprep
+from unicodedata import ucd_3_2_0 as unicodedata
+
+from slixmpp.util import unicode
+
+
+class StringPrepError(UnicodeError):
+ pass
+
+
+def b1_mapping(char):
+ """Map characters that are commonly mapped to nothing."""
+ return '' if stringprep.in_table_b1(char) else None
+
+
+def c12_mapping(char):
+ """Map non-ASCII whitespace to spaces."""
+ return ' ' if stringprep.in_table_c12(char) else None
+
+
+def map_input(data, tables=None):
+ """
+ Each character in the input stream MUST be checked against
+ a mapping table.
+ """
+ result = []
+ for char in data:
+ replacement = None
+
+ for mapping in tables:
+ replacement = mapping(char)
+ if replacement is not None:
+ break
+
+ if replacement is None:
+ replacement = char
+ result.append(replacement)
+ return ''.join(result)
+
+
+def normalize(data, nfkc=True):
+ """
+ A profile can specify one of two options for Unicode normalization:
+ - no normalization
+ - Unicode normalization with form KC
+ """
+ if nfkc:
+ data = unicodedata.normalize('NFKC', data)
+ return data
+
+
+def prohibit_output(data, tables=None):
+ """
+ Before the text can be emitted, it MUST be checked for prohibited
+ code points.
+ """
+ for char in data:
+ for check in tables:
+ if check(char):
+ raise StringPrepError("Prohibited code point: %s" % char)
+
+
+def check_bidi(data):
+ """
+ 1) The characters in section 5.8 MUST be prohibited.
+
+ 2) If a string contains any RandALCat character, the string MUST NOT
+ contain any LCat character.
+
+ 3) If a string contains any RandALCat character, a RandALCat
+ character MUST be the first character of the string, and a
+ RandALCat character MUST be the last character of the string.
+ """
+ if not data:
+ return data
+
+ has_lcat = False
+ has_randal = False
+
+ for c in data:
+ if stringprep.in_table_c8(c):
+ raise StringPrepError("BIDI violation: seciton 6 (1)")
+ if stringprep.in_table_d1(c):
+ has_randal = True
+ elif stringprep.in_table_d2(c):
+ has_lcat = True
+
+ if has_randal and has_lcat:
+ raise StringPrepError("BIDI violation: section 6 (2)")
+
+ first_randal = stringprep.in_table_d1(data[0])
+ last_randal = stringprep.in_table_d1(data[-1])
+ if has_randal and not (first_randal and last_randal):
+ raise StringPrepError("BIDI violation: section 6 (3)")
+
+
+def create(nfkc=True, bidi=True, mappings=None,
+ prohibited=None, unassigned=None):
+ """Create a profile of stringprep.
+
+ :param bool nfkc:
+ If `True`, perform NFKC Unicode normalization. Defaults to `True`.
+ :param bool bidi:
+ If `True`, perform bidirectional text checks. Defaults to `True`.
+ :param list mappings:
+ Optional list of functions for mapping characters to
+ suitable replacements.
+ :param list prohibited:
+ Optional list of functions which check for the presence of
+ prohibited characters.
+ :param list unassigned:
+ Optional list of functions for detecting the use of unassigned
+ code points.
+
+ :raises: StringPrepError
+ :return: Unicode string of the resulting text passing the
+ profile's requirements.
+ """
+ def profile(data, query=False):
+ try:
+ data = unicode(data)
+ except UnicodeError:
+ raise StringPrepError
+
+ data = map_input(data, mappings)
+ data = normalize(data, nfkc)
+ prohibit_output(data, prohibited)
+ if bidi:
+ check_bidi(data)
+ if query and unassigned:
+ check_unassigned(data, unassigned)
+ return data
+ return profile
diff --git a/slixmpp/version.py b/slixmpp/version.py
new file mode 100644
index 00000000..98a20603
--- /dev/null
+++ b/slixmpp/version.py
@@ -0,0 +1,13 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+# We don't want to have to import the entire library
+# just to get the version info for setup.py
+
+__version__ = '1.0'
+__version_info__ = (1, 0)
diff --git a/slixmpp/xmlstream/__init__.py b/slixmpp/xmlstream/__init__.py
new file mode 100644
index 00000000..b5302292
--- /dev/null
+++ b/slixmpp/xmlstream/__init__.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.jid import JID
+from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET
+from slixmpp.xmlstream.stanzabase import register_stanza_plugin
+from slixmpp.xmlstream.tostring import tostring, highlight
+from slixmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
+
+__all__ = ['JID', 'StanzaBase', 'ElementBase',
+ 'ET', 'StateMachine', 'tostring', 'highlight', 'XMLStream',
+ 'RESPONSE_TIMEOUT']
diff --git a/slixmpp/xmlstream/asyncio.py b/slixmpp/xmlstream/asyncio.py
new file mode 100644
index 00000000..0e0f610a
--- /dev/null
+++ b/slixmpp/xmlstream/asyncio.py
@@ -0,0 +1,50 @@
+"""
+A module that monkey patches the standard asyncio module to add an
+idle_call() method to the main loop. This method is used to execute a
+callback whenever the loop is not busy handling anything else. This means
+that it is a callback with lower priority than IO, timer, or even
+call_soon() ones. These callback are called only once each.
+"""
+
+import asyncio
+from asyncio import events
+from functools import wraps
+
+import collections
+
+def idle_call(self, callback):
+ if asyncio.iscoroutinefunction(callback):
+ raise TypeError("coroutines cannot be used with idle_call()")
+ handle = events.Handle(callback, [], self)
+ self._idle.append(handle)
+
+def my_run_once(self):
+ if self._idle:
+ self._ready.append(events.Handle(lambda: None, (), self))
+ real_run_once(self)
+ if self._idle:
+ handle = self._idle.popleft()
+ handle._run()
+
+cls = asyncio.get_event_loop().__class__
+
+cls._idle = collections.deque()
+cls.idle_call = idle_call
+real_run_once = cls._run_once
+cls._run_once = my_run_once
+
+def future_wrapper(func):
+ """
+ Make sure the result of a function call is an asyncio.Future()
+ object.
+ """
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ result = func(*args, **kwargs)
+ if isinstance(result, asyncio.Future):
+ return result
+ future = asyncio.Future()
+ future.set_result(result)
+ return future
+
+ return wrapper
diff --git a/slixmpp/xmlstream/cert.py b/slixmpp/xmlstream/cert.py
new file mode 100644
index 00000000..d357b326
--- /dev/null
+++ b/slixmpp/xmlstream/cert.py
@@ -0,0 +1,184 @@
+import logging
+from datetime import datetime, timedelta
+
+# Make a call to strptime before starting threads to
+# prevent thread safety issues.
+datetime.strptime('1970-01-01 12:00:00', "%Y-%m-%d %H:%M:%S")
+
+
+try:
+ from pyasn1.codec.der import decoder, encoder
+ from pyasn1.type.univ import Any, ObjectIdentifier, OctetString
+ from pyasn1.type.char import BMPString, IA5String, UTF8String
+ from pyasn1.type.useful import GeneralizedTime
+ from pyasn1_modules.rfc2459 import (Certificate, DirectoryString,
+ SubjectAltName, GeneralNames,
+ GeneralName)
+ from pyasn1_modules.rfc2459 import id_ce_subjectAltName as SUBJECT_ALT_NAME
+ from pyasn1_modules.rfc2459 import id_at_commonName as COMMON_NAME
+
+ XMPP_ADDR = ObjectIdentifier('1.3.6.1.5.5.7.8.5')
+ SRV_NAME = ObjectIdentifier('1.3.6.1.5.5.7.8.7')
+
+ HAVE_PYASN1 = True
+except ImportError:
+ HAVE_PYASN1 = False
+
+
+log = logging.getLogger(__name__)
+
+
+class CertificateError(Exception):
+ pass
+
+
+def decode_str(data):
+ encoding = 'utf-16-be' if isinstance(data, BMPString) else 'utf-8'
+ return bytes(data).decode(encoding)
+
+
+def extract_names(raw_cert):
+ results = {'CN': set(),
+ 'DNS': set(),
+ 'SRV': set(),
+ 'URI': set(),
+ 'XMPPAddr': set()}
+
+ cert = decoder.decode(raw_cert, asn1Spec=Certificate())[0]
+ tbs = cert.getComponentByName('tbsCertificate')
+ subject = tbs.getComponentByName('subject')
+ extensions = tbs.getComponentByName('extensions') or []
+
+ # Extract the CommonName(s) from the cert.
+ for rdnss in subject:
+ for rdns in rdnss:
+ for name in rdns:
+ oid = name.getComponentByName('type')
+ value = name.getComponentByName('value')
+
+ if oid != COMMON_NAME:
+ continue
+
+ value = decoder.decode(value, asn1Spec=DirectoryString())[0]
+ value = decode_str(value.getComponent())
+ results['CN'].add(value)
+
+ # Extract the Subject Alternate Names (DNS, SRV, URI, XMPPAddr)
+ for extension in extensions:
+ oid = extension.getComponentByName('extnID')
+ if oid != SUBJECT_ALT_NAME:
+ continue
+
+ value = decoder.decode(extension.getComponentByName('extnValue'),
+ asn1Spec=OctetString())[0]
+ sa_names = decoder.decode(value, asn1Spec=SubjectAltName())[0]
+ for name in sa_names:
+ name_type = name.getName()
+ if name_type == 'dNSName':
+ results['DNS'].add(decode_str(name.getComponent()))
+ if name_type == 'uniformResourceIdentifier':
+ value = decode_str(name.getComponent())
+ if value.startswith('xmpp:'):
+ results['URI'].add(value[5:])
+ elif name_type == 'otherName':
+ name = name.getComponent()
+
+ oid = name.getComponentByName('type-id')
+ value = name.getComponentByName('value')
+
+ if oid == XMPP_ADDR:
+ value = decoder.decode(value, asn1Spec=UTF8String())[0]
+ results['XMPPAddr'].add(decode_str(value))
+ elif oid == SRV_NAME:
+ value = decoder.decode(value, asn1Spec=IA5String())[0]
+ results['SRV'].add(decode_str(value))
+
+ return results
+
+
+def extract_dates(raw_cert):
+ if not HAVE_PYASN1:
+ log.warning("Could not find pyasn1 and pyasn1_modules. " + \
+ "SSL certificate expiration COULD NOT BE VERIFIED.")
+ return None, None
+
+ cert = decoder.decode(raw_cert, asn1Spec=Certificate())[0]
+ tbs = cert.getComponentByName('tbsCertificate')
+ validity = tbs.getComponentByName('validity')
+
+ not_before = validity.getComponentByName('notBefore')
+ not_before = str(not_before.getComponent())
+
+ not_after = validity.getComponentByName('notAfter')
+ not_after = str(not_after.getComponent())
+
+ if isinstance(not_before, GeneralizedTime):
+ not_before = datetime.strptime(not_before, '%Y%m%d%H%M%SZ')
+ else:
+ not_before = datetime.strptime(not_before, '%y%m%d%H%M%SZ')
+
+ if isinstance(not_after, GeneralizedTime):
+ not_after = datetime.strptime(not_after, '%Y%m%d%H%M%SZ')
+ else:
+ not_after = datetime.strptime(not_after, '%y%m%d%H%M%SZ')
+
+ return not_before, not_after
+
+
+def get_ttl(raw_cert):
+ not_before, not_after = extract_dates(raw_cert)
+ if not_after is None:
+ return None
+ return not_after - datetime.utcnow()
+
+
+def verify(expected, raw_cert):
+ if not HAVE_PYASN1:
+ log.warning("Could not find pyasn1 and pyasn1_modules. " + \
+ "SSL certificate COULD NOT BE VERIFIED.")
+ return
+
+ not_before, not_after = extract_dates(raw_cert)
+ cert_names = extract_names(raw_cert)
+
+ now = datetime.utcnow()
+
+ if not_before > now:
+ raise CertificateError(
+ 'Certificate has not entered its valid date range.')
+
+ if not_after <= now:
+ raise CertificateError(
+ 'Certificate has expired.')
+
+ if '.' in expected:
+ expected_wild = expected[expected.index('.'):]
+ else:
+ expected_wild = expected
+ expected_srv = '_xmpp-client.%s' % expected
+
+ for name in cert_names['XMPPAddr']:
+ if name == expected:
+ return True
+ for name in cert_names['SRV']:
+ if name == expected_srv or name == expected:
+ return True
+ for name in cert_names['DNS']:
+ if name == expected:
+ return True
+ if name.startswith('*'):
+ if '.' in name:
+ name_wild = name[name.index('.'):]
+ else:
+ name_wild = name
+ if expected_wild == name_wild:
+ return True
+ for name in cert_names['URI']:
+ if name == expected:
+ return True
+ for name in cert_names['CN']:
+ if name == expected:
+ return True
+
+ raise CertificateError(
+ 'Could not match certificate against hostname: %s' % expected)
diff --git a/slixmpp/xmlstream/handler/__init__.py b/slixmpp/xmlstream/handler/__init__.py
new file mode 100644
index 00000000..51a7ca6a
--- /dev/null
+++ b/slixmpp/xmlstream/handler/__init__.py
@@ -0,0 +1,16 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.handler.callback import Callback
+from slixmpp.xmlstream.handler.coroutine_callback import CoroutineCallback
+from slixmpp.xmlstream.handler.collector import Collector
+from slixmpp.xmlstream.handler.waiter import Waiter
+from slixmpp.xmlstream.handler.xmlcallback import XMLCallback
+from slixmpp.xmlstream.handler.xmlwaiter import XMLWaiter
+
+__all__ = ['Callback', 'CoroutineCallback', 'Waiter', 'XMLCallback', 'XMLWaiter']
diff --git a/slixmpp/xmlstream/handler/base.py b/slixmpp/xmlstream/handler/base.py
new file mode 100644
index 00000000..b6bff096
--- /dev/null
+++ b/slixmpp/xmlstream/handler/base.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.handler.base
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import weakref
+
+
+class BaseHandler(object):
+
+ """
+ Base class for stream handlers. Stream handlers are matched with
+ incoming stanzas so that the stanza may be processed in some way.
+ Stanzas may be matched with multiple handlers.
+
+ Handler execution may take place in two phases: during the incoming
+ stream processing, and in the main event loop. The :meth:`prerun()`
+ method is executed in the first case, and :meth:`run()` is called
+ during the second.
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~slixmpp.xmlstream.matcher.base.MatcherBase`
+ derived object that will be used to determine if a
+ stanza should be accepted by this handler.
+ :param stream: The :class:`~slixmpp.xmlstream.xmlstream.XMLStream`
+ instance that the handle will respond to.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ #: The name of the handler
+ self.name = name
+
+ #: The XML stream this handler is assigned to
+ self.stream = None
+ if stream is not None:
+ self.stream = weakref.ref(stream)
+ stream.register_handler(self)
+
+ self._destroy = False
+ self._payload = None
+ self._matcher = matcher
+
+ def match(self, xml):
+ """Compare a stanza or XML object with the handler's matcher.
+
+ :param xml: An XML or
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object
+ """
+ return self._matcher.match(xml)
+
+ def prerun(self, payload):
+ """Prepare the handler for execution while the XML
+ stream is being processed.
+
+ :param payload: A :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ object.
+ """
+ self._payload = payload
+
+ def run(self, payload):
+ """Execute the handler after XML stream processing and during the
+ main event loop.
+
+ :param payload: A :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ object.
+ """
+ self._payload = payload
+
+ def check_delete(self):
+ """Check if the handler should be removed from the list
+ of stream handlers.
+ """
+ return self._destroy
diff --git a/slixmpp/xmlstream/handler/callback.py b/slixmpp/xmlstream/handler/callback.py
new file mode 100644
index 00000000..4cb329af
--- /dev/null
+++ b/slixmpp/xmlstream/handler/callback.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.handler.callback
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.handler.base import BaseHandler
+
+
+class Callback(BaseHandler):
+
+ """
+ The Callback handler will execute a callback function with
+ matched stanzas.
+
+ The handler may execute the callback either during stream
+ processing or during the main event loop.
+
+ Callback functions are all executed in the same thread, so be aware if
+ you are executing functions that will block for extended periods of
+ time. Typically, you should signal your own events using the Slixmpp
+ object's :meth:`~slixmpp.xmlstream.xmlstream.XMLStream.event()`
+ method to pass the stanza off to a threaded event handler for further
+ processing.
+
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~slixmpp.xmlstream.matcher.base.MatcherBase`
+ derived object for matching stanza objects.
+ :param pointer: The function to execute during callback.
+ :param bool thread: **DEPRECATED.** Remains only for
+ backwards compatibility.
+ :param bool once: Indicates if the handler should be used only
+ once. Defaults to False.
+ :param bool instream: Indicates if the callback should be executed
+ during stream processing instead of in the
+ main event loop.
+ :param stream: The :class:`~slixmpp.xmlstream.xmlstream.XMLStream`
+ instance this handler should monitor.
+ """
+
+ def __init__(self, name, matcher, pointer, thread=False,
+ once=False, instream=False, stream=None):
+ BaseHandler.__init__(self, name, matcher, stream)
+ self._pointer = pointer
+ self._once = once
+ self._instream = instream
+
+ def prerun(self, payload):
+ """Execute the callback during stream processing, if
+ the callback was created with ``instream=True``.
+
+ :param payload: The matched
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object.
+ """
+ if self._once:
+ self._destroy = True
+ if self._instream:
+ self.run(payload, True)
+
+ def run(self, payload, instream=False):
+ """Execute the callback function with the matched stanza payload.
+
+ :param payload: The matched
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object.
+ :param bool instream: Force the handler to execute during stream
+ processing. This should only be used by
+ :meth:`prerun()`. Defaults to ``False``.
+ """
+ if not self._instream or instream:
+ self._pointer(payload)
+ if self._once:
+ self._destroy = True
+ del self._pointer
diff --git a/slixmpp/xmlstream/handler/collector.py b/slixmpp/xmlstream/handler/collector.py
new file mode 100644
index 00000000..d9e20279
--- /dev/null
+++ b/slixmpp/xmlstream/handler/collector.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.handler.collector
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+from queue import Queue, Empty
+
+from slixmpp.xmlstream.handler.base import BaseHandler
+
+
+log = logging.getLogger(__name__)
+
+
+class Collector(BaseHandler):
+
+ """
+ The Collector handler allows for collecting a set of stanzas
+ that match a given pattern. Unlike the Waiter handler, a
+ Collector does not block execution, and will continue to
+ accumulate matching stanzas until told to stop.
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~slixmpp.xmlstream.matcher.base.MatcherBase`
+ derived object for matching stanza objects.
+ :param stream: The :class:`~slixmpp.xmlstream.xmlstream.XMLStream`
+ instance this handler should monitor.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ BaseHandler.__init__(self, name, matcher, stream=stream)
+ self._payload = Queue()
+
+ def prerun(self, payload):
+ """Store the matched stanza when received during processing.
+
+ :param payload: The matched
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object.
+ """
+ self._payload.put(payload)
+
+ def run(self, payload):
+ """Do not process this handler during the main event loop."""
+ pass
+
+ def stop(self):
+ """
+ Stop collection of matching stanzas, and return the ones that
+ have been stored so far.
+ """
+ self._destroy = True
+ results = []
+ try:
+ while True:
+ results.append(self._payload.get(False))
+ except Empty:
+ pass
+
+ self.stream().remove_handler(self.name)
+ return results
diff --git a/slixmpp/xmlstream/handler/coroutine_callback.py b/slixmpp/xmlstream/handler/coroutine_callback.py
new file mode 100644
index 00000000..8ad9572e
--- /dev/null
+++ b/slixmpp/xmlstream/handler/coroutine_callback.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.handler.callback
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.handler.base import BaseHandler
+from slixmpp.xmlstream.asyncio import asyncio
+
+
+class CoroutineCallback(BaseHandler):
+
+ """
+ The Callback handler will execute a callback function with
+ matched stanzas.
+
+ The handler may execute the callback either during stream
+ processing or during the main event loop.
+
+ The event will be scheduled to be run soon in the event loop instead
+ of immediately.
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~slixmpp.xmlstream.matcher.base.MatcherBase`
+ derived object for matching stanza objects.
+ :param pointer: The function to execute during callback. If ``pointer``
+ is not a coroutine, this function will raise a ValueError.
+ :param bool once: Indicates if the handler should be used only
+ once. Defaults to False.
+ :param bool instream: Indicates if the callback should be executed
+ during stream processing instead of in the
+ main event loop.
+ :param stream: The :class:`~slixmpp.xmlstream.xmlstream.XMLStream`
+ instance this handler should monitor.
+ """
+
+ def __init__(self, name, matcher, pointer, once=False,
+ instream=False, stream=None):
+ BaseHandler.__init__(self, name, matcher, stream)
+ if not asyncio.iscoroutinefunction(pointer):
+ raise ValueError("Given function is not a coroutine")
+
+ @asyncio.coroutine
+ def pointer_wrapper(stanza, *args, **kwargs):
+ try:
+ yield from pointer(stanza, *args, **kwargs)
+ except Exception as e:
+ stanza.exception(e)
+
+ self._pointer = pointer_wrapper
+ self._once = once
+ self._instream = instream
+
+ def prerun(self, payload):
+ """Execute the callback during stream processing, if
+ the callback was created with ``instream=True``.
+
+ :param payload: The matched
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object.
+ """
+ if self._once:
+ self._destroy = True
+ if self._instream:
+ self.run(payload, True)
+
+ def run(self, payload, instream=False):
+ """Execute the callback function with the matched stanza payload.
+
+ :param payload: The matched
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object.
+ :param bool instream: Force the handler to execute during stream
+ processing. This should only be used by
+ :meth:`prerun()`. Defaults to ``False``.
+ """
+ if not self._instream or instream:
+ asyncio.async(self._pointer(payload))
+ if self._once:
+ self._destroy = True
+ del self._pointer
diff --git a/slixmpp/xmlstream/handler/waiter.py b/slixmpp/xmlstream/handler/waiter.py
new file mode 100644
index 00000000..c25063db
--- /dev/null
+++ b/slixmpp/xmlstream/handler/waiter.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.handler.waiter
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+from queue import Queue, Empty
+
+from slixmpp.xmlstream.handler.base import BaseHandler
+
+
+log = logging.getLogger(__name__)
+
+
+class Waiter(BaseHandler):
+
+ """
+ The Waiter handler allows an event handler to block until a
+ particular stanza has been received. The handler will either be
+ given the matched stanza, or ``False`` if the waiter has timed out.
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~slixmpp.xmlstream.matcher.base.MatcherBase`
+ derived object for matching stanza objects.
+ :param stream: The :class:`~slixmpp.xmlstream.xmlstream.XMLStream`
+ instance this handler should monitor.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ BaseHandler.__init__(self, name, matcher, stream=stream)
+ self._payload = Queue()
+
+ def prerun(self, payload):
+ """Store the matched stanza when received during processing.
+
+ :param payload: The matched
+ :class:`~slixmpp.xmlstream.stanzabase.ElementBase` object.
+ """
+ self._payload.put(payload)
+
+ def run(self, payload):
+ """Do not process this handler during the main event loop."""
+ pass
+
+ def wait(self, timeout=None):
+ """Block an event handler while waiting for a stanza to arrive.
+
+ Be aware that this will impact performance if called from a
+ non-threaded event handler.
+
+ Will return either the received stanza, or ``False`` if the
+ waiter timed out.
+
+ :param int timeout: The number of seconds to wait for the stanza
+ to arrive. Defaults to the the stream's
+ :class:`~slixmpp.xmlstream.xmlstream.XMLStream.response_timeout`
+ value.
+ """
+ if timeout is None:
+ timeout = self.stream().response_timeout
+
+ elapsed_time = 0
+ stanza = False
+ while elapsed_time < timeout and not self.stream().stop.is_set():
+ try:
+ stanza = self._payload.get(True, 1)
+ break
+ except Empty:
+ elapsed_time += 1
+ if elapsed_time >= timeout:
+ log.warning("Timed out waiting for %s", self.name)
+ self.stream().remove_handler(self.name)
+ return stanza
+
+ def check_delete(self):
+ """Always remove waiters after use."""
+ return True
diff --git a/slixmpp/xmlstream/handler/xmlcallback.py b/slixmpp/xmlstream/handler/xmlcallback.py
new file mode 100644
index 00000000..60ccbaed
--- /dev/null
+++ b/slixmpp/xmlstream/handler/xmlcallback.py
@@ -0,0 +1,36 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.handler import Callback
+
+
+class XMLCallback(Callback):
+
+ """
+ The XMLCallback class is identical to the normal Callback class,
+ except that XML contents of matched stanzas will be processed instead
+ of the stanza objects themselves.
+
+ Methods:
+ run -- Overrides Callback.run
+ """
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza's
+ XML contents, instead of the stanza itself.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ Callback.run(self, payload.xml, instream)
diff --git a/slixmpp/xmlstream/handler/xmlwaiter.py b/slixmpp/xmlstream/handler/xmlwaiter.py
new file mode 100644
index 00000000..dc014da0
--- /dev/null
+++ b/slixmpp/xmlstream/handler/xmlwaiter.py
@@ -0,0 +1,33 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.handler import Waiter
+
+
+class XMLWaiter(Waiter):
+
+ """
+ The XMLWaiter class is identical to the normal Waiter class
+ except that it returns the XML contents of the stanza instead
+ of the full stanza object itself.
+
+ Methods:
+ prerun -- Overrides Waiter.prerun
+ """
+
+ def prerun(self, payload):
+ """
+ Store the XML contents of the stanza to return to the
+ waiting event handler.
+
+ Overrides Waiter.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ Waiter.prerun(self, payload.xml)
diff --git a/slixmpp/xmlstream/matcher/__init__.py b/slixmpp/xmlstream/matcher/__init__.py
new file mode 100644
index 00000000..47487d4a
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/__init__.py
@@ -0,0 +1,17 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.matcher.id import MatcherId
+from slixmpp.xmlstream.matcher.idsender import MatchIDSender
+from slixmpp.xmlstream.matcher.many import MatchMany
+from slixmpp.xmlstream.matcher.stanzapath import StanzaPath
+from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
+from slixmpp.xmlstream.matcher.xpath import MatchXPath
+
+__all__ = ['MatcherId', 'MatchMany', 'StanzaPath',
+ 'MatchXMLMask', 'MatchXPath']
diff --git a/slixmpp/xmlstream/matcher/base.py b/slixmpp/xmlstream/matcher/base.py
new file mode 100644
index 00000000..4f15c63d
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/base.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.matcher.base
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+
+class MatcherBase(object):
+
+ """
+ Base class for stanza matchers. Stanza matchers are used to pick
+ stanzas out of the XML stream and pass them to the appropriate
+ stream handlers.
+
+ :param criteria: Object to compare some aspect of a stanza against.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = criteria
+
+ def match(self, xml):
+ """Check if a stanza matches the stored criteria.
+
+ Meant to be overridden.
+ """
+ return False
diff --git a/slixmpp/xmlstream/matcher/id.py b/slixmpp/xmlstream/matcher/id.py
new file mode 100644
index 00000000..ddef75dc
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/id.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.matcher.id
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+ """
+
+ def match(self, xml):
+ """Compare the given stanza's ``'id'`` attribute to the stored
+ ``id`` value.
+
+ :param xml: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ return xml['id'] == self._criteria
diff --git a/slixmpp/xmlstream/matcher/idsender.py b/slixmpp/xmlstream/matcher/idsender.py
new file mode 100644
index 00000000..79f73911
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/idsender.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.matcher.id
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchIDSender(MatcherBase):
+
+ """
+ The IDSender matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID, and that the 'from' value is one
+ of a set of approved entities that can respond to a request.
+ """
+
+ def match(self, xml):
+ """Compare the given stanza's ``'id'`` attribute to the stored
+ ``id`` value, and verify the sender's JID.
+
+ :param xml: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+
+ selfjid = self._criteria['self']
+ peerjid = self._criteria['peer']
+
+ allowed = {}
+ allowed[''] = True
+ allowed[selfjid.bare] = True
+ allowed[selfjid.host] = True
+ allowed[peerjid.full] = True
+ allowed[peerjid.bare] = True
+ allowed[peerjid.host] = True
+
+ _from = xml['from']
+
+ try:
+ return xml['id'] == self._criteria['id'] and allowed[_from]
+ except KeyError:
+ return False
diff --git a/slixmpp/xmlstream/matcher/many.py b/slixmpp/xmlstream/matcher/many.py
new file mode 100644
index 00000000..ef6a64d3
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/many.py
@@ -0,0 +1,40 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchMany(MatcherBase):
+
+ """
+ The MatchMany matcher may compare a stanza against multiple
+ criteria. It is essentially an OR relation combining multiple
+ matchers.
+
+ Each of the criteria must implement a match() method.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Match a stanza against multiple criteria. The match is successful
+ if one of the criteria matches.
+
+ Each of the criteria must implement a match() method.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ for m in self._criteria:
+ if m.match(xml):
+ return True
+ return False
diff --git a/slixmpp/xmlstream/matcher/stanzapath.py b/slixmpp/xmlstream/matcher/stanzapath.py
new file mode 100644
index 00000000..c9f245e1
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/stanzapath.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.matcher.stanzapath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.matcher.base import MatcherBase
+from slixmpp.xmlstream.stanzabase import fix_ns
+
+
+class StanzaPath(MatcherBase):
+
+ """
+ The StanzaPath matcher selects stanzas that match a given "stanza path",
+ which is similar to a normal XPath except that it uses the interfaces and
+ plugins of the stanza instead of the actual, underlying XML.
+
+ :param criteria: Object to compare some aspect of a stanza against.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = fix_ns(criteria, split=True,
+ propagate_ns=False,
+ default_ns='jabber:client')
+ self._raw_criteria = criteria
+
+ def match(self, stanza):
+ """
+ Compare a stanza against a "stanza path". A stanza path is similar to
+ an XPath expression, but uses the stanza's interfaces and plugins
+ instead of the underlying XML. See the documentation for the stanza
+ :meth:`~slixmpp.xmlstream.stanzabase.ElementBase.match()` method
+ for more information.
+
+ :param stanza: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ return stanza.match(self._criteria) or stanza.match(self._raw_criteria)
diff --git a/slixmpp/xmlstream/matcher/xmlmask.py b/slixmpp/xmlstream/matcher/xmlmask.py
new file mode 100644
index 00000000..7e26abe2
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/xmlmask.py
@@ -0,0 +1,117 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from xml.parsers.expat import ExpatError
+
+from slixmpp.xmlstream.stanzabase import ET
+from slixmpp.xmlstream.matcher.base import MatcherBase
+
+
+log = logging.getLogger(__name__)
+
+
+class MatchXMLMask(MatcherBase):
+
+ """
+ The XMLMask matcher selects stanzas whose XML matches a given
+ XML pattern, or mask. For example, message stanzas with body elements
+ could be matched using the mask:
+
+ .. code-block:: xml
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and
+ :class:`~slixmpp.xmlstream.matcher.xpath.MatchXPath` or
+ :class:`~slixmpp.xmlstream.matcher.stanzapath.StanzaPath`
+ should be used instead.
+
+ :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML
+ object or XML string to use as a mask.
+ """
+
+ def __init__(self, criteria, default_ns='jabber:client'):
+ MatcherBase.__init__(self, criteria)
+ if isinstance(criteria, str):
+ self._criteria = ET.fromstring(self._criteria)
+ self.default_ns = default_ns
+
+ def setDefaultNS(self, ns):
+ """Set the default namespace to use during comparisons.
+
+ :param ns: The new namespace to use as the default.
+ """
+ self.default_ns = ns
+
+ def match(self, xml):
+ """Compare a stanza object or XML object against the stored XML mask.
+
+ Overrides MatcherBase.match.
+
+ :param xml: The stanza object or XML object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ return self._mask_cmp(xml, self._criteria, True)
+
+ def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
+ """Compare an XML object against an XML mask.
+
+ :param source: The :class:`~xml.etree.ElementTree.Element` XML object
+ to compare against the mask.
+ :param mask: The :class:`~xml.etree.ElementTree.Element` XML object
+ serving as the mask.
+ :param use_ns: Indicates if namespaces should be respected during
+ the comparison.
+ :default_ns: The default namespace to apply to elements that
+ do not have a specified namespace.
+ Defaults to ``"__no_ns__"``.
+ """
+ if source is None:
+ # If the element was not found. May happend during recursive calls.
+ return False
+
+ # Convert the mask to an XML object if it is a string.
+ if not hasattr(mask, 'attrib'):
+ try:
+ mask = ET.fromstring(mask)
+ except ExpatError:
+ log.warning("Expat error: %s\nIn parsing: %s", '', mask)
+
+ mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
+ if source.tag not in [mask.tag, mask_ns_tag]:
+ return False
+
+ # If the mask includes text, compare it.
+ if mask.text and source.text and \
+ source.text.strip() != mask.text.strip():
+ return False
+
+ # Compare attributes. The stanza must include the attributes
+ # defined by the mask, but may include others.
+ for name, value in mask.attrib.items():
+ if source.attrib.get(name, "__None__") != value:
+ return False
+
+ # Recursively check subelements.
+ matched_elements = {}
+ for subelement in mask:
+ matched = False
+ for other in source.findall(subelement.tag):
+ matched_elements[other] = False
+ if self._mask_cmp(other, subelement, use_ns):
+ if not matched_elements.get(other, False):
+ matched_elements[other] = True
+ matched = True
+ if not matched:
+ return False
+
+ # Everything matches.
+ return True
diff --git a/slixmpp/xmlstream/matcher/xpath.py b/slixmpp/xmlstream/matcher/xpath.py
new file mode 100644
index 00000000..31ab1b8c
--- /dev/null
+++ b/slixmpp/xmlstream/matcher/xpath.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.matcher.xpath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.stanzabase import ET, fix_ns
+from slixmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchXPath(MatcherBase):
+
+ """
+ The XPath matcher selects stanzas whose XML contents matches a given
+ XPath expression.
+
+ .. warning::
+
+ Using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree
+ :meth:`~xml.etree.ElementTree.Element.find()` method does
+ not support the use of attribute selectors. If you need to
+ support Python 2.6 or 3.1, it might be more useful to use a
+ :class:`~slixmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher.
+
+ If the value of :data:`IGNORE_NS` is set to ``True``, then XPath
+ expressions will be matched without using namespaces.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = fix_ns(criteria)
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of :data:`IGNORE_NS` is set to ``True``, then XPath
+ expressions will be matched without using namespaces.
+
+ .. warning::
+
+ In Python 2.6 and 3.1 the ElementTree
+ :meth:`~xml.etree.ElementTree.Element.find()` method does not
+ support attribute selectors in the XPath expression.
+
+ :param xml: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ x = ET.Element('x')
+ x.append(xml)
+
+ return x.find(self._criteria) is not None
diff --git a/slixmpp/xmlstream/resolver.py b/slixmpp/xmlstream/resolver.py
new file mode 100644
index 00000000..778f7dc3
--- /dev/null
+++ b/slixmpp/xmlstream/resolver.py
@@ -0,0 +1,314 @@
+# -*- encoding: utf-8 -*-
+
+"""
+ slixmpp.xmlstream.dns
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ :copyright: (c) 2012 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from slixmpp.xmlstream.asyncio import asyncio
+import socket
+import logging
+import random
+
+
+log = logging.getLogger(__name__)
+
+
+#: Global flag indicating the availability of the ``aiodns`` package.
+#: Installing ``aiodns`` can be done via:
+#:
+#: .. code-block:: sh
+#:
+#: pip install aiodns
+AIODNS_AVAILABLE = False
+try:
+ import aiodns
+ AIODNS_AVAILABLE = True
+except ImportError as e:
+ log.debug("Could not find aiodns package. " + \
+ "Not all features will be available")
+
+
+def default_resolver(loop):
+ """Return a basic DNS resolver object.
+
+ :returns: A :class:`aiodns.DNSResolver` object if aiodns
+ is available. Otherwise, ``None``.
+ """
+ if AIODNS_AVAILABLE:
+ return aiodns.DNSResolver(loop=loop,
+ tries=1,
+ timeout=1.0)
+ return None
+
+
+@asyncio.coroutine
+def resolve(host, port=None, service=None, proto='tcp',
+ resolver=None, use_ipv6=True, use_aiodns=True, loop=None):
+ """Peform DNS resolution for a given hostname.
+
+ Resolution may perform SRV record lookups if a service and protocol
+ are specified. The returned addresses will be sorted according to
+ the SRV priorities and weights.
+
+ If no resolver is provided, the aiodns resolver will be used if
+ available. Otherwise the built-in socket facilities will be used,
+ but those do not provide SRV support.
+
+ If SRV records were used, queries to resolve alternative hosts will
+ be made as needed instead of all at once.
+
+ :param host: The hostname to resolve.
+ :param port: A default port to connect with. SRV records may
+ dictate use of a different port.
+ :param service: Optional SRV service name without leading underscore.
+ :param proto: Optional SRV protocol name without leading underscore.
+ :param resolver: Optionally provide a DNS resolver object that has
+ been custom configured.
+ :param use_ipv6: Optionally control the use of IPv6 in situations
+ where it is either not available, or performance
+ is degraded. Defaults to ``True``.
+ :param use_aiodns: Optionally control if aiodns is used to make
+ the DNS queries instead of the built-in DNS
+ library.
+
+ :type host: string
+ :type port: int
+ :type service: string
+ :type proto: string
+ :type resolver: :class:`aiodns.DNSResolver`
+ :type use_ipv6: bool
+ :type use_aiodns: bool
+
+ :return: An iterable of IP address, port pairs in the order
+ dictated by SRV priorities and weights, if applicable.
+ """
+
+ if not use_aiodns:
+ if AIODNS_AVAILABLE:
+ log.debug("DNS: Not using aiodns, but aiodns is installed.")
+ else:
+ log.debug("DNS: Not using aiodns.")
+
+ if not use_ipv6:
+ log.debug("DNS: Use of IPv6 has been disabled.")
+
+ if resolver is None and AIODNS_AVAILABLE and use_aiodns:
+ resolver = aiodns.DNSResolver(loop=loop)
+
+ # An IPv6 literal is allowed to be enclosed in square brackets, but
+ # the brackets must be stripped in order to process the literal;
+ # otherwise, things break.
+ host = host.strip('[]')
+
+ try:
+ # If `host` is an IPv4 literal, we can return it immediately.
+ ipv4 = socket.inet_aton(host)
+ return [(host, host, port)]
+ except socket.error:
+ pass
+
+ if use_ipv6:
+ try:
+ # Likewise, If `host` is an IPv6 literal, we can return
+ # it immediately.
+ if hasattr(socket, 'inet_pton'):
+ ipv6 = socket.inet_pton(socket.AF_INET6, host)
+ return [(host, host, port)]
+ except (socket.error, ValueError):
+ pass
+
+ # If no service was provided, then we can just do A/AAAA lookups on the
+ # provided host. Otherwise we need to get an ordered list of hosts to
+ # resolve based on SRV records.
+ if not service:
+ hosts = [(host, port)]
+ else:
+ hosts = yield from get_SRV(host, port, service, proto,
+ resolver=resolver,
+ use_aiodns=use_aiodns)
+ if not hosts:
+ hosts = [(host, port)]
+
+ results = []
+ for host, port in hosts:
+ if host == 'localhost':
+ if use_ipv6:
+ results.append((host, '::1', port))
+ results.append((host, '127.0.0.1', port))
+
+ if use_ipv6:
+ aaaa = yield from get_AAAA(host, resolver=resolver,
+ use_aiodns=use_aiodns, loop=loop)
+ for address in aaaa:
+ results.append((host, address, port))
+
+ a = yield from get_A(host, resolver=resolver,
+ use_aiodns=use_aiodns, loop=loop)
+ for address in a:
+ results.append((host, address, port))
+
+ return results
+
+@asyncio.coroutine
+def get_A(host, resolver=None, use_aiodns=True, loop=None):
+ """Lookup DNS A records for a given host.
+
+ If ``resolver`` is not provided, or is ``None``, then resolution will
+ be performed using the built-in :mod:`socket` module.
+
+ :param host: The hostname to resolve for A record IPv4 addresses.
+ :param resolver: Optional DNS resolver object to use for the query.
+ :param use_aiodns: Optionally control if aiodns is used to make
+ the DNS queries instead of the built-in DNS
+ library.
+
+ :type host: string
+ :type resolver: :class:`aiodns.DNSResolver` or ``None``
+ :type use_aiodns: bool
+
+ :return: A list of IPv4 literals.
+ """
+ log.debug("DNS: Querying %s for A records." % host)
+
+ # If not using aiodns, attempt lookup using the OS level
+ # getaddrinfo() method.
+ if resolver is None or not use_aiodns:
+ try:
+ recs = yield from loop.getaddrinfo(host, None,
+ family=socket.AF_INET,
+ type=socket.SOCK_STREAM)
+ return [rec[4][0] for rec in recs]
+ except socket.gaierror:
+ log.debug("DNS: Error retrieving A address info for %s." % host)
+ return []
+
+ # Using aiodns:
+ future = resolver.query(host, 'A')
+ try:
+ recs = yield from future
+ except Exception as e:
+ log.debug('DNS: Exception while querying for %s A records: %s', host, e)
+ recs = []
+ return [rec.host for rec in recs]
+
+
+@asyncio.coroutine
+def get_AAAA(host, resolver=None, use_aiodns=True, loop=None):
+ """Lookup DNS AAAA records for a given host.
+
+ If ``resolver`` is not provided, or is ``None``, then resolution will
+ be performed using the built-in :mod:`socket` module.
+
+ :param host: The hostname to resolve for AAAA record IPv6 addresses.
+ :param resolver: Optional DNS resolver object to use for the query.
+ :param use_aiodns: Optionally control if aiodns is used to make
+ the DNS queries instead of the built-in DNS
+ library.
+
+ :type host: string
+ :type resolver: :class:`aiodns.DNSResolver` or ``None``
+ :type use_aiodns: bool
+
+ :return: A list of IPv6 literals.
+ """
+ log.debug("DNS: Querying %s for AAAA records." % host)
+
+ # If not using aiodns, attempt lookup using the OS level
+ # getaddrinfo() method.
+ if resolver is None or not use_aiodns:
+ if not socket.has_ipv6:
+ log.debug("DNS: Unable to query %s for AAAA records: IPv6 is not supported", host)
+ return []
+ try:
+ recs = yield from loop.getaddrinfo(host, None,
+ family=socket.AF_INET6,
+ type=socket.SOCK_STREAM)
+ return [rec[4][0] for rec in recs]
+ except (OSError, socket.gaierror):
+ log.debug("DNS: Error retreiving AAAA address " + \
+ "info for %s." % host)
+ return []
+
+ # Using aiodns:
+ future = resolver.query(host, 'AAAA')
+ try:
+ recs = yield from future
+ except Exception as e:
+ log.debug('DNS: Exception while querying for %s AAAA records: %s', host, e)
+ recs = []
+ return recs
+
+@asyncio.coroutine
+def get_SRV(host, port, service, proto='tcp', resolver=None, use_aiodns=True):
+ """Perform SRV record resolution for a given host.
+
+ .. note::
+
+ This function requires the use of the ``aiodns`` package. Calling
+ :func:`get_SRV` without ``aiodns`` will return the provided host
+ and port without performing any DNS queries.
+
+ :param host: The hostname to resolve.
+ :param port: A default port to connect with. SRV records may
+ dictate use of a different port.
+ :param service: Optional SRV service name without leading underscore.
+ :param proto: Optional SRV protocol name without leading underscore.
+ :param resolver: Optionally provide a DNS resolver object that has
+ been custom configured.
+
+ :type host: string
+ :type port: int
+ :type service: string
+ :type proto: string
+ :type resolver: :class:`aiodns.DNSResolver`
+
+ :return: A list of hostname, port pairs in the order dictacted
+ by SRV priorities and weights.
+ """
+ if resolver is None or not use_aiodns:
+ log.warning("DNS: aiodns not found. Can not use SRV lookup.")
+ return [(host, port)]
+
+ log.debug("DNS: Querying SRV records for %s" % host)
+ try:
+ future = resolver.query('_%s._%s.%s' % (service, proto, host),
+ 'SRV')
+ recs = yield from future
+ except Exception as e:
+ log.debug('DNS: Exception while querying for %s SRV records: %s', host, e)
+ return []
+
+ answers = {}
+ for rec in recs:
+ if rec.priority not in answers:
+ answers[rec.priority] = []
+ if rec.weight == 0:
+ answers[rec.priority].insert(0, rec)
+ else:
+ answers[rec.priority].append(rec)
+
+ sorted_recs = []
+ for priority in sorted(answers.keys()):
+ while answers[priority]:
+ running_sum = 0
+ sums = {}
+ for rec in answers[priority]:
+ running_sum += rec.weight
+ sums[running_sum] = rec
+
+ selected = random.randint(0, running_sum + 1)
+ for running_sum in sums:
+ if running_sum >= selected:
+ rec = sums[running_sum]
+ host = rec.host
+ if host.endswith('.'):
+ host = host[:-1]
+ sorted_recs.append((host, rec.port))
+ answers[priority].remove(rec)
+ break
+
+ return sorted_recs
diff --git a/slixmpp/xmlstream/stanzabase.py b/slixmpp/xmlstream/stanzabase.py
new file mode 100644
index 00000000..1ddee825
--- /dev/null
+++ b/slixmpp/xmlstream/stanzabase.py
@@ -0,0 +1,1631 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.stanzabase
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ module implements a wrapper layer for XML objects
+ that allows them to be treated like dictionaries.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from __future__ import with_statement, unicode_literals
+
+import copy
+import logging
+import weakref
+from xml.etree import cElementTree as ET
+
+from slixmpp.xmlstream import JID
+from slixmpp.xmlstream.tostring import tostring
+from collections import OrderedDict
+
+
+log = logging.getLogger(__name__)
+
+
+# Used to check if an argument is an XML object.
+XML_TYPE = type(ET.Element('xml'))
+
+
+XML_NS = 'http://www.w3.org/XML/1998/namespace'
+
+
+def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False):
+ """
+ Associate a stanza object as a plugin for another stanza.
+
+ >>> from slixmpp.xmlstream import register_stanza_plugin
+ >>> register_stanza_plugin(Iq, CustomStanza)
+
+ Plugin stanzas marked as iterable will be included in the list of
+ substanzas for the parent, using ``parent['substanzas']``. If the
+ attribute ``plugin_multi_attrib`` was defined for the plugin, then
+ the substanza set can be filtered to only instances of the plugin
+ class. For example, given a plugin class ``Foo`` with
+ ``plugin_multi_attrib = 'foos'`` then::
+
+ parent['foos']
+
+ would return a collection of all ``Foo`` substanzas.
+
+ :param class stanza: The class of the parent stanza.
+ :param class plugin: The class of the plugin stanza.
+ :param bool iterable: Indicates if the plugin stanza should be
+ included in the parent stanza's iterable
+ ``'substanzas'`` interface results.
+ :param bool overrides: Indicates if the plugin should be allowed
+ to override the interface handlers for
+ the parent stanza, based on the plugin's
+ ``overrides`` field.
+
+ .. versionadded:: 1.0-Beta1
+ Made ``register_stanza_plugin`` the default name. The prior
+ ``registerStanzaPlugin`` function name remains as an alias.
+ """
+ tag = "{%s}%s" % (plugin.namespace, plugin.name)
+
+ # Prevent weird memory reference gotchas by ensuring
+ # that the parent stanza class has its own set of
+ # plugin info maps and is not using the mappings from
+ # an ancestor class (like ElementBase).
+ plugin_info = ('plugin_attrib_map', 'plugin_tag_map',
+ 'plugin_iterables', 'plugin_overrides')
+ for attr in plugin_info:
+ info = getattr(stanza, attr)
+ setattr(stanza, attr, info.copy())
+
+ stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
+ stanza.plugin_tag_map[tag] = plugin
+
+ if iterable:
+ stanza.plugin_iterables.add(plugin)
+ if plugin.plugin_multi_attrib:
+ multiplugin = multifactory(plugin, plugin.plugin_multi_attrib)
+ register_stanza_plugin(stanza, multiplugin)
+ if overrides:
+ for interface in plugin.overrides:
+ stanza.plugin_overrides[interface] = plugin.plugin_attrib
+
+
+def multifactory(stanza, plugin_attrib):
+ """
+ Returns a ElementBase class for handling reoccuring child stanzas
+ """
+
+ def plugin_filter(self):
+ return lambda x: isinstance(x, self._multistanza)
+
+ def plugin_lang_filter(self, lang):
+ return lambda x: isinstance(x, self._multistanza) and \
+ x['lang'] == lang
+
+ class Multi(ElementBase):
+ """
+ Template class for multifactory
+ """
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+
+ def get_multi(self, lang=None):
+ parent = self.parent()
+ if not lang or lang == '*':
+ res = filter(plugin_filter(self), parent)
+ else:
+ res = filter(plugin_filter(self, lang), parent)
+ return list(res)
+
+ def set_multi(self, val, lang=None):
+ parent = self.parent()
+ del_multi = getattr(self, 'del_%s' % plugin_attrib)
+ del_multi(lang)
+ for sub in val:
+ parent.append(sub)
+
+ def del_multi(self, lang=None):
+ parent = self.parent()
+ if not lang or lang == '*':
+ res = filter(plugin_filter(self), parent)
+ else:
+ res = filter(plugin_filter(self, lang), parent)
+ res = list(res)
+ if not res:
+ del parent.plugins[(plugin_attrib, None)]
+ parent.loaded_plugins.remove(plugin_attrib)
+ try:
+ parent.xml.remove(self.xml)
+ except ValueError:
+ pass
+ else:
+ for stanza in list(res):
+ parent.iterables.remove(stanza)
+ parent.xml.remove(stanza.xml)
+
+ Multi.is_extension = True
+ Multi.plugin_attrib = plugin_attrib
+ Multi._multistanza = stanza
+ Multi.interfaces = set([plugin_attrib])
+ Multi.lang_interfaces = set([plugin_attrib])
+ setattr(Multi, "get_%s" % plugin_attrib, get_multi)
+ setattr(Multi, "set_%s" % plugin_attrib, set_multi)
+ setattr(Multi, "del_%s" % plugin_attrib, del_multi)
+ return Multi
+
+
+def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''):
+ """Apply the stanza's namespace to elements in an XPath expression.
+
+ :param string xpath: The XPath expression to fix with namespaces.
+ :param bool split: Indicates if the fixed XPath should be left as a
+ list of element names with namespaces. Defaults to
+ False, which returns a flat string path.
+ :param bool propagate_ns: Overrides propagating parent element
+ namespaces to child elements. Useful if
+ you wish to simply split an XPath that has
+ non-specified namespaces, and child and
+ parent namespaces are known not to always
+ match. Defaults to True.
+ """
+ fixed = []
+ # Split the XPath into a series of blocks, where a block
+ # is started by an element with a namespace.
+ ns_blocks = xpath.split('{')
+ for ns_block in ns_blocks:
+ if '}' in ns_block:
+ # Apply the found namespace to following elements
+ # that do not have namespaces.
+ namespace = ns_block.split('}')[0]
+ elements = ns_block.split('}')[1].split('/')
+ else:
+ # Apply the stanza's namespace to the following
+ # elements since no namespace was provided.
+ namespace = default_ns
+ elements = ns_block.split('/')
+
+ for element in elements:
+ if element:
+ # Skip empty entry artifacts from splitting.
+ if propagate_ns and element[0] != '*':
+ tag = '{%s}%s' % (namespace, element)
+ else:
+ tag = element
+ fixed.append(tag)
+ if split:
+ return fixed
+ return '/'.join(fixed)
+
+
+class ElementBase(object):
+
+ """
+ The core of Slixmpp's stanza XML manipulation and handling is provided
+ by ElementBase. ElementBase wraps XML cElementTree objects and enables
+ access to the XML contents through dictionary syntax, similar in style
+ to the Ruby XMPP library Blather's stanza implementation.
+
+ Stanzas are defined by their name, namespace, and interfaces. For
+ example, a simplistic Message stanza could be defined as::
+
+ >>> class Message(ElementBase):
+ ... name = "message"
+ ... namespace = "jabber:client"
+ ... interfaces = set(('to', 'from', 'type', 'body'))
+ ... sub_interfaces = set(('body',))
+
+ The resulting Message stanza's contents may be accessed as so::
+
+ >>> message['to'] = "user@example.com"
+ >>> message['body'] = "Hi!"
+ >>> message['body']
+ "Hi!"
+ >>> del message['body']
+ >>> message['body']
+ ""
+
+ The interface values map to either custom access methods, stanza
+ XML attributes, or (if the interface is also in sub_interfaces) the
+ text contents of a stanza's subelement.
+
+ Custom access methods may be created by adding methods of the
+ form "getInterface", "setInterface", or "delInterface", where
+ "Interface" is the titlecase version of the interface name.
+
+ Stanzas may be extended through the use of plugins. A plugin
+ is simply a stanza that has a plugin_attrib value. For example::
+
+ >>> class MessagePlugin(ElementBase):
+ ... name = "custom_plugin"
+ ... namespace = "custom"
+ ... interfaces = set(('useful_thing', 'custom'))
+ ... plugin_attrib = "custom"
+
+ The plugin stanza class must be associated with its intended
+ container stanza by using register_stanza_plugin as so::
+
+ >>> register_stanza_plugin(Message, MessagePlugin)
+
+ The plugin may then be accessed as if it were built-in to the parent
+ stanza::
+
+ >>> message['custom']['useful_thing'] = 'foo'
+
+ If a plugin provides an interface that is the same as the plugin's
+ plugin_attrib value, then the plugin's interface may be assigned
+ directly from the parent stanza, as shown below, but retrieving
+ information will require all interfaces to be used, as so::
+
+ >>> # Same as using message['custom']['custom']
+ >>> message['custom'] = 'bar'
+ >>> # Must use all interfaces
+ >>> message['custom']['custom']
+ 'bar'
+
+ If the plugin sets :attr:`is_extension` to ``True``, then both setting
+ and getting an interface value that is the same as the plugin's
+ plugin_attrib value will work, as so::
+
+ >>> message['custom'] = 'bar' # Using is_extension=True
+ >>> message['custom']
+ 'bar'
+
+
+ :param xml: Initialize the stanza object with an existing XML object.
+ :param parent: Optionally specify a parent stanza object will
+ contain this substanza.
+ """
+
+ #: The XML tag name of the element, not including any namespace
+ #: prefixes. For example, an :class:`ElementBase` object for
+ #: ``<message />`` would use ``name = 'message'``.
+ name = 'stanza'
+
+ #: The XML namespace for the element. Given ``<foo xmlns="bar" />``,
+ #: then ``namespace = "bar"`` should be used. The default namespace
+ #: is ``jabber:client`` since this is being used in an XMPP library.
+ namespace = 'jabber:client'
+
+ #: For :class:`ElementBase` subclasses which are intended to be used
+ #: as plugins, the ``plugin_attrib`` value defines the plugin name.
+ #: Plugins may be accessed by using the ``plugin_attrib`` value as
+ #: the interface. An example using ``plugin_attrib = 'foo'``::
+ #:
+ #: register_stanza_plugin(Message, FooPlugin)
+ #: msg = Message()
+ #: msg['foo']['an_interface_from_the_foo_plugin']
+ plugin_attrib = 'plugin'
+
+ #: For :class:`ElementBase` subclasses that are intended to be an
+ #: iterable group of items, the ``plugin_multi_attrib`` value defines
+ #: an interface for the parent stanza which returns the entire group
+ #: of matching substanzas. So the following are equivalent::
+ #:
+ #: # Given stanza class Foo, with plugin_multi_attrib = 'foos'
+ #: parent['foos']
+ #: filter(isinstance(item, Foo), parent['substanzas'])
+ plugin_multi_attrib = ''
+
+ #: The set of keys that the stanza provides for accessing and
+ #: manipulating the underlying XML object. This set may be augmented
+ #: with the :attr:`plugin_attrib` value of any registered
+ #: stanza plugins.
+ interfaces = set(('type', 'to', 'from', 'id', 'payload'))
+
+ #: A subset of :attr:`interfaces` which maps interfaces to direct
+ #: subelements of the underlying XML object. Using this set, the text
+ #: of these subelements may be set, retrieved, or removed without
+ #: needing to define custom methods.
+ sub_interfaces = set()
+
+ #: A subset of :attr:`interfaces` which maps the presence of
+ #: subelements to boolean values. Using this set allows for quickly
+ #: checking for the existence of empty subelements like ``<required />``.
+ #:
+ #: .. versionadded:: 1.1
+ bool_interfaces = set()
+
+ #: .. versionadded:: 1.1.2
+ lang_interfaces = set()
+
+ #: In some cases you may wish to override the behaviour of one of the
+ #: parent stanza's interfaces. The ``overrides`` list specifies the
+ #: interface name and access method to be overridden. For example,
+ #: to override setting the parent's ``'condition'`` interface you
+ #: would use::
+ #:
+ #: overrides = ['set_condition']
+ #:
+ #: Getting and deleting the ``'condition'`` interface would not
+ #: be affected.
+ #:
+ #: .. versionadded:: 1.0-Beta5
+ overrides = []
+
+ #: If you need to add a new interface to an existing stanza, you
+ #: can create a plugin and set ``is_extension = True``. Be sure
+ #: to set the :attr:`plugin_attrib` value to the desired interface
+ #: name, and that it is the only interface listed in
+ #: :attr:`interfaces`. Requests for the new interface from the
+ #: parent stanza will be passed to the plugin directly.
+ #:
+ #: .. versionadded:: 1.0-Beta5
+ is_extension = False
+
+ #: A map of interface operations to the overriding functions.
+ #: For example, after overriding the ``set`` operation for
+ #: the interface ``body``, :attr:`plugin_overrides` would be::
+ #:
+ #: {'set_body': <some function>}
+ #:
+ #: .. versionadded: 1.0-Beta5
+ plugin_overrides = {}
+
+ #: A mapping of the :attr:`plugin_attrib` values of registered
+ #: plugins to their respective classes.
+ plugin_attrib_map = {}
+
+ #: A mapping of root element tag names (in ``'{namespace}elementname'``
+ #: format) to the plugin classes responsible for them.
+ plugin_tag_map = {}
+
+ #: The set of stanza classes that can be iterated over using
+ #: the 'substanzas' interface. Classes are added to this set
+ #: when registering a plugin with ``iterable=True``::
+ #:
+ #: register_stanza_plugin(DiscoInfo, DiscoItem, iterable=True)
+ #:
+ #: .. versionadded:: 1.0-Beta5
+ plugin_iterables = set()
+
+ #: A deprecated version of :attr:`plugin_iterables` that remains
+ #: for backward compatibility. It required a parent stanza to
+ #: know beforehand what stanza classes would be iterable::
+ #:
+ #: class DiscoItem(ElementBase):
+ #: ...
+ #:
+ #: class DiscoInfo(ElementBase):
+ #: subitem = (DiscoItem, )
+ #: ...
+ #:
+ #: .. deprecated:: 1.0-Beta5
+ subitem = set()
+
+ #: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``.
+ xml_ns = XML_NS
+
+ def __init__(self, xml=None, parent=None):
+ self._index = 0
+
+ #: The underlying XML object for the stanza. It is a standard
+ #: :class:`xml.etree.cElementTree` object.
+ self.xml = xml
+
+ #: An ordered dictionary of plugin stanzas, mapped by their
+ #: :attr:`plugin_attrib` value.
+ self.plugins = OrderedDict()
+ self.loaded_plugins = set()
+
+ #: A list of child stanzas whose class is included in
+ #: :attr:`plugin_iterables`.
+ self.iterables = []
+
+ #: The name of the tag for the stanza's root element. It is the
+ #: same as calling :meth:`tag_name()` and is formatted as
+ #: ``'{namespace}elementname'``.
+ self.tag = self.tag_name()
+
+ #: A :class:`weakref.weakref` to the parent stanza, if there is one.
+ #: If not, then :attr:`parent` is ``None``.
+ self.parent = None
+ if parent is not None:
+ if not isinstance(parent, weakref.ReferenceType):
+ self.parent = weakref.ref(parent)
+ else:
+ self.parent = parent
+
+ if self.subitem is not None:
+ for sub in self.subitem:
+ self.plugin_iterables.add(sub)
+
+ if self.setup(xml):
+ # If we generated our own XML, then everything is ready.
+ return
+
+ # Initialize values using provided XML
+ for child in self.xml:
+ if child.tag in self.plugin_tag_map:
+ plugin_class = self.plugin_tag_map[child.tag]
+ self.init_plugin(plugin_class.plugin_attrib,
+ existing_xml=child,
+ reuse=False)
+
+ def setup(self, xml=None):
+ """Initialize the stanza's XML contents.
+
+ Will return ``True`` if XML was generated according to the stanza's
+ definition instead of building a stanza object from an existing
+ XML object.
+
+ :param xml: An existing XML object to use for the stanza's content
+ instead of generating new XML.
+ """
+ if self.xml is None:
+ self.xml = xml
+
+ last_xml = self.xml
+ if self.xml is None:
+ # Generate XML from the stanza definition
+ for ename in self.name.split('/'):
+ new = ET.Element("{%s}%s" % (self.namespace, ename))
+ if self.xml is None:
+ self.xml = new
+ else:
+ last_xml.append(new)
+ last_xml = new
+ if self.parent is not None:
+ self.parent().xml.append(self.xml)
+
+ # We had to generate XML
+ return True
+ else:
+ # We did not generate XML
+ return False
+
+ def enable(self, attrib, lang=None):
+ """Enable and initialize a stanza plugin.
+
+ Alias for :meth:`init_plugin`.
+
+ :param string attrib: The :attr:`plugin_attrib` value of the
+ plugin to enable.
+ """
+ return self.init_plugin(attrib, lang)
+
+ def _get_plugin(self, name, lang=None, check=False):
+ if lang is None:
+ lang = self.get_lang()
+
+ if name not in self.plugin_attrib_map:
+ return None
+
+ plugin_class = self.plugin_attrib_map[name]
+
+ if plugin_class.is_extension:
+ if (name, None) in self.plugins:
+ return self.plugins[(name, None)]
+ else:
+ return None if check else self.init_plugin(name, lang)
+ else:
+ if (name, lang) in self.plugins:
+ return self.plugins[(name, lang)]
+ else:
+ return None if check else self.init_plugin(name, lang)
+
+ def init_plugin(self, attrib, lang=None, existing_xml=None, reuse=True):
+ """Enable and initialize a stanza plugin.
+
+ :param string attrib: The :attr:`plugin_attrib` value of the
+ plugin to enable.
+ """
+ default_lang = self.get_lang()
+ if not lang:
+ lang = default_lang
+
+ plugin_class = self.plugin_attrib_map[attrib]
+
+ if plugin_class.is_extension and (attrib, None) in self.plugins:
+ return self.plugins[(attrib, None)]
+ if reuse and (attrib, lang) in self.plugins:
+ return self.plugins[(attrib, lang)]
+
+ plugin = plugin_class(parent=self, xml=existing_xml)
+
+ if plugin.is_extension:
+ self.plugins[(attrib, None)] = plugin
+ else:
+ if lang != default_lang:
+ plugin['lang'] = lang
+ self.plugins[(attrib, lang)] = plugin
+
+ if plugin_class in self.plugin_iterables:
+ self.iterables.append(plugin)
+ if plugin_class.plugin_multi_attrib:
+ self.init_plugin(plugin_class.plugin_multi_attrib)
+
+ self.loaded_plugins.add(attrib)
+
+ return plugin
+
+ def _get_stanza_values(self):
+ """Return A JSON/dictionary version of the XML content
+ exposed through the stanza's interfaces::
+
+ >>> msg = Message()
+ >>> msg.values
+ {'body': '', 'from': , 'mucnick': '', 'mucroom': '',
+ 'to': , 'type': 'normal', 'id': '', 'subject': ''}
+
+ Likewise, assigning to :attr:`values` will change the XML
+ content::
+
+ >>> msg = Message()
+ >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'}
+ >>> msg
+ '<message to="user@example.com"><body>Hi!</body></message>'
+
+ .. versionadded:: 1.0-Beta1
+ """
+ values = OrderedDict()
+ values['lang'] = self['lang']
+ for interface in self.interfaces:
+ if isinstance(self[interface], JID):
+ values[interface] = self[interface].jid
+ else:
+ values[interface] = self[interface]
+ if interface in self.lang_interfaces:
+ values['%s|*' % interface] = self['%s|*' % interface]
+ for plugin, stanza in self.plugins.items():
+ lang = stanza['lang']
+ if lang:
+ values['%s|%s' % (plugin[0], lang)] = stanza.values
+ else:
+ values[plugin[0]] = stanza.values
+ if self.iterables:
+ iterables = []
+ for stanza in self.iterables:
+ iterables.append(stanza.values)
+ iterables[-1]['__childtag__'] = stanza.tag
+ values['substanzas'] = iterables
+ return values
+
+ def _set_stanza_values(self, values):
+ """Set multiple stanza interface values using a dictionary.
+
+ Stanza plugin values may be set using nested dictionaries.
+
+ :param values: A dictionary mapping stanza interface with values.
+ Plugin interfaces may accept a nested dictionary that
+ will be used recursively.
+
+ .. versionadded:: 1.0-Beta1
+ """
+ iterable_interfaces = [p.plugin_attrib for \
+ p in self.plugin_iterables]
+
+ if 'lang' in values:
+ self['lang'] = values['lang']
+
+ if 'substanzas' in values:
+ # Remove existing substanzas
+ for stanza in self.iterables:
+ try:
+ self.xml.remove(stanza.xml)
+ except ValueError:
+ pass
+ self.iterables = []
+
+ # Add new substanzas
+ for subdict in values['substanzas']:
+ if '__childtag__' in subdict:
+ for subclass in self.plugin_iterables:
+ child_tag = "{%s}%s" % (subclass.namespace,
+ subclass.name)
+ if subdict['__childtag__'] == child_tag:
+ sub = subclass(parent=self)
+ sub.values = subdict
+ self.iterables.append(sub)
+
+ for interface, value in values.items():
+ full_interface = interface
+ interface_lang = ('%s|' % interface).split('|')
+ interface = interface_lang[0]
+ lang = interface_lang[1] or self.get_lang()
+
+ if interface == 'lang':
+ continue
+ elif interface == 'substanzas':
+ continue
+ elif interface in self.interfaces:
+ self[full_interface] = value
+ elif interface in self.plugin_attrib_map:
+ if interface not in iterable_interfaces:
+ plugin = self._get_plugin(interface, lang)
+ if plugin:
+ plugin.values = value
+ return self
+
+ def __getitem__(self, attrib):
+ """Return the value of a stanza interface using dict-like syntax.
+
+ Example::
+
+ >>> msg['body']
+ 'Message contents'
+
+ Stanza interfaces are typically mapped directly to the underlying XML
+ object, but can be overridden by the presence of a ``get_attrib``
+ method (or ``get_foo`` where the interface is named ``'foo'``, etc).
+
+ The search order for interface value retrieval for an interface
+ named ``'foo'`` is:
+
+ 1. The list of substanzas (``'substanzas'``)
+ 2. The result of calling the ``get_foo`` override handler.
+ 3. The result of calling ``get_foo``.
+ 4. The result of calling ``getFoo``.
+ 5. The contents of the ``foo`` subelement, if ``foo`` is listed
+ in :attr:`sub_interfaces`.
+ 6. True or False depending on the existence of a ``foo``
+ subelement and ``foo`` is in :attr:`bool_interfaces`.
+ 7. The value of the ``foo`` attribute of the XML object.
+ 8. The plugin named ``'foo'``
+ 9. An empty string.
+
+ :param string attrib: The name of the requested stanza interface.
+ """
+ full_attrib = attrib
+ attrib_lang = ('%s|' % attrib).split('|')
+ attrib = attrib_lang[0]
+ lang = attrib_lang[1] or None
+
+ kwargs = {}
+ if lang and attrib in self.lang_interfaces:
+ kwargs['lang'] = lang
+
+ kwargs = OrderedDict(kwargs)
+
+ if attrib == 'substanzas':
+ return self.iterables
+ elif attrib in self.interfaces or attrib == 'lang':
+ get_method = "get_%s" % attrib.lower()
+ get_method2 = "get%s" % attrib.title()
+
+ if self.plugin_overrides:
+ name = self.plugin_overrides.get(get_method, None)
+ if name:
+ plugin = self._get_plugin(name, lang)
+ if plugin:
+ handler = getattr(plugin, get_method, None)
+ if handler:
+ return handler(**kwargs)
+
+ if hasattr(self, get_method):
+ return getattr(self, get_method)(**kwargs)
+ elif hasattr(self, get_method2):
+ return getattr(self, get_method2)(**kwargs)
+ else:
+ if attrib in self.sub_interfaces:
+ return self._get_sub_text(attrib, lang=lang)
+ elif attrib in self.bool_interfaces:
+ elem = self.xml.find('{%s}%s' % (self.namespace, attrib))
+ return elem is not None
+ else:
+ return self._get_attr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ plugin = self._get_plugin(attrib, lang)
+ if plugin and plugin.is_extension:
+ return plugin[full_attrib]
+ return plugin
+ else:
+ return ''
+
+ def __setitem__(self, attrib, value):
+ """Set the value of a stanza interface using dictionary-like syntax.
+
+ Example::
+
+ >>> msg['body'] = "Hi!"
+ >>> msg['body']
+ 'Hi!'
+
+ Stanza interfaces are typically mapped directly to the underlying XML
+ object, but can be overridden by the presence of a ``set_attrib``
+ method (or ``set_foo`` where the interface is named ``'foo'``, etc).
+
+ The effect of interface value assignment for an interface
+ named ``'foo'`` will be one of:
+
+ 1. Delete the interface's contents if the value is None.
+ 2. Call the ``set_foo`` override handler, if it exists.
+ 3. Call ``set_foo``, if it exists.
+ 4. Call ``setFoo``, if it exists.
+ 5. Set the text of a ``foo`` element, if ``'foo'`` is
+ in :attr:`sub_interfaces`.
+ 6. Add or remove an empty subelement ``foo``
+ if ``foo`` is in :attr:`bool_interfaces`.
+ 7. Set the value of a top level XML attribute named ``foo``.
+ 8. Attempt to pass the value to a plugin named ``'foo'`` using
+ the plugin's ``'foo'`` interface.
+ 9. Do nothing.
+
+ :param string attrib: The name of the stanza interface to modify.
+ :param value: The new value of the stanza interface.
+ """
+ full_attrib = attrib
+ attrib_lang = ('%s|' % attrib).split('|')
+ attrib = attrib_lang[0]
+ lang = attrib_lang[1] or None
+
+ kwargs = {}
+ if lang and attrib in self.lang_interfaces:
+ kwargs['lang'] = lang
+
+ kwargs = OrderedDict(kwargs)
+
+ if attrib in self.interfaces or attrib == 'lang':
+ if value is not None:
+ set_method = "set_%s" % attrib.lower()
+ set_method2 = "set%s" % attrib.title()
+
+ if self.plugin_overrides:
+ name = self.plugin_overrides.get(set_method, None)
+ if name:
+ plugin = self._get_plugin(name, lang)
+ if plugin:
+ handler = getattr(plugin, set_method, None)
+ if handler:
+ return handler(value, **kwargs)
+
+ if hasattr(self, set_method):
+ getattr(self, set_method)(value, **kwargs)
+ elif hasattr(self, set_method2):
+ getattr(self, set_method2)(value, **kwargs)
+ else:
+ if attrib in self.sub_interfaces:
+ if lang == '*':
+ return self._set_all_sub_text(attrib,
+ value,
+ lang='*')
+ return self._set_sub_text(attrib, text=value,
+ lang=lang)
+ elif attrib in self.bool_interfaces:
+ if value:
+ return self._set_sub_text(attrib, '',
+ keep=True,
+ lang=lang)
+ else:
+ return self._set_sub_text(attrib, '',
+ keep=False,
+ lang=lang)
+ else:
+ self._set_attr(attrib, value)
+ else:
+ self.__delitem__(attrib)
+ elif attrib in self.plugin_attrib_map:
+ plugin = self._get_plugin(attrib, lang)
+ if plugin:
+ plugin[full_attrib] = value
+ return self
+
+ def __delitem__(self, attrib):
+ """Delete the value of a stanza interface using dict-like syntax.
+
+ Example::
+
+ >>> msg['body'] = "Hi!"
+ >>> msg['body']
+ 'Hi!'
+ >>> del msg['body']
+ >>> msg['body']
+ ''
+
+ Stanza interfaces are typically mapped directly to the underlyig XML
+ object, but can be overridden by the presence of a ``del_attrib``
+ method (or ``del_foo`` where the interface is named ``'foo'``, etc).
+
+ The effect of deleting a stanza interface value named ``foo`` will be
+ one of:
+
+ 1. Call ``del_foo`` override handler, if it exists.
+ 2. Call ``del_foo``, if it exists.
+ 3. Call ``delFoo``, if it exists.
+ 4. Delete ``foo`` element, if ``'foo'`` is in
+ :attr:`sub_interfaces`.
+ 5. Remove ``foo`` element if ``'foo'`` is in
+ :attr:`bool_interfaces`.
+ 6. Delete top level XML attribute named ``foo``.
+ 7. Remove the ``foo`` plugin, if it was loaded.
+ 8. Do nothing.
+
+ :param attrib: The name of the affected stanza interface.
+ """
+ full_attrib = attrib
+ attrib_lang = ('%s|' % attrib).split('|')
+ attrib = attrib_lang[0]
+ lang = attrib_lang[1] or None
+
+ kwargs = {}
+ if lang and attrib in self.lang_interfaces:
+ kwargs['lang'] = lang
+
+ kwargs = OrderedDict(kwargs)
+
+ if attrib in self.interfaces or attrib == 'lang':
+ del_method = "del_%s" % attrib.lower()
+ del_method2 = "del%s" % attrib.title()
+
+ if self.plugin_overrides:
+ name = self.plugin_overrides.get(del_method, None)
+ if name:
+ plugin = self._get_plugin(attrib, lang)
+ if plugin:
+ handler = getattr(plugin, del_method, None)
+ if handler:
+ return handler(**kwargs)
+
+ if hasattr(self, del_method):
+ getattr(self, del_method)(**kwargs)
+ elif hasattr(self, del_method2):
+ getattr(self, del_method2)(**kwargs)
+ else:
+ if attrib in self.sub_interfaces:
+ return self._del_sub(attrib, lang=lang)
+ elif attrib in self.bool_interfaces:
+ return self._del_sub(attrib, lang=lang)
+ else:
+ self._del_attr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ plugin = self._get_plugin(attrib, lang, check=True)
+ if not plugin:
+ return self
+ if plugin.is_extension:
+ del plugin[full_attrib]
+ del self.plugins[(attrib, None)]
+ else:
+ del self.plugins[(attrib, plugin['lang'])]
+ self.loaded_plugins.remove(attrib)
+ try:
+ self.xml.remove(plugin.xml)
+ except ValueError:
+ pass
+ return self
+
+ def _set_attr(self, name, value):
+ """Set the value of a top level attribute of the XML object.
+
+ If the new value is None or an empty string, then the attribute will
+ be removed.
+
+ :param name: The name of the attribute.
+ :param value: The new value of the attribute, or None or '' to
+ remove it.
+ """
+ if value is None or value == '':
+ self.__delitem__(name)
+ else:
+ self.xml.attrib[name] = value
+
+ def _del_attr(self, name):
+ """Remove a top level attribute of the XML object.
+
+ :param name: The name of the attribute.
+ """
+ if name in self.xml.attrib:
+ del self.xml.attrib[name]
+
+ def _get_attr(self, name, default=''):
+ """Return the value of a top level attribute of the XML object.
+
+ In case the attribute has not been set, a default value can be
+ returned instead. An empty string is returned if no other default
+ is supplied.
+
+ :param name: The name of the attribute.
+ :param default: Optional value to return if the attribute has not
+ been set. An empty string is returned otherwise.
+ """
+ return self.xml.attrib.get(name, default)
+
+ def _get_sub_text(self, name, default='', lang=None):
+ """Return the text contents of a sub element.
+
+ In case the element does not exist, or it has no textual content,
+ a default value can be returned instead. An empty string is returned
+ if no other default is supplied.
+
+ :param name: The name or XPath expression of the element.
+ :param default: Optional default to return if the element does
+ not exists. An empty string is returned otherwise.
+ """
+ name = self._fix_ns(name)
+ if lang == '*':
+ return self._get_all_sub_text(name, default, None)
+
+ default_lang = self.get_lang()
+ if not lang:
+ lang = default_lang
+
+ stanzas = self.xml.findall(name)
+ if not stanzas:
+ return default
+ for stanza in stanzas:
+ if stanza.attrib.get('{%s}lang' % XML_NS, default_lang) == lang:
+ if stanza.text is None:
+ return default
+ return stanza.text
+ return default
+
+ def _get_all_sub_text(self, name, default='', lang=None):
+ name = self._fix_ns(name)
+
+ default_lang = self.get_lang()
+ results = OrderedDict()
+ stanzas = self.xml.findall(name)
+ if stanzas:
+ for stanza in stanzas:
+ stanza_lang = stanza.attrib.get('{%s}lang' % XML_NS,
+ default_lang)
+ if not lang or lang == '*' or stanza_lang == lang:
+ results[stanza_lang] = stanza.text
+ return results
+
+ def _set_sub_text(self, name, text=None, keep=False, lang=None):
+ """Set the text contents of a sub element.
+
+ In case the element does not exist, a element will be created,
+ and its text contents will be set.
+
+ If the text is set to an empty string, or None, then the
+ element will be removed, unless keep is set to True.
+
+ :param name: The name or XPath expression of the element.
+ :param text: The new textual content of the element. If the text
+ is an empty string or None, the element will be removed
+ unless the parameter keep is True.
+ :param keep: Indicates if the element should be kept if its text is
+ removed. Defaults to False.
+ """
+ default_lang = self.get_lang()
+ if lang is None:
+ lang = default_lang
+
+ if not text and not keep:
+ return self._del_sub(name, lang=lang)
+
+ path = self._fix_ns(name, split=True)
+ name = path[-1]
+ parent = self.xml
+
+ # The first goal is to find the parent of the subelement, or, if
+ # we can't find that, the closest grandparent element.
+ missing_path = []
+ search_order = path[:-1]
+ while search_order:
+ parent = self.xml.find('/'.join(search_order))
+ ename = search_order.pop()
+ if parent is not None:
+ break
+ else:
+ missing_path.append(ename)
+ missing_path.reverse()
+
+ # Find all existing elements that match the desired
+ # element path (there may be multiples due to different
+ # languages values).
+ if parent is not None:
+ elements = self.xml.findall('/'.join(path))
+ else:
+ parent = self.xml
+ elements = []
+
+ # Insert the remaining grandparent elements that don't exist yet.
+ for ename in missing_path:
+ element = ET.Element(ename)
+ parent.append(element)
+ parent = element
+
+ # Re-use an existing element with the proper language, if one exists.
+ for element in elements:
+ elang = element.attrib.get('{%s}lang' % XML_NS, default_lang)
+ if not lang and elang == default_lang or lang and lang == elang:
+ element.text = text
+ return element
+
+ # No useable element exists, so create a new one.
+ element = ET.Element(name)
+ element.text = text
+ if lang and lang != default_lang:
+ element.attrib['{%s}lang' % XML_NS] = lang
+ parent.append(element)
+ return element
+
+ def _set_all_sub_text(self, name, values, keep=False, lang=None):
+ self._del_sub(name, lang)
+ for value_lang, value in values.items():
+ if not lang or lang == '*' or value_lang == lang:
+ self._set_sub_text(name, text=value,
+ keep=keep,
+ lang=value_lang)
+
+ def _del_sub(self, name, all=False, lang=None):
+ """Remove sub elements that match the given name or XPath.
+
+ If the element is in a path, then any parent elements that become
+ empty after deleting the element may also be deleted if requested
+ by setting all=True.
+
+ :param name: The name or XPath expression for the element(s) to remove.
+ :param bool all: If True, remove all empty elements in the path to the
+ deleted element. Defaults to False.
+ """
+ path = self._fix_ns(name, split=True)
+ original_target = path[-1]
+
+ default_lang = self.get_lang()
+ if not lang:
+ lang = default_lang
+
+ for level, _ in enumerate(path):
+ # Generate the paths to the target elements and their parent.
+ element_path = "/".join(path[:len(path) - level])
+ parent_path = "/".join(path[:len(path) - level - 1])
+
+ elements = self.xml.findall(element_path)
+ parent = self.xml.find(parent_path)
+
+ if elements:
+ if parent is None:
+ parent = self.xml
+ for element in elements:
+ if element.tag == original_target or not list(element):
+ # Only delete the originally requested elements, and
+ # any parent elements that have become empty.
+ elem_lang = element.attrib.get('{%s}lang' % XML_NS,
+ default_lang)
+ if lang == '*' or elem_lang == lang:
+ parent.remove(element)
+ if not all:
+ # If we don't want to delete elements up the tree, stop
+ # after deleting the first level of elements.
+ return
+
+ def match(self, xpath):
+ """Compare a stanza object with an XPath-like expression.
+
+ If the XPath matches the contents of the stanza object, the match
+ is successful.
+
+ The XPath expression may include checks for stanza attributes.
+ For example::
+
+ 'presence@show=xa@priority=2/status'
+
+ Would match a presence stanza whose show value is set to ``'xa'``,
+ has a priority value of ``'2'``, and has a status element.
+
+ :param string xpath: The XPath expression to check against. It
+ may be either a string or a list of element
+ names with attribute checks.
+ """
+ if not isinstance(xpath, list):
+ xpath = self._fix_ns(xpath, split=True, propagate_ns=False)
+
+ # Extract the tag name and attribute checks for the first XPath node.
+ components = xpath[0].split('@')
+ tag = components[0]
+ attributes = components[1:]
+
+ if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
+ tag not in self.loaded_plugins and tag not in self.plugin_attrib:
+ # The requested tag is not in this stanza, so no match.
+ return False
+
+ # Check the rest of the XPath against any substanzas.
+ matched_substanzas = False
+ for substanza in self.iterables:
+ if xpath[1:] == []:
+ break
+ matched_substanzas = substanza.match(xpath[1:])
+ if matched_substanzas:
+ break
+
+ # Check attribute values.
+ for attribute in attributes:
+ name, value = attribute.split('=')
+ if self[name] != value:
+ return False
+
+ # Check sub interfaces.
+ if len(xpath) > 1:
+ next_tag = xpath[1]
+ if next_tag in self.sub_interfaces and self[next_tag]:
+ return True
+
+ # Attempt to continue matching the XPath using the stanza's plugins.
+ if not matched_substanzas and len(xpath) > 1:
+ # Convert {namespace}tag@attribs to just tag
+ next_tag = xpath[1].split('@')[0].split('}')[-1]
+ langs = [name[1] for name in self.plugins if name[0] == next_tag]
+ for lang in langs:
+ plugin = self._get_plugin(next_tag, lang)
+ if plugin and plugin.match(xpath[1:]):
+ return True
+ return False
+
+ # Everything matched.
+ return True
+
+ def find(self, xpath):
+ """Find an XML object in this stanza given an XPath expression.
+
+ Exposes ElementTree interface for backwards compatibility.
+
+ .. note::
+
+ Matching on attribute values is not supported in Python 2.6
+ or Python 3.1
+
+ :param string xpath: An XPath expression matching a single
+ desired element.
+ """
+ return self.xml.find(xpath)
+
+ def findall(self, xpath):
+ """Find multiple XML objects in this stanza given an XPath expression.
+
+ Exposes ElementTree interface for backwards compatibility.
+
+ .. note::
+
+ Matching on attribute values is not supported in Python 2.6
+ or Python 3.1.
+
+ :param string xpath: An XPath expression matching multiple
+ desired elements.
+ """
+ return self.xml.findall(xpath)
+
+ def get(self, key, default=None):
+ """Return the value of a stanza interface.
+
+ If the found value is None or an empty string, return the supplied
+ default value.
+
+ Allows stanza objects to be used like dictionaries.
+
+ :param string key: The name of the stanza interface to check.
+ :param default: Value to return if the stanza interface has a value
+ of ``None`` or ``""``. Will default to returning None.
+ """
+ value = self[key]
+ if value is None or value == '':
+ return default
+ return value
+
+ def keys(self):
+ """Return the names of all stanza interfaces provided by the
+ stanza object.
+
+ Allows stanza objects to be used like dictionaries.
+ """
+ out = []
+ out += [x for x in self.interfaces]
+ out += [x for x in self.loaded_plugins]
+ out.append('lang')
+ if self.iterables:
+ out.append('substanzas')
+ return out
+
+ def append(self, item):
+ """Append either an XML object or a substanza to this stanza object.
+
+ If a substanza object is appended, it will be added to the list
+ of iterable stanzas.
+
+ Allows stanza objects to be used like lists.
+
+ :param item: Either an XML object or a stanza object to add to
+ this stanza's contents.
+ """
+ if not isinstance(item, ElementBase):
+ if type(item) == XML_TYPE:
+ return self.appendxml(item)
+ else:
+ raise TypeError
+ self.xml.append(item.xml)
+ self.iterables.append(item)
+ if item.__class__ in self.plugin_iterables:
+ if item.__class__.plugin_multi_attrib:
+ self.init_plugin(item.__class__.plugin_multi_attrib)
+ elif item.__class__ == self.plugin_tag_map.get(item.tag_name(), None):
+ self.init_plugin(item.plugin_attrib,
+ existing_xml=item.xml,
+ reuse=False)
+ return self
+
+ def appendxml(self, xml):
+ """Append an XML object to the stanza's XML.
+
+ The added XML will not be included in the list of
+ iterable substanzas.
+
+ :param XML xml: The XML object to add to the stanza.
+ """
+ self.xml.append(xml)
+ return self
+
+ def pop(self, index=0):
+ """Remove and return the last substanza in the list of
+ iterable substanzas.
+
+ Allows stanza objects to be used like lists.
+
+ :param int index: The index of the substanza to remove.
+ """
+ substanza = self.iterables.pop(index)
+ self.xml.remove(substanza.xml)
+ return substanza
+
+ def next(self):
+ """Return the next iterable substanza."""
+ return self.__next__()
+
+ def clear(self):
+ """Remove all XML element contents and plugins.
+
+ Any attribute values will be preserved.
+ """
+ for child in list(self.xml):
+ self.xml.remove(child)
+
+ for plugin in list(self.plugins.keys()):
+ del self.plugins[plugin]
+ return self
+
+ @classmethod
+ def tag_name(cls):
+ """Return the namespaced name of the stanza's root element.
+
+ The format for the tag name is::
+
+ '{namespace}elementname'
+
+ For example, for the stanza ``<foo xmlns="bar" />``,
+ ``stanza.tag_name()`` would return ``"{bar}foo"``.
+ """
+ return "{%s}%s" % (cls.namespace, cls.name)
+
+ def get_lang(self, lang=None):
+ result = self.xml.attrib.get('{%s}lang' % XML_NS, '')
+ if not result and self.parent and self.parent():
+ return self.parent()['lang']
+ return result
+
+ def set_lang(self, lang):
+ self.del_lang()
+ attr = '{%s}lang' % XML_NS
+ if lang:
+ self.xml.attrib[attr] = lang
+
+ def del_lang(self):
+ attr = '{%s}lang' % XML_NS
+ if attr in self.xml.attrib:
+ del self.xml.attrib[attr]
+
+ @property
+ def attrib(self):
+ """Return the stanza object itself.
+
+ Older implementations of stanza objects used XML objects directly,
+ requiring the use of ``.attrib`` to access attribute values.
+
+ Use of the dictionary syntax with the stanza object itself for
+ accessing stanza interfaces is preferred.
+
+ .. deprecated:: 1.0
+ """
+ return self
+
+ def _fix_ns(self, xpath, split=False, propagate_ns=True):
+ return fix_ns(xpath, split=split,
+ propagate_ns=propagate_ns,
+ default_ns=self.namespace)
+
+ def __eq__(self, other):
+ """Compare the stanza object with another to test for equality.
+
+ Stanzas are equal if their interfaces return the same values,
+ and if they are both instances of ElementBase.
+
+ :param ElementBase other: The stanza object to compare against.
+ """
+ if not isinstance(other, ElementBase):
+ return False
+
+ # Check that this stanza is a superset of the other stanza.
+ values = self.values
+ for key in other.keys():
+ if key not in values or values[key] != other[key]:
+ return False
+
+ # Check that the other stanza is a superset of this stanza.
+ values = other.values
+ for key in self.keys():
+ if key not in values or values[key] != self[key]:
+ return False
+
+ # Both stanzas are supersets of each other, therefore they
+ # must be equal.
+ return True
+
+ def __ne__(self, other):
+ """Compare the stanza object with another to test for inequality.
+
+ Stanzas are not equal if their interfaces return different values,
+ or if they are not both instances of ElementBase.
+
+ :param ElementBase other: The stanza object to compare against.
+ """
+ return not self.__eq__(other)
+
+ def __bool__(self):
+ """Stanza objects should be treated as True in boolean contexts.
+
+ Python 3.x version.
+ """
+ return True
+
+ def __nonzero__(self):
+ """Stanza objects should be treated as True in boolean contexts.
+
+ Python 2.x version.
+ """
+ return True
+
+ def __len__(self):
+ """Return the number of iterable substanzas in this stanza."""
+ return len(self.iterables)
+
+ def __iter__(self):
+ """Return an iterator object for the stanza's substanzas.
+
+ The iterator is the stanza object itself. Attempting to use two
+ iterators on the same stanza at the same time is discouraged.
+ """
+ self._index = 0
+ return self
+
+ def __next__(self):
+ """Return the next iterable substanza."""
+ self._index += 1
+ if self._index > len(self.iterables):
+ self._index = 0
+ raise StopIteration
+ return self.iterables[self._index - 1]
+
+ def __copy__(self):
+ """Return a copy of the stanza object that does not share the same
+ underlying XML object.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent)
+
+ def __str__(self, top_level_ns=True):
+ """Return a string serialization of the underlying XML object.
+
+ .. seealso:: :ref:`tostring`
+
+ :param bool top_level_ns: Display the top-most namespace.
+ Defaults to True.
+ """
+ return tostring(self.xml, xmlns='',
+ top_level=True)
+
+ def __repr__(self):
+ """Use the stanza's serialized XML as its representation."""
+ return self.__str__()
+
+
+class StanzaBase(ElementBase):
+
+ """
+ StanzaBase provides the foundation for all other stanza objects used
+ by Slixmpp, and defines a basic set of interfaces common to nearly
+ all stanzas. These interfaces are the ``'id'``, ``'type'``, ``'to'``,
+ and ``'from'`` attributes. An additional interface, ``'payload'``, is
+ available to access the XML contents of the stanza. Most stanza objects
+ will provided more specific interfaces, however.
+
+ **Stanza Interfaces:**
+
+ :id: An optional id value that can be used to associate stanzas
+ :to: A JID object representing the recipient's JID.
+ :from: A JID object representing the sender's JID.
+ with their replies.
+ :type: The type of stanza, typically will be ``'normal'``,
+ ``'error'``, ``'get'``, or ``'set'``, etc.
+ :payload: The XML contents of the stanza.
+
+ :param XMLStream stream: Optional :class:`slixmpp.xmlstream.XMLStream`
+ object responsible for sending this stanza.
+ :param XML xml: Optional XML contents to initialize stanza values.
+ :param string stype: Optional stanza type value.
+ :param sto: Optional string or :class:`slixmpp.xmlstream.JID`
+ object of the recipient's JID.
+ :param sfrom: Optional string or :class:`slixmpp.xmlstream.JID`
+ object of the sender's JID.
+ :param string sid: Optional ID value for the stanza.
+ :param parent: Optionally specify a parent stanza object will
+ contain this substanza.
+ """
+
+ #: The default XMPP client namespace
+ namespace = 'jabber:client'
+
+ #: There is a small set of attributes which apply to all XMPP stanzas:
+ #: the stanza type, the to and from JIDs, the stanza ID, and, especially
+ #: in the case of an Iq stanza, a payload.
+ interfaces = set(('type', 'to', 'from', 'id', 'payload'))
+
+ #: A basic set of allowed values for the ``'type'`` interface.
+ types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat'))
+
+ def __init__(self, stream=None, xml=None, stype=None,
+ sto=None, sfrom=None, sid=None, parent=None):
+ self.stream = stream
+ if stream is not None:
+ self.namespace = stream.default_ns
+ ElementBase.__init__(self, xml, parent)
+ if stype is not None:
+ self['type'] = stype
+ if sto is not None:
+ self['to'] = sto
+ if sfrom is not None:
+ self['from'] = sfrom
+ if sid is not None:
+ self['id'] = sid
+ self.tag = "{%s}%s" % (self.namespace, self.name)
+
+ def set_type(self, value):
+ """Set the stanza's ``'type'`` attribute.
+
+ Only type values contained in :attr:`types` are accepted.
+
+ :param str value: One of the values contained in :attr:`types`
+ """
+ if value in self.types:
+ self.xml.attrib['type'] = value
+ return self
+
+ def get_to(self):
+ """Return the value of the stanza's ``'to'`` attribute."""
+ return JID(self._get_attr('to'))
+
+ def set_to(self, value):
+ """Set the ``'to'`` attribute of the stanza.
+
+ :param value: A string or :class:`slixmpp.xmlstream.JID` object
+ representing the recipient's JID.
+ """
+ return self._set_attr('to', str(value))
+
+ def get_from(self):
+ """Return the value of the stanza's ``'from'`` attribute."""
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value):
+ """Set the 'from' attribute of the stanza.
+
+ :param from: A string or JID object representing the sender's JID.
+ :type from: str or :class:`.JID`
+ """
+ return self._set_attr('from', str(value))
+
+ def get_payload(self):
+ """Return a list of XML objects contained in the stanza."""
+ return list(self.xml)
+
+ def set_payload(self, value):
+ """Add XML content to the stanza.
+
+ :param value: Either an XML or a stanza object, or a list
+ of XML or stanza objects.
+ """
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ self.append(val)
+ return self
+
+ def del_payload(self):
+ """Remove the XML contents of the stanza."""
+ self.clear()
+ return self
+
+ def reply(self, clear=True):
+ """Prepare the stanza for sending a reply.
+
+ Swaps the ``'from'`` and ``'to'`` attributes.
+
+ If ``clear=True``, then also remove the stanza's
+ contents to make room for the reply content.
+
+ For client streams, the ``'from'`` attribute is removed.
+
+ :param bool clear: Indicates if the stanza's contents should be
+ removed. Defaults to ``True``.
+ """
+ new_stanza = copy.copy(self)
+ # if it's a component, use from
+ if self.stream and hasattr(self.stream, "is_component") and \
+ self.stream.is_component:
+ new_stanza['from'], new_stanza['to'] = self['to'], self['from']
+ else:
+ new_stanza['to'] = self['from']
+ del new_stanza['from']
+ if clear:
+ new_stanza.clear()
+ return new_stanza
+
+ def error(self):
+ """Set the stanza's type to ``'error'``."""
+ self['type'] = 'error'
+ return self
+
+ def unhandled(self):
+ """Called if no handlers have been registered to process this stanza.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def exception(self, e):
+ """Handle exceptions raised during stanza processing.
+
+ Meant to be overridden.
+ """
+ log.exception('Error handling {%s}%s stanza', self.namespace,
+ self.name)
+
+ def send(self):
+ """Queue the stanza to be sent on the XML stream.
+
+ :param bool now: Indicates if the queue should be skipped and the
+ stanza sent immediately. Useful for stream
+ initialization. Defaults to ``False``.
+ """
+ self.stream.send(self)
+
+ def __copy__(self):
+ """Return a copy of the stanza object that does not share the
+ same underlying XML object, but does share the same XML stream.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml),
+ stream=self.stream)
+
+ def __str__(self, top_level_ns=False):
+ """Serialize the stanza's XML to a string.
+
+ :param bool top_level_ns: Display the top-most namespace.
+ Defaults to ``False``.
+ """
+ xmlns = self.stream.default_ns if self.stream else ''
+ return tostring(self.xml, xmlns=xmlns,
+ stream=self.stream,
+ top_level=(self.stream is None))
+
+
+#: A JSON/dictionary version of the XML content exposed through
+#: the stanza interfaces::
+#:
+#: >>> msg = Message()
+#: >>> msg.values
+#: {'body': '', 'from': , 'mucnick': '', 'mucroom': '',
+#: 'to': , 'type': 'normal', 'id': '', 'subject': ''}
+#:
+#: Likewise, assigning to the :attr:`values` will change the XML
+#: content::
+#:
+#: >>> msg = Message()
+#: >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'}
+#: >>> msg
+#: '<message to="user@example.com"><body>Hi!</body></message>'
+#:
+#: Child stanzas are exposed as nested dictionaries.
+ElementBase.values = property(ElementBase._get_stanza_values,
+ ElementBase._set_stanza_values)
+
+ElementBase.get_stanza_values = ElementBase._get_stanza_values
+ElementBase.set_stanza_values = ElementBase._set_stanza_values
diff --git a/slixmpp/xmlstream/tostring.py b/slixmpp/xmlstream/tostring.py
new file mode 100644
index 00000000..6726bf1e
--- /dev/null
+++ b/slixmpp/xmlstream/tostring.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+"""
+ slixmpp.xmlstream.tostring
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module converts XML objects into Unicode strings and
+ intelligently includes namespaces only when necessary to
+ keep the output readable.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+XML_NS = 'http://www.w3.org/XML/1998/namespace'
+
+
+def tostring(xml=None, xmlns='', stream=None, outbuffer='',
+ top_level=False, open_only=False, namespaces=None):
+ """Serialize an XML object to a Unicode string.
+
+ If an outer xmlns is provided using ``xmlns``, then the current element's
+ namespace will not be included if it matches the outer namespace. An
+ exception is made for elements that have an attached stream, and appear
+ at the stream root.
+
+ :param XML xml: The XML object to serialize.
+ :param string xmlns: Optional namespace of an element wrapping the XML
+ object.
+ :param stream: The XML stream that generated the XML object.
+ :param string outbuffer: Optional buffer for storing serializations
+ during recursive calls.
+ :param bool top_level: Indicates that the element is the outermost
+ element.
+ :param set namespaces: Track which namespaces are in active use so
+ that new ones can be declared when needed.
+
+ :type xml: :py:class:`~xml.etree.ElementTree.Element`
+ :type stream: :class:`~slixmpp.xmlstream.xmlstream.XMLStream`
+
+ :rtype: Unicode string
+ """
+ # Add previous results to the start of the output.
+ output = [outbuffer]
+
+ # Extract the element's tag name.
+ tag_name = xml.tag.split('}', 1)[-1]
+
+ # Extract the element's namespace if it is defined.
+ if '}' in xml.tag:
+ tag_xmlns = xml.tag.split('}', 1)[0][1:]
+ else:
+ tag_xmlns = ''
+
+ default_ns = ''
+ stream_ns = ''
+ use_cdata = False
+
+ if stream:
+ default_ns = stream.default_ns
+ stream_ns = stream.stream_ns
+ use_cdata = stream.use_cdata
+
+ # Output the tag name and derived namespace of the element.
+ namespace = ''
+ if tag_xmlns:
+ if top_level and tag_xmlns not in [default_ns, xmlns, stream_ns] \
+ or not top_level and tag_xmlns != xmlns:
+ namespace = ' xmlns="%s"' % tag_xmlns
+ if stream and tag_xmlns in stream.namespace_map:
+ mapped_namespace = stream.namespace_map[tag_xmlns]
+ if mapped_namespace:
+ tag_name = "%s:%s" % (mapped_namespace, tag_name)
+ output.append("<%s" % tag_name)
+ output.append(namespace)
+
+ # Output escaped attribute values.
+ new_namespaces = set()
+ for attrib, value in xml.attrib.items():
+ value = escape(value, use_cdata)
+ if '}' not in attrib:
+ output.append(' %s="%s"' % (attrib, value))
+ else:
+ attrib_ns = attrib.split('}')[0][1:]
+ attrib = attrib.split('}')[1]
+ if attrib_ns == XML_NS:
+ output.append(' xml:%s="%s"' % (attrib, value))
+ elif stream and attrib_ns in stream.namespace_map:
+ mapped_ns = stream.namespace_map[attrib_ns]
+ if mapped_ns:
+ if namespaces is None:
+ namespaces = set()
+ if attrib_ns not in namespaces:
+ namespaces.add(attrib_ns)
+ new_namespaces.add(attrib_ns)
+ output.append(' xmlns:%s="%s"' % (
+ mapped_ns, attrib_ns))
+ output.append(' %s:%s="%s"' % (
+ mapped_ns, attrib, value))
+
+ if open_only:
+ # Only output the opening tag, regardless of content.
+ output.append(">")
+ return ''.join(output)
+
+ if len(xml) or xml.text:
+ # If there are additional child elements to serialize.
+ output.append(">")
+ if xml.text:
+ output.append(escape(xml.text, use_cdata))
+ if len(xml):
+ for child in xml:
+ output.append(tostring(child, tag_xmlns, stream,
+ namespaces=namespaces))
+ output.append("</%s>" % tag_name)
+ elif xml.text:
+ # If we only have text content.
+ output.append(">%s</%s>" % (escape(xml.text, use_cdata), tag_name))
+ else:
+ # Empty element.
+ output.append(" />")
+ if xml.tail:
+ # If there is additional text after the element.
+ output.append(escape(xml.tail, use_cdata))
+ for ns in new_namespaces:
+ # Remove namespaces introduced in this context. This is necessary
+ # because the namespaces object continues to be shared with other
+ # contexts.
+ namespaces.remove(ns)
+ return ''.join(output)
+
+
+def escape(text, use_cdata=False):
+ """Convert special characters in XML to escape sequences.
+
+ :param string text: The XML text to convert.
+ :rtype: Unicode string
+ """
+ escapes = {'&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ "'": '&apos;',
+ '"': '&quot;'}
+
+ if not use_cdata:
+ text = list(text)
+ for i, c in enumerate(text):
+ text[i] = escapes.get(c, c)
+ return ''.join(text)
+ else:
+ escape_needed = False
+ for c in text:
+ if c in escapes:
+ escape_needed = True
+ break
+ if escape_needed:
+ escaped = map(lambda x : "<![CDATA[%s]]>" % x, text.split("]]>"))
+ return "<![CDATA[]]]><![CDATA[]>]]>".join(escaped)
+ return text
+
+
+def _get_highlight():
+ try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters import Terminal256Formatter
+
+ LEXER = get_lexer_by_name('xml')
+ FORMATTER = Terminal256Formatter()
+
+ class Highlighter:
+ __slots__ = ['string']
+ def __init__(self, string):
+ self.string = string
+ def __str__(self):
+ return highlight(str(self.string), LEXER, FORMATTER).strip()
+
+ return Highlighter
+ except ImportError:
+ return lambda x: x
+
+highlight = _get_highlight()
diff --git a/slixmpp/xmlstream/xmlstream.py b/slixmpp/xmlstream/xmlstream.py
new file mode 100644
index 00000000..28bfb17c
--- /dev/null
+++ b/slixmpp/xmlstream/xmlstream.py
@@ -0,0 +1,943 @@
+"""
+ slixmpp.xmlstream.xmlstream
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides the module for creating and
+ interacting with generic XML streams, along with
+ the necessary eventing infrastructure.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import functools
+import logging
+import socket as Socket
+import ssl
+import weakref
+import uuid
+
+import xml.etree.ElementTree
+
+from slixmpp.xmlstream.asyncio import asyncio
+from slixmpp.xmlstream import tostring, highlight
+from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
+from slixmpp.xmlstream.resolver import resolve, default_resolver
+
+#: The time in seconds to wait before timing out waiting for response stanzas.
+RESPONSE_TIMEOUT = 30
+
+log = logging.getLogger(__name__)
+
+class NotConnectedError(Exception):
+ """
+ Raised when we try to send something over the wire but we are not
+ connected.
+ """
+
+class XMLStream(asyncio.BaseProtocol):
+ """
+ An XML stream connection manager and event dispatcher.
+
+ The XMLStream class abstracts away the issues of establishing a
+ connection with a server and sending and receiving XML "stanzas".
+ A stanza is a complete XML element that is a direct child of a root
+ document element. Two streams are used, one for each communication
+ direction, over the same socket. Once the connection is closed, both
+ streams should be complete and valid XML documents.
+
+ Three types of events are provided to manage the stream:
+ :Stream: Triggered based on received stanzas, similar in concept
+ to events in a SAX XML parser.
+ :Custom: Triggered manually.
+ :Scheduled: Triggered based on time delays.
+
+ Typically, stanzas are first processed by a stream event handler which
+ will then trigger custom events to continue further processing,
+ especially since custom event handlers may run in individual threads.
+
+ :param socket: Use an existing socket for the stream. Defaults to
+ ``None`` to generate a new socket.
+ :param string host: The name of the target server.
+ :param int port: The port to use for the connection. Defaults to 0.
+ """
+
+ def __init__(self, socket=None, host='', port=0):
+ # The asyncio.Transport object provided by the connection_made()
+ # callback when we are connected
+ self.transport = None
+
+ # The socket the is used internally by the transport object
+ self.socket = None
+
+ self.connect_loop_wait = 0
+
+ self.parser = None
+ self.xml_depth = 0
+ self.xml_root = None
+
+ self.force_starttls = None
+ self.disable_starttls = None
+
+ # A dict of {name: handle}
+ self.scheduled_events = {}
+
+ self.ssl_context = ssl.create_default_context()
+ self.ssl_context.check_hostname = False
+ self.ssl_context.verify_mode = ssl.CERT_NONE
+
+ # The event to trigger when the create_connection() succeeds. It can
+ # be "connected" or "tls_success" depending on the step we are at.
+ self.event_when_connected = "connected"
+
+ #: The list of accepted ciphers, in OpenSSL Format.
+ #: It might be useful to override it for improved security
+ #: over the python defaults.
+ self.ciphers = None
+
+ #: Path to a file containing certificates for verifying the
+ #: server SSL certificate. A non-``None`` value will trigger
+ #: certificate checking.
+ #:
+ #: .. note::
+ #:
+ #: On Mac OS X, certificates in the system keyring will
+ #: be consulted, even if they are not in the provided file.
+ self.ca_certs = None
+
+ #: Path to a file containing a client certificate to use for
+ #: authenticating via SASL EXTERNAL. If set, there must also
+ #: be a corresponding `:attr:keyfile` value.
+ self.certfile = None
+
+ #: Path to a file containing the private key for the selected
+ #: client certificate to use for authenticating via SASL EXTERNAL.
+ self.keyfile = None
+
+ self._der_cert = None
+
+ # The asyncio event loop
+ self._loop = None
+
+ #: The default port to return when querying DNS records.
+ self.default_port = int(port)
+
+ #: The domain to try when querying DNS records.
+ self.default_domain = ''
+
+ #: The expected name of the server, for validation.
+ self._expected_server_name = ''
+ self._service_name = ''
+
+ #: The desired, or actual, address of the connected server.
+ self.address = (host, int(port))
+
+ #: Enable connecting to the server directly over SSL, in
+ #: particular when the service provides two ports: one for
+ #: non-SSL traffic and another for SSL traffic.
+ self.use_ssl = False
+
+ #: If set to ``True``, attempt to connect through an HTTP
+ #: proxy based on the settings in :attr:`proxy_config`.
+ self.use_proxy = False
+
+ #: If set to ``True``, attempt to use IPv6.
+ self.use_ipv6 = True
+
+ #: If set to ``True``, allow using the ``dnspython`` DNS library
+ #: if available. If set to ``False``, the builtin DNS resolver
+ #: will be used, even if ``dnspython`` is installed.
+ self.use_aiodns = True
+
+ #: Use CDATA for escaping instead of XML entities. Defaults
+ #: to ``False``.
+ self.use_cdata = False
+
+ #: An optional dictionary of proxy settings. It may provide:
+ #: :host: The host offering proxy services.
+ #: :port: The port for the proxy service.
+ #: :username: Optional username for accessing the proxy.
+ #: :password: Optional password for accessing the proxy.
+ self.proxy_config = {}
+
+ #: The default namespace of the stream content, not of the
+ #: stream wrapper itself.
+ self.default_ns = ''
+
+ self.default_lang = None
+ self.peer_default_lang = None
+
+ #: The namespace of the enveloping stream element.
+ self.stream_ns = ''
+
+ #: The default opening tag for the stream element.
+ self.stream_header = "<stream>"
+
+ #: The default closing tag for the stream element.
+ self.stream_footer = "</stream>"
+
+ #: If ``True``, periodically send a whitespace character over the
+ #: wire to keep the connection alive. Mainly useful for connections
+ #: traversing NAT.
+ self.whitespace_keepalive = True
+
+ #: The default interval between keepalive signals when
+ #: :attr:`whitespace_keepalive` is enabled.
+ self.whitespace_keepalive_interval = 300
+
+ #: Flag for controlling if the session can be considered ended
+ #: if the connection is terminated.
+ self.end_session_on_disconnect = True
+
+ #: A mapping of XML namespaces to well-known prefixes.
+ self.namespace_map = {StanzaBase.xml_ns: 'xml'}
+
+ self.__root_stanza = []
+ self.__handlers = []
+ self.__event_handlers = {}
+ self.__filters = {'in': [], 'out': [], 'out_sync': []}
+
+ self._id = 0
+
+ #: We use an ID prefix to ensure that all ID values are unique.
+ self._id_prefix = '%s-' % uuid.uuid4()
+
+ #: A list of DNS results that have not yet been tried.
+ self.dns_answers = None
+
+ #: The service name to check with DNS SRV records. For
+ #: example, setting this to ``'xmpp-client'`` would query the
+ #: ``_xmpp-client._tcp`` service.
+ self.dns_service = None
+
+ #: An asyncio Future being done when the stream is disconnected.
+ self.disconnected = asyncio.Future()
+
+ self.add_event_handler('disconnected', self._remove_schedules)
+ self.add_event_handler('session_start', self._start_keepalive)
+
+ @property
+ def loop(self):
+ if self._loop is None:
+ self._loop = asyncio.get_event_loop()
+ return self._loop
+
+ @loop.setter
+ def loop(self, value):
+ self._loop = value
+
+ def new_id(self):
+ """Generate and return a new stream ID in hexadecimal form.
+
+ Many stanzas, handlers, or matchers may require unique
+ ID values. Using this method ensures that all new ID values
+ are unique in this stream.
+ """
+ self._id += 1
+ return self.get_id()
+
+ def get_id(self):
+ """Return the current unique stream ID in hexadecimal form."""
+ return "%s%X" % (self._id_prefix, self._id)
+
+ def connect(self, host='', port=0, use_ssl=False,
+ force_starttls=True, disable_starttls=False):
+ """Create a new socket and connect to the server.
+
+ :param host: The name of the desired server for the connection.
+ :param port: Port to connect to on the server.
+ :param use_ssl: Flag indicating if SSL should be used by connecting
+ directly to a port using SSL. If it is False, the
+ connection will be upgraded to SSL/TLS later, using
+ STARTTLS. Only use this value for old servers that
+ have specific port for SSL/TLS
+ TODO fix the comment
+ :param force_starttls: If True, the connection will be aborted if
+ the server does not initiate a STARTTLS
+ negociation. If None, the connection will be
+ upgraded to TLS only if the server initiate
+ the STARTTLS negociation, otherwise it will
+ connect in clear. If False it will never
+ upgrade to TLS, even if the server provides
+ it. Use this for example if you’re on
+ localhost
+
+ """
+ if host and port:
+ self.address = (host, int(port))
+ try:
+ Socket.inet_aton(self.address[0])
+ except (Socket.error, ssl.SSLError):
+ self.default_domain = self.address[0]
+
+ # Respect previous TLS usage.
+ if use_ssl is not None:
+ self.use_ssl = use_ssl
+ if force_starttls is not None:
+ self.force_starttls = force_starttls
+ if disable_starttls is not None:
+ self.disable_starttls = disable_starttls
+
+ self.event("connecting")
+ asyncio.async(self._connect_routine())
+
+ @asyncio.coroutine
+ def _connect_routine(self):
+ self.event_when_connected = "connected"
+
+ record = yield from self.pick_dns_answer(self.default_domain)
+ if record is not None:
+ host, address, port = record
+ self.address = (address, port)
+ self._service_name = host
+ else:
+ # No DNS records left, stop iterating
+ # and try (host, port) as a last resort
+ self.dns_answers = None
+
+ yield from asyncio.sleep(self.connect_loop_wait)
+ try:
+ yield from self.loop.create_connection(lambda: self,
+ self.address[0],
+ self.address[1],
+ ssl=self.use_ssl)
+ except Socket.gaierror as e:
+ self.event('connection_failed',
+ 'No DNS record available for %s' % self.default_domain)
+ except OSError as e:
+ log.debug('Connection failed: %s', e)
+ self.event("connection_failed", e)
+ self.connect_loop_wait = self.connect_loop_wait * 2 + 1
+ asyncio.async(self._connect_routine())
+ else:
+ self.connect_loop_wait = 0
+
+ def process(self, *, forever=True, timeout=None):
+ """Process all the available XMPP events (receiving or sending data on the
+ socket(s), calling various registered callbacks, calling expired
+ timers, handling signal events, etc). If timeout is None, this
+ function will run forever. If timeout is a number, this function
+ will return after the given time in seconds.
+ """
+ if timeout is None:
+ if forever:
+ self.loop.run_forever()
+ else:
+ self.loop.run_until_complete(self.disconnected)
+ else:
+ tasks = [asyncio.sleep(timeout)]
+ if not forever:
+ tasks.append(self.disconnected)
+ self.loop.run_until_complete(asyncio.wait(tasks))
+
+ def init_parser(self):
+ """init the XML parser. The parser must always be reset for each new
+ connexion
+ """
+ self.xml_depth = 0
+ self.xml_root = None
+ self.parser = xml.etree.ElementTree.XMLPullParser(("start", "end"))
+
+ def connection_made(self, transport):
+ """Called when the TCP connection has been established with the server
+ """
+ self.event(self.event_when_connected)
+ self.transport = transport
+ self.socket = self.transport.get_extra_info("socket")
+ self.init_parser()
+ self.send_raw(self.stream_header)
+
+ def data_received(self, data):
+ """Called when incoming data is received on the socket.
+
+ We feed that data to the parser and the see if this produced any XML
+ event. This could trigger one or more event (a stanza is received,
+ the stream is opened, etc).
+ """
+ self.parser.feed(data)
+ for event, xml in self.parser.read_events():
+ if event == 'start':
+ if self.xml_depth == 0:
+ # We have received the start of the root element.
+ self.xml_root = xml
+ log.debug('RECV: %s', highlight(tostring(self.xml_root, xmlns=self.default_ns,
+ stream=self,
+ top_level=True,
+ open_only=True)))
+ self.start_stream_handler(self.xml_root)
+ self.xml_depth += 1
+ if event == 'end':
+ self.xml_depth -= 1
+ if self.xml_depth == 0:
+ # The stream's root element has closed,
+ # terminating the stream.
+ log.debug("End of stream received")
+ self.abort()
+ elif self.xml_depth == 1:
+ # A stanza is an XML element that is a direct child of
+ # the root element, hence the check of depth == 1
+ self.loop.idle_call(functools.partial(self.__spawn_event, xml))
+ if self.xml_root is not None:
+ # Keep the root element empty of children to
+ # save on memory use.
+ self.xml_root.clear()
+
+ def is_connected(self):
+ return self.transport is not None
+
+ def eof_received(self):
+ """When the TCP connection is properly closed by the remote end
+ """
+ self.event("eof_received")
+
+ def connection_lost(self, exception):
+ """On any kind of disconnection, initiated by us or not. This signals the
+ closure of the TCP connection
+ """
+ log.info("connection_lost: %s", (exception,))
+ self.event("disconnected")
+ if self.end_session_on_disconnect:
+ self.event('session_end')
+ # All these objects are associated with one TCP connection. Since
+ # we are not connected anymore, destroy them
+ self.parser = None
+ self.transport = None
+ self.socket = None
+
+ def disconnect(self, wait=2.0):
+ """Close the XML stream and wait for an acknowldgement from the server for
+ at most `wait` seconds. After the given number of seconds has
+ passed without a response from the serveur, or when the server
+ successfuly responds with a closure of its own stream, abort() is
+ called. If wait is 0.0, this is almost equivalent to calling abort()
+ directly.
+
+ Does nothing if we are not connected.
+
+ :param wait: Time to wait for a response from the server.
+
+ """
+ if self.transport:
+ self.send_raw(self.stream_footer)
+ self.schedule('Disconnect wait', wait,
+ self.abort, repeat=False)
+
+ def abort(self):
+ """
+ Forcibly close the connection
+ """
+ if self.transport:
+ self.transport.close()
+ self.transport.abort()
+ self.event("killed")
+ self.disconnected.set_result(True)
+ self.disconnected = asyncio.Future()
+
+ def reconnect(self, wait=2.0):
+ """Calls disconnect(), and once we are disconnected (after the timeout, or
+ when the server acknowledgement is received), call connect()
+ """
+ log.debug("reconnecting...")
+ self.disconnect(wait)
+ self.add_event_handler('disconnected', self.connect, disposable=True)
+
+ def configure_socket(self):
+ """Set timeout and other options for self.socket.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def configure_dns(self, resolver, domain=None, port=None):
+ """
+ Configure and set options for a :class:`~dns.resolver.Resolver`
+ instance, and other DNS related tasks. For example, you
+ can also check :meth:`~socket.socket.getaddrinfo` to see
+ if you need to call out to ``libresolv.so.2`` to
+ run ``res_init()``.
+
+ Meant to be overridden.
+
+ :param resolver: A :class:`~dns.resolver.Resolver` instance
+ or ``None`` if ``dnspython`` is not installed.
+ :param domain: The initial domain under consideration.
+ :param port: The initial port under consideration.
+ """
+ pass
+
+ def start_tls(self):
+ """Perform handshakes for TLS.
+
+ If the handshake is successful, the XML stream will need
+ to be restarted.
+ """
+ self.event_when_connected = "tls_success"
+
+ if self.ciphers is not None:
+ self.ssl_context.set_ciphers(self.ciphers)
+ if self.keyfile and self.certfile:
+ try:
+ self.ssl_context.load_cert_chain(self.certfile, self.keyfile)
+ except (ssl.SSLError, OSError):
+ log.debug('Error loading the cert chain:', exc_info=True)
+ else:
+ log.debug('Loaded cert file %s and key file %s',
+ self.certfile, self.keyfile)
+ if self.ca_certs is not None:
+ self.ssl_context.verify_mode = ssl.CERT_REQUIRED
+ self.ssl_context.load_verify_locations(cafile=self.ca_certs)
+
+ ssl_connect_routine = self.loop.create_connection(lambda: self, ssl=self.ssl_context,
+ sock=self.socket,
+ server_hostname=self.default_domain)
+ @asyncio.coroutine
+ def ssl_coro():
+ try:
+ transp, prot = yield from ssl_connect_routine
+ except ssl.SSLError as e:
+ log.debug('SSL: Unable to connect', exc_info=True)
+ log.error('CERT: Invalid certificate trust chain.')
+ if not self.event_handled('ssl_invalid_chain'):
+ self.disconnect()
+ else:
+ self.event('ssl_invalid_chain', e)
+ else:
+ der_cert = transp.get_extra_info("socket").getpeercert(True)
+ pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
+ self.event('ssl_cert', pem_cert)
+
+ asyncio.async(ssl_coro())
+
+ def _start_keepalive(self, event):
+ """Begin sending whitespace periodically to keep the connection alive.
+
+ May be disabled by setting::
+
+ self.whitespace_keepalive = False
+
+ The keepalive interval can be set using::
+
+ self.whitespace_keepalive_interval = 300
+ """
+ self.schedule('Whitespace Keepalive',
+ self.whitespace_keepalive_interval,
+ self.send_raw,
+ args=(' ',),
+ repeat=True)
+
+ def _remove_schedules(self, event):
+ """Remove some schedules that become pointless when disconnected"""
+ self.cancel_schedule('Whitespace Keepalive')
+ self.cancel_schedule('Disconnect wait')
+
+ def start_stream_handler(self, xml):
+ """Perform any initialization actions, such as handshakes,
+ once the stream header has been sent.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def register_stanza(self, stanza_class):
+ """Add a stanza object class as a known root stanza.
+
+ A root stanza is one that appears as a direct child of the stream's
+ root element.
+
+ Stanzas that appear as substanzas of a root stanza do not need to
+ be registered here. That is done using register_stanza_plugin() from
+ slixmpp.xmlstream.stanzabase.
+
+ Stanzas that are not registered will not be converted into
+ stanza objects, but may still be processed using handlers and
+ matchers.
+
+ :param stanza_class: The top-level stanza object's class.
+ """
+ self.__root_stanza.append(stanza_class)
+
+ def remove_stanza(self, stanza_class):
+ """Remove a stanza from being a known root stanza.
+
+ A root stanza is one that appears as a direct child of the stream's
+ root element.
+
+ Stanzas that are not registered will not be converted into
+ stanza objects, but may still be processed using handlers and
+ matchers.
+ """
+ self.__root_stanza.remove(stanza_class)
+
+ def add_filter(self, mode, handler, order=None):
+ """Add a filter for incoming or outgoing stanzas.
+
+ These filters are applied before incoming stanzas are
+ passed to any handlers, and before outgoing stanzas
+ are put in the send queue.
+
+ Each filter must accept a single stanza, and return
+ either a stanza or ``None``. If the filter returns
+ ``None``, then the stanza will be dropped from being
+ processed for events or from being sent.
+
+ :param mode: One of ``'in'`` or ``'out'``.
+ :param handler: The filter function.
+ :param int order: The position to insert the filter in
+ the list of active filters.
+ """
+ if order:
+ self.__filters[mode].insert(order, handler)
+ else:
+ self.__filters[mode].append(handler)
+
+ def del_filter(self, mode, handler):
+ """Remove an incoming or outgoing filter."""
+ self.__filters[mode].remove(handler)
+
+ def register_handler(self, handler, before=None, after=None):
+ """Add a stream event handler that will be executed when a matching
+ stanza is received.
+
+ :param handler:
+ The :class:`~slixmpp.xmlstream.handler.base.BaseHandler`
+ derived object to execute.
+ """
+ if handler.stream is None:
+ self.__handlers.append(handler)
+ handler.stream = weakref.ref(self)
+
+ def remove_handler(self, name):
+ """Remove any stream event handlers with the given name.
+
+ :param name: The name of the handler.
+ """
+ idx = 0
+ for handler in self.__handlers:
+ if handler.name == name:
+ self.__handlers.pop(idx)
+ return True
+ idx += 1
+ return False
+
+ @asyncio.coroutine
+ def get_dns_records(self, domain, port=None):
+ """Get the DNS records for a domain.
+
+ :param domain: The domain in question.
+ :param port: If the results don't include a port, use this one.
+ """
+ if port is None:
+ port = self.default_port
+
+ resolver = default_resolver(loop=self.loop)
+ self.configure_dns(resolver, domain=domain, port=port)
+
+ result = yield from resolve(domain, port,
+ service=self.dns_service,
+ resolver=resolver,
+ use_ipv6=self.use_ipv6,
+ use_aiodns=self.use_aiodns,
+ loop=self.loop)
+ return result
+
+ @asyncio.coroutine
+ def pick_dns_answer(self, domain, port=None):
+ """Pick a server and port from DNS answers.
+
+ Gets DNS answers if none available.
+ Removes used answer from available answers.
+
+ :param domain: The domain in question.
+ :param port: If the results don't include a port, use this one.
+ """
+ if self.dns_answers is None:
+ dns_records = yield from self.get_dns_records(domain, port)
+ self.dns_answers = iter(dns_records)
+
+ try:
+ return next(self.dns_answers)
+ except StopIteration:
+ return
+
+ def add_event_handler(self, name, pointer, disposable=False):
+ """Add a custom event handler that will be executed whenever
+ its event is manually triggered.
+
+ :param name: The name of the event that will trigger
+ this handler.
+ :param pointer: The function to execute.
+ :param disposable: If set to ``True``, the handler will be
+ discarded after one use. Defaults to ``False``.
+ """
+ if not name in self.__event_handlers:
+ self.__event_handlers[name] = []
+ self.__event_handlers[name].append((pointer, disposable))
+
+ def del_event_handler(self, name, pointer):
+ """Remove a function as a handler for an event.
+
+ :param name: The name of the event.
+ :param pointer: The function to remove as a handler.
+ """
+ if not name in self.__event_handlers:
+ return
+
+ # Need to keep handlers that do not use
+ # the given function pointer
+ def filter_pointers(handler):
+ return handler[0] != pointer
+
+ self.__event_handlers[name] = list(filter(
+ filter_pointers,
+ self.__event_handlers[name]))
+
+ def event_handled(self, name):
+ """Returns the number of registered handlers for an event.
+
+ :param name: The name of the event to check.
+ """
+ return len(self.__event_handlers.get(name, []))
+
+ def event(self, name, data={}):
+ """Manually trigger a custom event.
+
+ :param name: The name of the event to trigger.
+ :param data: Data that will be passed to each event handler.
+ Defaults to an empty dictionary, but is usually
+ a stanza object.
+ """
+ log.debug("Event triggered: %s", name)
+
+ handlers = self.__event_handlers.get(name, [])
+ for handler in handlers:
+ handler_callback, disposable = handler
+ old_exception = getattr(data, 'exception', None)
+
+ # If the callback is a coroutine, schedule it instead of
+ # running it directly
+ if asyncio.iscoroutinefunction(handler_callback):
+ @asyncio.coroutine
+ def handler_callback_routine(cb):
+ try:
+ yield from cb(data)
+ except Exception as e:
+ if old_exception:
+ old_exception(e)
+ else:
+ self.exception(e)
+ asyncio.async(handler_callback_routine(handler_callback))
+ else:
+ try:
+ handler_callback(data)
+ except Exception as e:
+ if old_exception:
+ old_exception(e)
+ else:
+ self.exception(e)
+ if disposable:
+ # If the handler is disposable, we will go ahead and
+ # remove it now instead of waiting for it to be
+ # processed in the queue.
+ try:
+ self.__event_handlers[name].remove(handler)
+ except ValueError:
+ pass
+
+ def schedule(self, name, seconds, callback, args=tuple(),
+ kwargs={}, repeat=False):
+ """Schedule a callback function to execute after a given delay.
+
+ :param name: A unique name for the scheduled callback.
+ :param seconds: The time in seconds to wait before executing.
+ :param callback: A pointer to the function to execute.
+ :param args: A tuple of arguments to pass to the function.
+ :param kwargs: A dictionary of keyword arguments to pass to
+ the function.
+ :param repeat: Flag indicating if the scheduled event should
+ be reset and repeat after executing.
+ """
+ if seconds is None:
+ seconds = RESPONSE_TIMEOUT
+ cb = functools.partial(callback, *args, **kwargs)
+ if repeat:
+ handle = self.loop.call_later(seconds, self._execute_and_reschedule,
+ name, cb, seconds)
+ else:
+ handle = self.loop.call_later(seconds, self._execute_and_unschedule,
+ name, cb)
+
+ # Save that handle, so we can just cancel this scheduled event by
+ # canceling scheduled_events[name]
+ self.scheduled_events[name] = handle
+
+ def cancel_schedule(self, name):
+ try:
+ handle = self.scheduled_events.pop(name)
+ handle.cancel()
+ except KeyError:
+ log.debug("Tried to cancel unscheduled event: %s" % (name,))
+
+ def _safe_cb_run(self, name, cb):
+ log.debug('Scheduled event: %s', name)
+ try:
+ cb()
+ except Exception as e:
+ self.exception(e)
+
+ def _execute_and_reschedule(self, name, cb, seconds):
+ """Simple method that calls the given callback, and then schedule itself to
+ be called after the given number of seconds.
+ """
+ self._safe_cb_run(name, cb)
+ handle = self.loop.call_later(seconds, self._execute_and_reschedule,
+ name, cb, seconds)
+ self.scheduled_events[name] = handle
+
+ def _execute_and_unschedule(self, name, cb):
+ """
+ Execute the callback and remove the handler for it.
+ """
+ self._safe_cb_run(name, cb)
+ del self.scheduled_events[name]
+
+ def incoming_filter(self, xml):
+ """Filter incoming XML objects before they are processed.
+
+ Possible uses include remapping namespaces, or correcting elements
+ from sources with incorrect behavior.
+
+ Meant to be overridden.
+ """
+ return xml
+
+ def send(self, data, use_filters=True):
+ """A wrapper for :meth:`send_raw()` for sending stanza objects.
+
+ May optionally block until an expected response is received.
+
+ :param data: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza to send on the stream.
+ :param bool use_filters: Indicates if outgoing filters should be
+ applied to the given stanza data. Disabling
+ filters is useful when resending stanzas.
+ Defaults to ``True``.
+ """
+ if isinstance(data, ElementBase):
+ if use_filters:
+ for filter in self.__filters['out']:
+ data = filter(data)
+ if data is None:
+ return
+
+ if isinstance(data, ElementBase):
+ if use_filters:
+ for filter in self.__filters['out_sync']:
+ data = filter(data)
+ if data is None:
+ return
+ str_data = tostring(data.xml, xmlns=self.default_ns,
+ stream=self,
+ top_level=True)
+ self.send_raw(str_data)
+ else:
+ self.send_raw(data)
+
+ def send_xml(self, data):
+ """Send an XML object on the stream
+
+ :param data: The :class:`~xml.etree.ElementTree.Element` XML object
+ to send on the stream.
+ """
+ return self.send(tostring(data))
+
+ def send_raw(self, data):
+ """Send raw data across the stream.
+
+ :param string data: Any bytes or utf-8 string value.
+ """
+ log.debug("SEND: %s", highlight(data))
+ if not self.transport:
+ raise NotConnectedError()
+ if isinstance(data, str):
+ data = data.encode('utf-8')
+ self.transport.write(data)
+
+ def _build_stanza(self, xml, default_ns=None):
+ """Create a stanza object from a given XML object.
+
+ If a specialized stanza type is not found for the XML, then
+ a generic :class:`~slixmpp.xmlstream.stanzabase.StanzaBase`
+ stanza will be returned.
+
+ :param xml: The :class:`~xml.etree.ElementTree.Element` XML object
+ to convert into a stanza object.
+ :param default_ns: Optional default namespace to use instead of the
+ stream's current default namespace.
+ """
+ if default_ns is None:
+ default_ns = self.default_ns
+ stanza_type = StanzaBase
+ for stanza_class in self.__root_stanza:
+ if xml.tag == "{%s}%s" % (default_ns, stanza_class.name) or \
+ xml.tag == stanza_class.tag_name():
+ stanza_type = stanza_class
+ break
+ stanza = stanza_type(self, xml)
+ if stanza['lang'] is None and self.peer_default_lang:
+ stanza['lang'] = self.peer_default_lang
+ return stanza
+
+ def __spawn_event(self, xml):
+ """
+ Analyze incoming XML stanzas and convert them into stanza
+ objects if applicable and queue stream events to be processed
+ by matching handlers.
+
+ :param xml: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
+ stanza to analyze.
+ """
+ # Apply any preprocessing filters.
+ xml = self.incoming_filter(xml)
+
+ # Convert the raw XML object into a stanza object. If no registered
+ # stanza type applies, a generic StanzaBase stanza will be used.
+ stanza = self._build_stanza(xml)
+ for filter in self.__filters['in']:
+ if stanza is not None:
+ stanza = filter(stanza)
+ if stanza is None:
+ return
+
+ log.debug("RECV: %s", highlight(stanza))
+
+ # Match the stanza against registered handlers. Handlers marked
+ # to run "in stream" will be executed immediately; the rest will
+ # be queued.
+ handled = False
+ matched_handlers = [h for h in self.__handlers if h.match(stanza)]
+ for handler in matched_handlers:
+ handler.prerun(stanza)
+ try:
+ handler.run(stanza)
+ except Exception as e:
+ stanza.exception(e)
+ if handler.check_delete():
+ self.__handlers.remove(handler)
+ handled = True
+
+ # Some stanzas require responses, such as Iq queries. A default
+ # handler will be executed immediately for this case.
+ if not handled:
+ stanza.unhandled()
+
+ def exception(self, exception):
+ """Process an unknown exception.
+
+ Meant to be overridden.
+
+ :param exception: An unhandled exception object.
+ """
+ pass
+