summaryrefslogtreecommitdiff
path: root/slixmpp
diff options
context:
space:
mode:
authorFlorent Le Coz <louiz@louiz.org>2014-07-17 14:19:04 +0200
committerFlorent Le Coz <louiz@louiz.org>2014-07-17 14:19:04 +0200
commit5ab77c745270d7d5c016c1dc7ef2a82533a4b16e (patch)
tree259377cc666f8b9c7954fc4e7b8f7a912bcfe101 /slixmpp
parente5582694c07236e6830c20361840360a1dde37f3 (diff)
downloadslixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.gz
slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.bz2
slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.xz
slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.zip
Rename to slixmpp
Diffstat (limited to 'slixmpp')
-rw-r--r--slixmpp/__init__.py30
-rw-r--r--slixmpp/api.py200
-rw-r--r--slixmpp/basexmpp.py832
-rw-r--r--slixmpp/clientxmpp.py333
-rw-r--r--slixmpp/componentxmpp.py159
-rw-r--r--slixmpp/exceptions.py91
-rw-r--r--slixmpp/features/__init__.py16
-rw-r--r--slixmpp/features/feature_bind/__init__.py19
-rw-r--r--slixmpp/features/feature_bind/bind.py65
-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.py244
-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.py66
-rw-r--r--slixmpp/jid.py638
-rw-r--r--slixmpp/plugins/__init__.py86
-rw-r--r--slixmpp/plugins/base.py360
-rw-r--r--slixmpp/plugins/gmail_notify.py149
-rw-r--r--slixmpp/plugins/google/__init__.py47
-rw-r--r--slixmpp/plugins/google/auth/__init__.py10
-rw-r--r--slixmpp/plugins/google/auth/auth.py52
-rw-r--r--slixmpp/plugins/google/auth/stanza.py49
-rw-r--r--slixmpp/plugins/google/gmail/__init__.py10
-rw-r--r--slixmpp/plugins/google/gmail/notifications.py96
-rw-r--r--slixmpp/plugins/google/gmail/stanza.py101
-rw-r--r--slixmpp/plugins/google/nosave/__init__.py10
-rw-r--r--slixmpp/plugins/google/nosave/nosave.py83
-rw-r--r--slixmpp/plugins/google/nosave/stanza.py59
-rw-r--r--slixmpp/plugins/google/settings/__init__.py10
-rw-r--r--slixmpp/plugins/google/settings/settings.py65
-rw-r--r--slixmpp/plugins/google/settings/stanza.py110
-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.py183
-rw-r--r--slixmpp/plugins/xep_0004/stanza/form.py257
-rw-r--r--slixmpp/plugins/xep_0009/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0009/binding.py173
-rw-r--r--slixmpp/plugins/xep_0009/remote.py742
-rw-r--r--slixmpp/plugins/xep_0009/rpc.py218
-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.py157
-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.py134
-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.py110
-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.py170
-rw-r--r--slixmpp/plugins/xep_0027/stanza.py53
-rw-r--r--slixmpp/plugins/xep_0030/__init__.py23
-rw-r--r--slixmpp/plugins/xep_0030/disco.py740
-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.py402
-rw-r--r--slixmpp/plugins/xep_0047/__init__.py21
-rw-r--r--slixmpp/plugins/xep_0047/ibb.py215
-rw-r--r--slixmpp/plugins/xep_0047/stanza.py67
-rw-r--r--slixmpp/plugins/xep_0047/stream.py148
-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.py53
-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.py688
-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.py561
-rw-r--r--slixmpp/plugins/xep_0054/vcard_temp.py146
-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.py577
-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__.py7
-rw-r--r--slixmpp/plugins/xep_0065/proxy.py292
-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.py115
-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.py147
-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.py125
-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.py111
-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.py85
-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.py214
-rw-r--r--slixmpp/plugins/xep_0096/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0096/file_transfer.py58
-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.py93
-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.py88
-rw-r--r--slixmpp/plugins/xep_0115/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0115/caps.py345
-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.py96
-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.py54
-rw-r--r--slixmpp/plugins/xep_0152/__init__.py16
-rw-r--r--slixmpp/plugins/xep_0152/reachability.py93
-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.py152
-rw-r--r--slixmpp/plugins/xep_0163.py123
-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.py92
-rw-r--r--slixmpp/plugins/xep_0184/__init__.py19
-rw-r--r--slixmpp/plugins/xep_0184/receipt.py131
-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.py83
-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.py97
-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.py314
-rw-r--r--slixmpp/plugins/xep_0199/__init__.py20
-rw-r--r--slixmpp/plugins/xep_0199/ping.py186
-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.py98
-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.py127
-rw-r--r--slixmpp/plugins/xep_0223.py127
-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.py140
-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.py65
-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.py39
-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.py81
-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.py94
-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.py255
-rw-r--r--slixmpp/plugins/xep_0323/sensordata.py723
-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.py64
-rw-r--r--slixmpp/plugins/xep_0325/__init__.py18
-rw-r--r--slixmpp/plugins/xep_0325/control.py574
-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/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.py26
-rw-r--r--slixmpp/stanza/error.py174
-rw-r--r--slixmpp/stanza/htmlim.py21
-rw-r--r--slixmpp/stanza/iq.py281
-rw-r--r--slixmpp/stanza/message.py199
-rw-r--r--slixmpp/stanza/nick.py23
-rw-r--r--slixmpp/stanza/presence.py191
-rw-r--r--slixmpp/stanza/rootstanza.py86
-rw-r--r--slixmpp/stanza/roster.py158
-rw-r--r--slixmpp/stanza/stream_error.py83
-rw-r--r--slixmpp/stanza/stream_features.py57
-rw-r--r--slixmpp/test/__init__.py11
-rw-r--r--slixmpp/test/livesocket.py172
-rw-r--r--slixmpp/test/mocksocket.py153
-rw-r--r--slixmpp/test/slixtest.py772
-rw-r--r--slixmpp/thirdparty/__init__.py12
-rw-r--r--slixmpp/thirdparty/gnupg.py1017
-rw-r--r--slixmpp/thirdparty/mini_dateutil.py273
-rw-r--r--slixmpp/thirdparty/ordereddict.py127
-rw-r--r--slixmpp/thirdparty/socks.py378
-rw-r--r--slixmpp/thirdparty/statemachine.py286
-rw-r--r--slixmpp/util/__init__.py43
-rw-r--r--slixmpp/util/misc_ops.py165
-rw-r--r--slixmpp/util/sasl/__init__.py17
-rw-r--r--slixmpp/util/sasl/client.py174
-rw-r--r--slixmpp/util/sasl/mechanisms.py551
-rw-r--r--slixmpp/util/stringprep_profiles.py151
-rw-r--r--slixmpp/version.py13
-rw-r--r--slixmpp/xmlstream/__init__.py19
-rw-r--r--slixmpp/xmlstream/cert.py184
-rw-r--r--slixmpp/xmlstream/filesocket.py54
-rw-r--r--slixmpp/xmlstream/handler/__init__.py15
-rw-r--r--slixmpp/xmlstream/handler/base.py84
-rw-r--r--slixmpp/xmlstream/handler/callback.py79
-rw-r--r--slixmpp/xmlstream/handler/collector.py66
-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/jid.py5
-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.py333
-rw-r--r--slixmpp/xmlstream/scheduler.py250
-rw-r--r--slixmpp/xmlstream/stanzabase.py1644
-rw-r--r--slixmpp/xmlstream/tostring.py172
-rw-r--r--slixmpp/xmlstream/xmlstream.py1808
325 files changed, 37560 insertions, 0 deletions
diff --git a/slixmpp/__init__.py b/slixmpp/__init__.py
new file mode 100644
index 00000000..faf9dad6
--- /dev/null
+++ b/slixmpp/__init__.py
@@ -0,0 +1,30 @@
+"""
+ 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
+if hasattr(logging, 'NullHandler'):
+ NullHandler = logging.NullHandler
+else:
+ class NullHandler(logging.Handler):
+ def handle(self, record):
+ pass
+logging.getLogger(__name__).addHandler(NullHandler())
+del 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, RestartStream
+from slixmpp.xmlstream.matcher import *
+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..03557218
--- /dev/null
+++ b/slixmpp/basexmpp.py
@@ -0,0 +1,832 @@
+# -*- 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
+"""
+
+from __future__ import with_statement, unicode_literals
+
+import sys
+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.stanza.htmlim import HTMLIM
+
+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__)
+
+# In order to make sure that Unicode is handled properly
+# in Python 2.x, reset the default encoding.
+if sys.version_info < (3, 0):
+ from slixmpp.util.misc_ops import setdefaultencoding
+ setdefaultencoding('utf8')
+
+
+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'):
+ XMLStream.__init__(self)
+
+ 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, cache_lock=True)
+
+ #: 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, cache_lock=True)
+
+ 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('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, *args, **kwargs):
+ """Initialize plugins and begin processing the XML stream.
+
+ The number of threads used for processing stream events is determined
+ by :data:`HANDLER_THREADS`.
+
+ :param bool block: If ``False``, then event dispatcher will run
+ in a separate thread, allowing for the stream to be
+ used in the background for another application.
+ Otherwise, ``process(block=True)`` blocks the current
+ thread. Defaults to ``False``.
+ :param bool threaded: **DEPRECATED**
+ If ``True``, then event dispatcher will run
+ in a separate thread, allowing for the stream to be
+ used in the background for another application.
+ Defaults to ``True``. This does **not** mean that no
+ threads are used at all if ``threaded=False``.
+
+ Regardless of these threading options, these threads will
+ always exist:
+
+ - The event queue processor
+ - The send queue processor
+ - The scheduler
+ """
+ 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
+ return XMLStream.process(self, *args, **kwargs)
+
+ def register_plugin(self, plugin, pconfig={}, 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, cache_lock=True)
+
+ 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_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)
+
+
+# Restore the old, lowercased name for backwards compatibility.
+basexmpp = BaseXMPP
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+BaseXMPP.registerPlugin = BaseXMPP.register_plugin
+BaseXMPP.makeIq = BaseXMPP.make_iq
+BaseXMPP.makeIqGet = BaseXMPP.make_iq_get
+BaseXMPP.makeIqResult = BaseXMPP.make_iq_result
+BaseXMPP.makeIqSet = BaseXMPP.make_iq_set
+BaseXMPP.makeIqError = BaseXMPP.make_iq_error
+BaseXMPP.makeIqQuery = BaseXMPP.make_iq_query
+BaseXMPP.makeQueryRoster = BaseXMPP.make_query_roster
+BaseXMPP.makeMessage = BaseXMPP.make_message
+BaseXMPP.makePresence = BaseXMPP.make_presence
+BaseXMPP.sendMessage = BaseXMPP.send_message
+BaseXMPP.sendPresence = BaseXMPP.send_presence
+BaseXMPP.sendPresenceSubscription = BaseXMPP.send_presence_subscription
diff --git a/slixmpp/clientxmpp.py b/slixmpp/clientxmpp.py
new file mode 100644
index 00000000..55c82f82
--- /dev/null
+++ b/slixmpp/clientxmpp.py
@@ -0,0 +1,333 @@
+# -*- 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
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+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 ssl: **Deprecated.**
+ :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={}, plugin_whitelist=[],
+ escape_quotes=True, sasl_mech=None, lang='en'):
+ BaseXMPP.__init__(self, jid, 'jabber:client')
+
+ 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(), reattempt=True,
+ use_tls=True, use_ssl=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 reattempt: If ``True``, repeat attempting to connect if an
+ error occurs. Defaults to ``True``.
+ :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``.
+ """
+ self.session_started_event.clear()
+
+ # 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_tls=use_tls, use_ssl=use_ssl,
+ reattempt=reattempt)
+
+ 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 block: Specify if the roster request will block
+ until a response is received, or a timeout
+ occurs. Defaults to ``True``.
+ :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'])
+
+ block = kwargs.get('block', True)
+ timeout = kwargs.get('timeout', None)
+ callback = kwargs.get('callback', None)
+
+ return self.client_roster.update(jid, name, subscription, groups,
+ block, 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, block=True, timeout=None, callback=None):
+ """Request the roster from the server.
+
+ :param block: Specify if the roster request will block until a
+ response is received, or a timeout occurs.
+ Defaults to ``True``.
+ :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``.
+ """
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq.enable('roster')
+ if 'rosterver' in self.features:
+ iq['roster']['ver'] = self.client_roster.version
+
+
+ if not block or callback is not None:
+ block = False
+ 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
+
+ response = iq.send(block, timeout, callback)
+
+ if block:
+ self.event('roster_update', response)
+ return response
+
+ 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]
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+ClientXMPP.updateRoster = ClientXMPP.update_roster
+ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
+ClientXMPP.getRoster = ClientXMPP.get_roster
+ClientXMPP.registerFeature = ClientXMPP.register_feature
diff --git a/slixmpp/componentxmpp.py b/slixmpp/componentxmpp.py
new file mode 100644
index 00000000..c640c3dc
--- /dev/null
+++ b/slixmpp/componentxmpp.py
@@ -0,0 +1,159 @@
+# -*- 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
+"""
+
+from __future__ import absolute_import
+
+import logging
+import sys
+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={}, plugin_whitelist=[], use_jc_ns=False):
+ 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.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,
+ use_tls=False, reattempt=True):
+ """Connect to the server.
+
+ Setting ``reattempt`` to ``True`` will cause connection attempts to
+ be made every second until a successful connection is established.
+
+ :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.
+ :param use_tls: Flag indicating if TLS should be used, allowing for
+ connecting to a port without using SSL immediately and
+ later upgrading the connection.
+ :param reattempt: Flag indicating if the socket should reconnect
+ after disconnections.
+ """
+ if host is None:
+ host = self.server_host
+ if port is None:
+ port = self.server_port
+
+ self.server_name = self.boundjid.host
+
+ if use_tls:
+ log.info("XEP-0114 components can not use TLS")
+
+ log.debug("Connecting to %s:%s", host, port)
+ return XMLStream.connect(self, host=host, port=port,
+ use_ssl=use_ssl,
+ use_tls=False,
+ reattempt=reattempt)
+
+ 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 = '%s%s' % (sid, self.secret)
+ if sys.version_info >= (3, 0):
+ # Handle Unicode byte encoding in Python 3.
+ pre_hash = bytes(pre_hash, 'utf-8')
+
+ handshake = ET.Element('{jabber:component:accept}handshake')
+ handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
+ self.send_xml(handshake, now=True)
+
+ def _handle_handshake(self, xml):
+ """The handshake has been accepted.
+
+ :param xml: The reply handshake stanza.
+ """
+ self.session_bind_event.set()
+ self.session_started_event.set()
+ self.event('session_bind', self.boundjid, direct=True)
+ 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..94bbf202
--- /dev/null
+++ b/slixmpp/exceptions.py
@@ -0,0 +1,91 @@
+# -*- 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
+
+
+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..ac69ee77
--- /dev/null
+++ b/slixmpp/features/feature_bind/bind.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.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)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('bind')
+ if self.xmpp.requested_jid.resource:
+ iq['bind']['resource'] = self.xmpp.requested_jid.resource
+ response = iq.send(now=True)
+
+ self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True)
+ self.xmpp.bound = True
+ self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
+ 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 features['features']:
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.session_started_event.set()
+ 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..663bfe57
--- /dev/null
+++ b/slixmpp/features/feature_mechanisms/mechanisms.py
@@ -0,0 +1,244 @@
+"""
+ 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 RestartStream, 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("no_auth", direct=True)
+ self.xmpp.event("failed_auth", direct=True)
+ 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.SASLFailed:
+ 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()
+ else:
+ resp.send(now=True)
+
+ 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.SASLFailed:
+ 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()
+ else:
+ if resp.get_value() == '':
+ resp.del_value()
+ resp.send(now=True)
+
+ 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, direct=True)
+ raise RestartStream()
+
+ 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, direct=True)
+ 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..c2694a9f
--- /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(now=True)
+
+ self.xmpp.features.add('session')
+
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.session_started_event.set()
+ 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..4b9dd60b
--- /dev/null
+++ b/slixmpp/features/feature_starttls/starttls.py
@@ -0,0 +1,66 @@
+"""
+ 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 RestartStream, 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 not self.xmpp.use_tls:
+ return False
+ else:
+ self.xmpp.send(features['starttls'], now=True)
+ 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')
+ raise RestartStream()
diff --git a/slixmpp/jid.py b/slixmpp/jid.py
new file mode 100644
index 00000000..b7ebfbe9
--- /dev/null
+++ b/slixmpp/jid.py
@@ -0,0 +1,638 @@
+# -*- 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
+"""
+
+from __future__ import unicode_literals
+
+import re
+import socket
+import stringprep
+import threading
+import encodings.idna
+
+from copy import deepcopy
+
+from slixmpp.util import stringprep_profiles
+from slixmpp.thirdparty import OrderedDict
+
+#: These characters are not allowed to appear in a JID.
+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'
+
+#: 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 = set(['\\20', '\\22', '\\26', '\\27', '\\2f',
+ '\\3a', '\\3c', '\\3e', '\\40', '\\5c'])
+
+#: A mapping of unallowed characters to their escape sequences. An escape
+#: sequence for '\' is also included since it must also be escaped in
+#: certain situations.
+JID_ESCAPE_TRANSFORMATIONS = {' ': '\\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': '\\'}
+
+JID_CACHE = OrderedDict()
+JID_CACHE_LOCK = threading.Lock()
+JID_CACHE_MAX_SIZE = 1024
+
+def _cache(key, parts, locked):
+ JID_CACHE[key] = (parts, locked)
+ if len(JID_CACHE) > JID_CACHE_MAX_SIZE:
+ with JID_CACHE_LOCK:
+ while len(JID_CACHE) > JID_CACHE_MAX_SIZE:
+ found = None
+ for key, item in JID_CACHE.items():
+ if not item[1]: # if not locked
+ found = key
+ break
+ if not found: # more than MAX_SIZE locked
+ # warn?
+ break
+ del JID_CACHE[found]
+
+# 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])
+
+# 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 _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.
+ """
+ try:
+ if node is not None:
+ node = nodeprep(node)
+
+ 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
+ except stringprep_profiles.StringPrepError:
+ raise InvalidJID('Invalid local part')
+
+
+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 hasattr(socket, 'inet_pton'):
+ try:
+ socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
+ domain = '[%s]' % domain.strip('[]')
+ 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]
+
+ domain_parts = []
+ for label in domain.split('.'):
+ try:
+ label = encodings.idna.nameprep(label)
+ encodings.idna.ToASCII(label)
+ pass_nameprep = True
+ except UnicodeError:
+ pass_nameprep = False
+
+ if not pass_nameprep:
+ raise InvalidJID('Could not encode domain as ASCII')
+
+ if label.startswith('xn--'):
+ label = encodings.idna.ToUnicode(label)
+
+ for char in label:
+ if char in ILLEGAL_CHARS:
+ raise InvalidJID('Domain contains illegal characters')
+
+ if '-' in (label[0], label[-1]):
+ raise InvalidJID('Domain started or ended with -')
+
+ domain_parts.append(label)
+ domain = '.'.join(domain_parts)
+
+ 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.
+ """
+ try:
+ if resource is not None:
+ resource = resourceprep(resource)
+
+ 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
+ except stringprep_profiles.StringPrepError:
+ raise InvalidJID('Invalid resource')
+
+
+def _escape_node(node):
+ """Escape the local portion of a JID."""
+ result = []
+
+ for i, char in enumerate(node):
+ if char == '\\':
+ if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES:
+ result.append('\\5c')
+ continue
+ result.append(char)
+
+ for i, char in enumerate(result):
+ if char != '\\':
+ result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char)
+
+ escaped = ''.join(result)
+
+ if escaped.startswith('\\20') or escaped.endswith('\\20'):
+ raise InvalidJID('Escaped local part starts or ends with "\\20"')
+
+ _validate_node(escaped)
+
+ return escaped
+
+
+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)
+ unescaped = ''.join(unescaped)
+
+ return 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:
+ result.append(local)
+ result.append('@')
+ if domain:
+ result.append(domain)
+ if resource:
+ 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(object):
+
+ """
+ .. versionadded:: 1.1.10
+ """
+
+ def __init__(self, local, domain, resource):
+ self._jid = (local, domain, resource)
+
+ # pylint: disable=R0911
+ def __getattr__(self, name):
+ """Retrieve the given JID component.
+
+ :param name: one of: user, server, domain, resource,
+ full, or bare.
+ """
+ if name == 'resource':
+ return self._jid[2] or ''
+ elif name in ('user', 'username', 'local', 'node'):
+ return self._jid[0] or ''
+ elif name in ('server', 'domain', 'host'):
+ return self._jid[1] or ''
+ elif name in ('full', 'jid'):
+ return _format_jid(*self._jid)
+ elif name == 'bare':
+ return _format_jid(self._jid[0], self._jid[1])
+ elif name == '_jid':
+ return getattr(super(JID, self), '_jid')
+ else:
+ return None
+
+ def __str__(self):
+ """Use the full JID as the string value."""
+ return _format_jid(*self._jid)
+
+ def __repr__(self):
+ """Use the full JID as the representation."""
+ return self.__str__()
+
+
+class JID(object):
+
+ """
+ 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:**
+ :jid: Alias for ``full``.
+ :full: The string value of the full JID.
+ :bare: The string value of the bare JID.
+ :user: The username portion of the JID.
+ :username: Alias for ``user``.
+ :local: Alias for ``user``.
+ :node: Alias for ``user``.
+ :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]'``.
+ :param string local:
+ Optional. Specify the local, or username, portion
+ of the JID. If provided, it will override the local
+ value provided by the `jid` parameter. The given
+ local value will also be escaped if necessary.
+ :param string domain:
+ Optional. Specify the domain of the JID. If
+ provided, it will override the domain given by
+ the `jid` parameter.
+ :param string resource:
+ Optional. Specify the resource value of the JID.
+ If provided, it will override the domain given
+ by the `jid` parameter.
+
+ :raises InvalidJID:
+ """
+
+ # pylint: disable=W0212
+ def __init__(self, jid=None, **kwargs):
+ locked = kwargs.get('cache_lock', False)
+ in_local = kwargs.get('local', None)
+ in_domain = kwargs.get('domain', None)
+ in_resource = kwargs.get('resource', None)
+ parts = None
+ if in_local or in_domain or in_resource:
+ parts = (in_local, in_domain, in_resource)
+
+ # only check cache if there is a jid string, or parts, not if there
+ # are both
+ self._jid = None
+ key = None
+ if (jid is not None) and (parts is None):
+ if isinstance(jid, JID):
+ # it's already good to go, and there are no additions
+ self._jid = jid._jid
+ return
+ key = jid
+ self._jid, locked = JID_CACHE.get(jid, (None, locked))
+ elif jid is None and parts is not None:
+ key = parts
+ self._jid, locked = JID_CACHE.get(parts, (None, locked))
+ if not self._jid:
+ if not jid:
+ parsed_jid = (None, None, None)
+ elif not isinstance(jid, JID):
+ parsed_jid = _parse_jid(jid)
+ else:
+ parsed_jid = jid._jid
+
+ local, domain, resource = parsed_jid
+
+ if 'local' in kwargs:
+ local = _escape_node(in_local)
+ if 'domain' in kwargs:
+ domain = _validate_domain(in_domain)
+ if 'resource' in kwargs:
+ resource = _validate_resource(in_resource)
+
+ self._jid = (local, domain, resource)
+ if key:
+ _cache(key, self._jid, locked)
+
+ 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._jid[0]),
+ self._jid[1],
+ self._jid[2])
+
+ def regenerate(self):
+ """No-op
+
+ .. deprecated:: 1.1.10
+ """
+ pass
+
+ def reset(self, data):
+ """Start fresh from a new JID string.
+
+ :param string data: A string of the form ``'[user@]domain[/resource]'``.
+
+ .. deprecated:: 1.1.10
+ """
+ self._jid = JID(data)._jid
+
+ @property
+ def resource(self):
+ return self._jid[2] or ''
+
+ @property
+ def user(self):
+ return self._jid[0] or ''
+
+ @property
+ def local(self):
+ return self._jid[0] or ''
+
+ @property
+ def node(self):
+ return self._jid[0] or ''
+
+ @property
+ def username(self):
+ return self._jid[0] or ''
+
+ @property
+ def bare(self):
+ return _format_jid(self._jid[0], self._jid[1])
+
+ @property
+ def server(self):
+ return self._jid[1] or ''
+
+ @property
+ def domain(self):
+ return self._jid[1] or ''
+
+ @property
+ def host(self):
+ return self._jid[1] or ''
+
+ @property
+ def full(self):
+ return _format_jid(*self._jid)
+
+ @property
+ def jid(self):
+ return _format_jid(*self._jid)
+
+ @property
+ def bare(self):
+ return _format_jid(self._jid[0], self._jid[1])
+
+
+ @resource.setter
+ def resource(self, value):
+ self._jid = JID(self, resource=value)._jid
+
+ @user.setter
+ def user(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @username.setter
+ def username(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @local.setter
+ def local(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @node.setter
+ def node(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @server.setter
+ def server(self, value):
+ self._jid = JID(self, domain=value)._jid
+
+ @domain.setter
+ def domain(self, value):
+ self._jid = JID(self, domain=value)._jid
+
+ @host.setter
+ def host(self, value):
+ self._jid = JID(self, domain=value)._jid
+
+ @full.setter
+ def full(self, value):
+ self._jid = JID(value)._jid
+
+ @jid.setter
+ def jid(self, value):
+ self._jid = JID(value)._jid
+
+ @bare.setter
+ def bare(self, value):
+ parsed = JID(value)._jid
+ self._jid = (parsed[0], parsed[1], self._jid[2])
+
+
+ def __str__(self):
+ """Use the full JID as the string value."""
+ return _format_jid(*self._jid)
+
+ def __repr__(self):
+ """Use the full JID as the representation."""
+ return self.__str__()
+
+ # 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
+
+ other = JID(other)
+ return self._jid == other._jid
+
+ # pylint: disable=W0212
+ 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(self.__str__())
+
+ def __copy__(self):
+ """Generate a duplicate JID."""
+ return JID(self)
+
+ def __deepcopy__(self, memo):
+ """Generate a duplicate JID."""
+ return JID(deepcopy(str(self), memo))
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
new file mode 100644
index 00000000..cf24caed
--- /dev/null
+++ b/slixmpp/plugins/__init__.py
@@ -0,0 +1,86 @@
+"""
+ 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_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
+]
diff --git a/slixmpp/plugins/base.py b/slixmpp/plugins/base.py
new file mode 100644
index 00000000..9694a414
--- /dev/null
+++ b/slixmpp/plugins/base.py
@@ -0,0 +1,360 @@
+# -*- 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
+
+
+if sys.version_info >= (3, 0):
+ unicode = str
+
+
+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, unicode)):
+ __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.
+ """
+ top_level = False
+ 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()
+
+ if top_level:
+ 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
+ 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
+
+
+base_plugin = BasePlugin
diff --git a/slixmpp/plugins/gmail_notify.py b/slixmpp/plugins/gmail_notify.py
new file mode 100644
index 00000000..d40e932d
--- /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 . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, 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(base.base_plugin):
+ """
+ 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))
+
+ registerStanzaPlugin(Iq, GmailQuery)
+ registerStanzaPlugin(Iq, MailBox)
+ registerStanzaPlugin(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/__init__.py b/slixmpp/plugins/google/__init__.py
new file mode 100644
index 00000000..443bce35
--- /dev/null
+++ b/slixmpp/plugins/google/__init__.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.plugins.base import register_plugin, BasePlugin
+
+from slixmpp.plugins.google.gmail import Gmail
+from slixmpp.plugins.google.auth import GoogleAuth
+from slixmpp.plugins.google.settings import GoogleSettings
+from slixmpp.plugins.google.nosave import GoogleNoSave
+
+
+class Google(BasePlugin):
+
+ """
+ Google: Custom GTalk Features
+
+ Also see: <https://developers.google.com/talk/jep_extensions/extensions>
+ """
+
+ name = 'google'
+ description = 'Google: Custom GTalk Features'
+ dependencies = set([
+ 'gmail',
+ 'google_settings',
+ 'google_nosave',
+ 'google_auth'
+ ])
+
+ def __getitem__(self, attr):
+ if attr in ('settings', 'nosave', 'auth'):
+ return self.xmpp['google_%s' % attr]
+ elif attr == 'gmail':
+ return self.xmpp['gmail']
+ else:
+ raise KeyError(attr)
+
+
+register_plugin(Gmail)
+register_plugin(GoogleAuth)
+register_plugin(GoogleSettings)
+register_plugin(GoogleNoSave)
+register_plugin(Google)
diff --git a/slixmpp/plugins/google/auth/__init__.py b/slixmpp/plugins/google/auth/__init__.py
new file mode 100644
index 00000000..ceb94e4c
--- /dev/null
+++ b/slixmpp/plugins/google/auth/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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.google.auth import stanza
+from slixmpp.plugins.google.auth.auth import GoogleAuth
diff --git a/slixmpp/plugins/google/auth/auth.py b/slixmpp/plugins/google/auth/auth.py
new file mode 100644
index 00000000..e01d81a7
--- /dev/null
+++ b/slixmpp/plugins/google/auth/auth.py
@@ -0,0 +1,52 @@
+"""
+ 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.xmlstream import register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.google.auth import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class GoogleAuth(BasePlugin):
+
+ """
+ Google: Auth Extensions (JID Domain Discovery, OAuth2)
+
+ Also see:
+ <https://developers.google.com/talk/jep_extensions/jid_domain_change>
+ <https://developers.google.com/talk/jep_extensions/oauth>
+ """
+
+ name = 'google_auth'
+ description = 'Google: Auth Extensions (JID Domain Discovery, OAuth2)'
+ dependencies = set(['feature_mechanisms'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.namespace_map['http://www.google.com/talk/protocol/auth'] = 'ga'
+
+ register_stanza_plugin(self.xmpp['feature_mechanisms'].stanza.Auth,
+ stanza.GoogleAuth)
+
+ self.xmpp.add_filter('out', self._auth)
+
+ def plugin_end(self):
+ self.xmpp.del_filter('out', self._auth)
+
+ def _auth(self, stanza):
+ if isinstance(stanza, self.xmpp['feature_mechanisms'].stanza.Auth):
+ stanza.stream = self.xmpp
+ stanza['google']['client_uses_full_bind_result'] = True
+ if stanza['mechanism'] == 'X-OAUTH2':
+ stanza['google']['service'] = 'oauth2'
+ print(stanza)
+ return stanza
diff --git a/slixmpp/plugins/google/auth/stanza.py b/slixmpp/plugins/google/auth/stanza.py
new file mode 100644
index 00000000..524a41e2
--- /dev/null
+++ b/slixmpp/plugins/google/auth/stanza.py
@@ -0,0 +1,49 @@
+"""
+ 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('')
+ print('setting up google extension')
+
+ def get_client_uses_full_bind_result(self):
+ return self.parent()._get_attr(self.disovery_attr) == 'true'
+
+ def set_client_uses_full_bind_result(self, value):
+ print('>>>', 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/__init__.py b/slixmpp/plugins/google/gmail/__init__.py
new file mode 100644
index 00000000..beb960de
--- /dev/null
+++ b/slixmpp/plugins/google/gmail/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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.google.gmail import stanza
+from slixmpp.plugins.google.gmail.notifications import Gmail
diff --git a/slixmpp/plugins/google/gmail/notifications.py b/slixmpp/plugins/google/gmail/notifications.py
new file mode 100644
index 00000000..aa9b6a37
--- /dev/null
+++ b/slixmpp/plugins/google/gmail/notifications.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.
+"""
+
+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, block=True, timeout=None, callback=None):
+ last_time = self._last_result_time
+ last_tid = self._last_result_tid
+
+ if not block:
+ callback = lambda iq: self._update_last_results(iq, callback)
+
+ resp = self.search(newer_time=last_time,
+ newer_tid=last_tid,
+ block=block,
+ timeout=timeout,
+ callback=callback)
+
+ if block:
+ self._update_last_results(resp)
+ return resp
+
+ def _update_last_results(self, iq, callback=None):
+ self._last_result_time = data['gmail_messages']['result_time']
+ threads = data['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, block=True,
+ 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(block=block, timeout=timeout, callback=callback)
diff --git a/slixmpp/plugins/google/gmail/stanza.py b/slixmpp/plugins/google/gmail/stanza.py
new file mode 100644
index 00000000..73826f54
--- /dev/null
+++ b/slixmpp/plugins/google/gmail/stanza.py
@@ -0,0 +1,101 @@
+"""
+ 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 GmailQuery(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'query'
+ plugin_attrib = 'gmail'
+ interfaces = set(['newer_than_time', 'newer_than_tid', 'search'])
+
+ def get_search(self):
+ return self._get_attr('q', '')
+
+ def set_search(self, search):
+ self._set_attr('q', search)
+
+ def del_search(self):
+ self._del_attr('q')
+
+ def get_newer_than_time(self):
+ return self._get_attr('newer-than-time', '')
+
+ def set_newer_than_time(self, value):
+ self._set_attr('newer-than-time', value)
+
+ def del_newer_than_time(self):
+ self._del_attr('newer-than-time')
+
+ def get_newer_than_tid(self):
+ return self._get_attr('newer-than-tid', '')
+
+ def set_newer_than_tid(self, value):
+ self._set_attr('newer-than-tid', value)
+
+ def del_newer_than_tid(self):
+ self._del_attr('newer-than-tid')
+
+
+class MailBox(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mailbox'
+ plugin_attrib = 'gmail_messages'
+ interfaces = set(['result_time', 'url', 'matched', 'estimate'])
+
+ def get_matched(self):
+ return self._get_attr('total-matched', '')
+
+ def get_estimate(self):
+ return self._get_attr('total-estimate', '') == '1'
+
+ def get_result_time(self):
+ return self._get_attr('result-time', '')
+
+
+class MailThread(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mail-thread-info'
+ plugin_attrib = 'thread'
+ plugin_multi_attrib = 'threads'
+ interfaces = set(['tid', 'participation', 'messages', 'date',
+ 'senders', 'url', 'labels', 'subject', 'snippet'])
+ sub_interfaces = set(['labels', 'subject', 'snippet'])
+
+ def get_senders(self):
+ result = []
+ senders = self.xml.findall('{%s}senders/{%s}sender' % (
+ self.namespace, self.namespace))
+
+ for sender in senders:
+ result.append(MailSender(xml=sender))
+
+ return result
+
+
+class MailSender(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'sender'
+ plugin_attrib = name
+ interfaces = set(['address', 'name', 'originator', 'unread'])
+
+ def get_originator(self):
+ return self.xml.attrib.get('originator', '0') == '1'
+
+ def get_unread(self):
+ return self.xml.attrib.get('unread', '0') == '1'
+
+
+class NewMail(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'new-mail'
+ plugin_attrib = 'gmail_notification'
+
+
+register_stanza_plugin(MailBox, MailThread, iterable=True)
diff --git a/slixmpp/plugins/google/nosave/__init__.py b/slixmpp/plugins/google/nosave/__init__.py
new file mode 100644
index 00000000..ee20d6ea
--- /dev/null
+++ b/slixmpp/plugins/google/nosave/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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.google.nosave import stanza
+from slixmpp.plugins.google.nosave.nosave import GoogleNoSave
diff --git a/slixmpp/plugins/google/nosave/nosave.py b/slixmpp/plugins/google/nosave/nosave.py
new file mode 100644
index 00000000..735830a8
--- /dev/null
+++ b/slixmpp/plugins/google/nosave/nosave.py
@@ -0,0 +1,83 @@
+"""
+ 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, 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.google.nosave import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class GoogleNoSave(BasePlugin):
+
+ """
+ Google: Off the Record Chats
+
+ NOTE: This is NOT an encryption method.
+
+ Also see <https://developers.google.com/talk/jep_extensions/otr>.
+ """
+
+ name = 'google_nosave'
+ description = 'Google: Off the Record Chats'
+ dependencies = set(['google_settings'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, stanza.NoSave)
+ register_stanza_plugin(Iq, stanza.NoSaveQuery)
+
+ self.xmpp.register_handler(
+ Callback('Google Nosave',
+ StanzaPath('iq@type=set/google_nosave'),
+ self._handle_nosave_change))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Google Nosave')
+
+ def enable(self, jid=None, block=True, timeout=None, callback=None):
+ if jid is None:
+ self.xmpp['google_settings'].update({'archiving_enabled': False},
+ block=block, timeout=timeout, callback=callback)
+ else:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['google_nosave']['item']['jid'] = jid
+ iq['google_nosave']['item']['value'] = True
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def disable(self, jid=None, block=True, timeout=None, callback=None):
+ if jid is None:
+ self.xmpp['google_settings'].update({'archiving_enabled': True},
+ block=block, timeout=timeout, callback=callback)
+ else:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['google_nosave']['item']['jid'] = jid
+ iq['google_nosave']['item']['value'] = False
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('google_nosave')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def _handle_nosave_change(self, iq):
+ reply = self.xmpp.Iq()
+ reply['type'] = 'result'
+ reply['id'] = iq['id']
+ reply['to'] = iq['from']
+ reply.send()
+ self.xmpp.event('google_nosave_change', iq)
diff --git a/slixmpp/plugins/google/nosave/stanza.py b/slixmpp/plugins/google/nosave/stanza.py
new file mode 100644
index 00000000..c9e1fb97
--- /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):
+ self._set_attr('source', str(value))
+
+
+register_stanza_plugin(NoSaveQuery, Item)
diff --git a/slixmpp/plugins/google/settings/__init__.py b/slixmpp/plugins/google/settings/__init__.py
new file mode 100644
index 00000000..fa49e875
--- /dev/null
+++ b/slixmpp/plugins/google/settings/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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.google.settings import stanza
+from slixmpp.plugins.google.settings.settings import GoogleSettings
diff --git a/slixmpp/plugins/google/settings/settings.py b/slixmpp/plugins/google/settings/settings.py
new file mode 100644
index 00000000..3dfb338d
--- /dev/null
+++ b/slixmpp/plugins/google/settings/settings.py
@@ -0,0 +1,65 @@
+"""
+ 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 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, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('google_settings')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def update(self, settings, block=True, 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(block=block, 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/google/settings/stanza.py b/slixmpp/plugins/google/settings/stanza.py
new file mode 100644
index 00000000..556a6538
--- /dev/null
+++ b/slixmpp/plugins/google/settings/stanza.py
@@ -0,0 +1,110 @@
+"""
+ 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 ET, ElementBase, register_stanza_plugin
+
+
+class UserSettings(ElementBase):
+ name = 'usersetting'
+ namespace = 'google:setting'
+ plugin_attrib = 'google_settings'
+ interfaces = set(['auto_accept_suggestions',
+ 'mail_notifications',
+ 'archiving_enabled',
+ 'gmail',
+ 'email_verified',
+ 'domain_privacy_notice',
+ 'display_name'])
+
+ def _get_setting(self, setting):
+ xml = self.xml.find('{%s}%s' % (self.namespace, setting))
+ if xml is not None:
+ return xml.attrib.get('value', '') == 'true'
+ return False
+
+ def _set_setting(self, setting, value):
+ self._del_setting(setting)
+ if value in (True, False):
+ xml = ET.Element('{%s}%s' % (self.namespace, setting))
+ xml.attrib['value'] = 'true' if value else 'false'
+ self.xml.append(xml)
+
+ def _del_setting(self, setting):
+ xml = self.xml.find('{%s}%s' % (self.namespace, setting))
+ if xml is not None:
+ self.xml.remove(xml)
+
+ def get_display_name(self):
+ xml = self.xml.find('{%s}%s' % (self.namespace, 'displayname'))
+ if xml is not None:
+ return xml.attrib.get('value', '')
+ return ''
+
+ def set_display_name(self, value):
+ self._del_setting(setting)
+ if value:
+ xml = ET.Element('{%s}%s' % (self.namespace, 'displayname'))
+ xml.attrib['value'] = value
+ self.xml.append(xml)
+
+ def del_display_name(self):
+ self._del_setting('displayname')
+
+ def get_auto_accept_suggestions(self):
+ return self._get_setting('autoacceptsuggestions')
+
+ def get_mail_notifications(self):
+ return self._get_setting('mailnotifications')
+
+ def get_archiving_enabled(self):
+ return self._get_setting('archivingenabled')
+
+ def get_gmail(self):
+ return self._get_setting('gmail')
+
+ def get_email_verified(self):
+ return self._get_setting('emailverified')
+
+ def get_domain_privacy_notice(self):
+ return self._get_setting('domainprivacynotice')
+
+ def set_auto_accept_suggestions(self, value):
+ self._set_setting('autoacceptsuggestions', value)
+
+ def set_mail_notifications(self, value):
+ self._set_setting('mailnotifications', value)
+
+ def set_archiving_enabled(self, value):
+ self._set_setting('archivingenabled', value)
+
+ def set_gmail(self, value):
+ self._set_setting('gmail', value)
+
+ def set_email_verified(self, value):
+ self._set_setting('emailverified', value)
+
+ def set_domain_privacy_notice(self, value):
+ self._set_setting('domainprivacynotice', value)
+
+ def del_auto_accept_suggestions(self):
+ self._del_setting('autoacceptsuggestions')
+
+ def del_mail_notifications(self):
+ self._del_setting('mailnotifications')
+
+ def del_archiving_enabled(self):
+ self._del_setting('archivingenabled')
+
+ def del_gmail(self):
+ self._del_setting('gmail')
+
+ def del_email_verified(self):
+ self._del_setting('emailverified')
+
+ def del_domain_privacy_notice(self):
+ self._del_setting('domainprivacynotice')
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..ceddcd0e
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/stanza/field.py
@@ -0,0 +1,183 @@
+"""
+ 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'
+ interfaces = set(('answer', 'desc', 'required', 'value',
+ 'options', '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',))
+
+
+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..1f900ee6
--- /dev/null
+++ b/slixmpp/plugins/xep_0004/stanza/form.py
@@ -0,0 +1,257 @@
+"""
+ 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 slixmpp.thirdparty import OrderedDict
+
+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 = set(('fields', 'instructions', 'items',
+ 'reported', 'title', 'type', 'values'))
+ 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['fields']
+
+ def set_type(self, ftype):
+ self._set_attr('type', ftype)
+ if ftype == 'submit':
+ fields = self['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:
+ field['options'] = options
+ 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):
+ instructions = ''
+ 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['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(**field)
+
+ 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):
+ for var in reported:
+ field = reported[var]
+ field['var'] = var
+ self.add_reported(var, **field)
+
+ def set_values(self, values):
+ fields = self['fields']
+ for field in values:
+ if field not in fields:
+ fields[field] = self.add_field(var=field)
+ 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.setType = Form.set_type
+Form.addField = Form.add_field
+Form.addItem = Form.add_item
+Form.addReported = Form.add_reported
+Form.delFields = Form.del_fields
+Form.delInstructions = Form.del_instructions
+Form.delItems = Form.del_items
+Form.delReported = Form.del_reported
+Form.getFields = Form.get_fields
+Form.getInstructions = Form.get_instructions
+Form.getItems = Form.get_items
+Form.getReported = Form.get_reported
+Form.getValues = Form.get_values
+Form.setFields = Form.set_fields
+Form.setInstructions = Form.set_instructions
+Form.setItems = Form.set_items
+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..5416fc41
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/binding.py
@@ -0,0 +1,173 @@
+"""
+ 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
+import sys
+
+if sys.version_info > (3, 0):
+ unicode = str
+
+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) in (str, unicode):
+ 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) in (str, unicode):
+ 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..99df929a
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/remote.py
@@ -0,0 +1,742 @@
+"""
+ 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 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 _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 isinstance(function_argument, basestring):
+ 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
+
+
+class Callback(object):
+ '''
+ A base class for callback handlers.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+
+ @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()
+
+
+
+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>'.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+
+ 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.sendPresence()
+ 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.iteritems() 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.iteritems():
+ #!!! 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):
+ '''
+ Closes this session.
+ '''
+ self._client.disconnect(False)
+ 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.registerPlugin('xep_0004') # Data Forms
+ client.registerPlugin('xep_0009') # Jabber-RPC
+ client.registerPlugin('xep_0030') # Service Discovery
+ client.registerPlugin('xep_0060') # PubSub
+ client.registerPlugin('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..ff17c417
--- /dev/null
+++ b/slixmpp/plugins/xep_0009/rpc.py
@@ -0,0 +1,218 @@
+"""
+ 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.makeIqSet()
+ 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.makeIqResult(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.makeIqResult(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.makeIqError(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.reply().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.reply().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.reply().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.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..2fef9ca8
--- /dev/null
+++ b/slixmpp/plugins/xep_0012/last_activity.py
@@ -0,0 +1,157 @@
+"""
+ 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 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)
+
+ def get_last_activity(self, jid, local=False, ifrom=None, block=True,
+ timeout=None, 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,
+ block=block,
+ callback=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:
+ iq.reply()
+ reply = iq
+
+ 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..d6b500d3
--- /dev/null
+++ b/slixmpp/plugins/xep_0013/offline.py
@@ -0,0 +1,134 @@
+"""
+ 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, block=True, timeout=None, 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)
+
+ if not block and callback is not None:
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['offline']['results'] = results
+ callback(iq)
+ return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
+ else:
+ try:
+ resp = iq.send(block=block, timeout=timeout, callback=callback)
+ resp['offline']['results'] = collector.stop()
+ return resp
+ except XMPPError as e:
+ collector.stop()
+ raise e
+
+ def remove(self, nodes, ifrom=None, block=True, timeout=None, 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)
+
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def fetch(self, ifrom=None, block=True, timeout=None, 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)
+
+ if not block and callback is not None:
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['offline']['results'] = results
+ callback(iq)
+ return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
+ else:
+ try:
+ resp = iq.send(block=block, timeout=timeout, callback=callback)
+ resp['offline']['results'] = collector.stop()
+ return resp
+ except XMPPError as e:
+ collector.stop()
+ raise e
+
+ def purge(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['offline']['purge'] = True
+ return iq.send(block=block, timeout=timeout, callback=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..25d55048
--- /dev/null
+++ b/slixmpp/plugins/xep_0016/privacy.py
@@ -0,0 +1,110 @@
+"""
+ 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, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('privacy')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_list(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy']['list']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_active(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('active')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_default(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('default')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def activate(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['active']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def deactivate(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('active')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def make_default(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['default']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def remove_default(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('default')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def edit_list(self, name, rules, block=True, timeout=None, 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, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=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..e9978416
--- /dev/null
+++ b/slixmpp/plugins/xep_0027/gpg.py
@@ -0,0 +1,170 @@
+"""
+ 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,
+ threaded=True)
+
+ 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..7f8e52b1
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/__init__.py
@@ -0,0 +1,23 @@
+"""
+ 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.getItems = XEP_0030.get_items
+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..12837958
--- /dev/null
+++ b/slixmpp/plugins/xep_0030/disco.py
@@ -0,0 +1,740 @@
+"""
+ 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.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 _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)
+
+ 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.
+ 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. The
+ timeout value is only used when block=True.
+ 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),
+ block=kwargs.get('block', True),
+ 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)
+
+ 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.
+ 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.
+ 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']:
+ return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
+ else:
+ 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 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={}):
+ """
+ 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.
+ """
+ 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.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.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..e749cc2e
--- /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.append(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..5950ab1b
--- /dev/null
+++ b/slixmpp/plugins/xep_0045.py
@@ -0,0 +1,402 @@
+"""
+ 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 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'].getStanzaValues()
+ 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.makeIqSet()
+ 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.makePresence(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.makeIqSet()
+ 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.makeIqSet(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.makeIqSet(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.makeMessage(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.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
+ else:
+ self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
+ del self.rooms[room]
+
+ def getRoomConfig(self, room, ifrom=''):
+ iq = self.xmpp.makeIqGet('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.makeIqSet(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.makeIqSet(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()
+
+
+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..ce697f01
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/ibb.py
@@ -0,0 +1,215 @@
+import uuid
+import logging
+import threading
+
+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,
+ 'window_size': 1,
+ 'auto_accept': False,
+ }
+
+ def plugin_init(self):
+ self._streams = {}
+ self._pending_streams = {}
+ self._pending_lock = threading.Lock()
+ self._stream_lock = threading.Lock()
+
+ self._preauthed_sids_lock = threading.Lock()
+ 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):
+ with self._stream_lock:
+ 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:
+ if iq['ibb_open']['block_size'] <= self.max_block_size:
+ return True
+ return False
+
+ def _authorized_sid(self, jid, sid, ifrom, iq):
+ with self._preauthed_sids_lock:
+ 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):
+ with self._preauthed_sids_lock:
+ self._preauthed_sids[(jid, sid, ifrom)] = True
+
+ def open_stream(self, jid, block_size=None, sid=None, window=1, use_messages=False,
+ ifrom=None, block=True, 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'] = 'iq'
+
+ stream = IBBytestream(self.xmpp, sid, block_size,
+ iq['from'], iq['to'], window,
+ use_messages)
+
+ with self._stream_lock:
+ self._pending_streams[iq['id']] = stream
+
+ self._pending_streams[iq['id']] = stream
+
+ if block:
+ resp = iq.send(timeout=timeout)
+ self._handle_opened_stream(resp)
+ return stream
+ else:
+ cb = None
+ if callback is not None:
+ def chained(resp):
+ self._handle_opened_stream(resp)
+ callback(resp)
+ cb = chained
+ else:
+ cb = self._handle_opened_stream
+ return iq.send(block=block, timeout=timeout, callback=cb)
+
+ def _handle_opened_stream(self, iq):
+ if iq['type'] == 'result':
+ with self._stream_lock:
+ stream = self._pending_streams.get(iq['id'], None)
+ if stream is not None:
+ 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.set()
+ self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
+ self.xmpp.event('ibb_stream_start', stream)
+ self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream)
+
+ with self._stream_lock:
+ if iq['id'] in self._pending_streams:
+ del self._pending_streams[iq['id']]
+
+ 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='modify', condition='not-acceptable')
+
+ if size > self.max_block_size:
+ raise XMPPError('resource-constraint')
+
+ stream = IBBytestream(self.xmpp, sid, size,
+ iq['to'], iq['from'],
+ self.window_size)
+ stream.stream_started.set()
+ self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
+ iq.reply()
+ iq.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..62199077
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/stanza.py
@@ -0,0 +1,67 @@
+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'))
+
+ 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):
+ b64_data = self.xml.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..d3235972
--- /dev/null
+++ b/slixmpp/plugins/xep_0047/stream.py
@@ -0,0 +1,148 @@
+import socket
+import threading
+import logging
+
+from slixmpp.stanza import Iq
+from slixmpp.util import Queue
+from slixmpp.exceptions import XMPPError
+
+
+log = logging.getLogger(__name__)
+
+
+class IBBytestream(object):
+
+ def __init__(self, xmpp, sid, block_size, jid, peer, window_size=1, use_messages=False):
+ self.xmpp = xmpp
+ self.sid = sid
+ self.block_size = block_size
+ self.window_size = window_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._send_seq_lock = threading.Lock()
+ self._recv_seq_lock = threading.Lock()
+
+ self.stream_started = threading.Event()
+ self.stream_in_closed = threading.Event()
+ self.stream_out_closed = threading.Event()
+
+ self.recv_queue = Queue()
+
+ self.send_window = threading.BoundedSemaphore(value=self.window_size)
+ self.window_ids = set()
+ self.window_empty = threading.Event()
+ self.window_empty.set()
+
+ def send(self, data):
+ if not self.stream_started.is_set() or \
+ self.stream_out_closed.is_set():
+ raise socket.error
+ data = data[0:self.block_size]
+ self.send_window.acquire()
+ with self._send_seq_lock:
+ 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()
+ self.send_window.release()
+ 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
+ self.window_empty.clear()
+ self.window_ids.add(iq['id'])
+ iq.send(block=False, callback=self._recv_ack)
+ return len(data)
+
+ def sendall(self, data):
+ sent_len = 0
+ while sent_len < len(data):
+ sent_len += self.send(data[sent_len:])
+
+ def _recv_ack(self, iq):
+ self.window_ids.remove(iq['id'])
+ if not self.window_ids:
+ self.window_empty.set()
+ self.send_window.release()
+ if iq['type'] == 'error':
+ self.close()
+
+ def _recv_data(self, stanza):
+ with self._recv_seq_lock:
+ 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(data)
+ self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data})
+
+ if isinstance(stanza, Iq):
+ stanza.reply()
+ stanza.send()
+
+ def recv(self, *args, **kwargs):
+ return self.read(block=True)
+
+ def read(self, block=True, timeout=None, **kwargs):
+ if not self.stream_started.is_set() or \
+ self.stream_in_closed.is_set():
+ raise socket.error
+ if timeout is not None:
+ block = True
+ try:
+ return self.recv_queue.get(block, timeout)
+ except:
+ return None
+
+ def close(self):
+ 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.set()
+ iq.send(block=False,
+ callback=lambda x: self.stream_in_closed.set())
+ self.xmpp.event('ibb_stream_end', self)
+
+ def _closed(self, iq):
+ self.stream_in_closed.set()
+ self.stream_out_closed.set()
+ iq.reply()
+ iq.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..99a04fe9
--- /dev/null
+++ b/slixmpp/plugins/xep_0049/private_storage.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.
+"""
+
+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, block=True, timeout=None, 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(block=block, timeout=timeout, callback=callback)
+
+ def retrieve(self, name, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq['private'].enable(name)
+ return iq.send(block=block, timeout=timeout, callback=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..01d36489
--- /dev/null
+++ b/slixmpp/plugins/xep_0050/adhoc.py
@@ -0,0 +1,688 @@
+"""
+ 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 IqError
+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>
+
+ Configuration Values:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+
+ 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:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+ 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 base_plugin.plugin_init
+ post_init -- Overrides base_plugin.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 = {
+ 'threaded': True,
+ '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)
+
+ self.xmpp.add_event_handler('command_execute',
+ self._handle_command_start,
+ threaded=self.threaded)
+ self.xmpp.add_event_handler('command_next',
+ self._handle_command_next,
+ threaded=self.threaded)
+ self.xmpp.add_event_handler('command_cancel',
+ self._handle_command_cancel,
+ threaded=self.threaded)
+ self.xmpp.add_event_handler('command_complete',
+ self._handle_command_complete,
+ threaded=self.threaded)
+
+ 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)
+
+ 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.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.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]
+
+ iq.reply()
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['actions'] = []
+ iq['command']['status'] = 'completed'
+ iq['command']['notes'] = session['notes']
+ 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.
+ 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.
+ 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.
+ block -- Specify if the send call will block until a
+ response is received, or a timeout occurs.
+ Defaults to True.
+ 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:
+ if kwargs.get('block', True):
+ try:
+ result = iq.send(**kwargs)
+ except IqError as err:
+ result = err.iq
+ self._handle_command_result(result)
+ else:
+ iq.send(block=False, callback=self._handle_command_result)
+
+ def start_command(self, jid, node, session, ifrom=None, block=False):
+ """
+ Initiate executing a command provided by a remote agent.
+
+ The default workflow provided is non-blocking, but a blocking
+ version may be used with block=True.
+
+ 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.
+ block -- If True, block execution until a result
+ is received. Defaults to False.
+ """
+ session['jid'] = jid
+ session['node'] = node
+ session['timestamp'] = time.time()
+ session['block'] = block
+ 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
+ if session['block']:
+ try:
+ result = iq.send(block=True)
+ except IqError as err:
+ result = err.iq
+ self._handle_command_result(result)
+ else:
+ iq.send(block=False, 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,
+ block=session['block'])
+
+ 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,
+ block=session['block'])
+
+ 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,
+ block=session['block'])
+
+ 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..cdd6215e
--- /dev/null
+++ b/slixmpp/plugins/xep_0054/stanza.py
@@ -0,0 +1,561 @@
+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)
+ 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
+ return xep_0082.parse(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
+ return xep_0082.parse(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()
+ time = xep_0082.parse('00:00:00%s' % self.xml.text)
+ return time.tzinfo
+
+
+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..9709c998
--- /dev/null
+++ b/slixmpp/plugins/xep_0054/vcard_temp.py
@@ -0,0 +1,146 @@
+"""
+ 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
+
+
+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()
+
+ def get_vcard(self, jid=None, ifrom=None, local=None, cached=False,
+ block=True, callback=None, timeout=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')
+
+ vcard = iq.send(block=block, callback=callback, timeout=timeout)
+
+ if block:
+ self.api['set_vcard'](vcard['from'], args=vcard['vcard_temp'])
+ return vcard
+
+ def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None,
+ callback=None, timeout=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(block=block, callback=callback, timeout=timeout)
+
+ 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.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..223b014f
--- /dev/null
+++ b/slixmpp/plugins/xep_0060/pubsub.py
@@ -0,0 +1,577 @@
+"""
+ 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,
+ block=True, 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def subscribe(self, jid, node, bare=True, subscribee=None, options=None,
+ ifrom=None, block=True, 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None,
+ ifrom=None, block=True, 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 subscribe to.
+ 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 subscribing to the node.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def get_subscriptions(self, jid, node=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['subscriptions']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_affiliations(self, jid, node=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['affiliations']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_subscription_options(self, jid, node=None, user_jid=None,
+ ifrom=None, block=True, 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(block=block, callback=callback, timeout=timeout)
+
+ def set_subscription_options(self, jid, node, user_jid, options,
+ ifrom=None, block=True, 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(block=block, callback=callback, timeout=timeout)
+
+ def get_node_config(self, jid, node=None, ifrom=None, block=True,
+ 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def get_node_subscriptions(self, jid, node, ifrom=None, block=True,
+ 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def get_node_affiliations(self, jid, node, ifrom=None, block=True,
+ 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def delete_node(self, jid, node, ifrom=None, block=True,
+ 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def set_node_config(self, jid, node, config, ifrom=None, block=True,
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def publish(self, jid, node, id=None, payload=None, options=None,
+ ifrom=None, block=True, 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def retract(self, jid, node, id, notify=None, ifrom=None, block=True,
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def purge(self, jid, node, ifrom=None, block=True, 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(block=block, callback=callback, timeout=timeout)
+
+ 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, block=True,
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def get_items(self, jid, node, item_ids=None, max_items=None,
+ iterator=False, ifrom=None, block=False,
+ 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(block=block, callback=callback, timeout=timeout)
+
+ def get_item_ids(self, jid, node, ifrom=None, block=True,
+ callback=None, timeout=None, iterator=False):
+ """
+ Retrieve the ItemIDs hosted by a given node, using disco.
+ """
+ return self.xmpp['xep_0030'].get_items(jid, node,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout,
+ iterator=iterator)
+
+ def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
+ block=True, 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(block=block, callback=callback, timeout=timeout)
+
+ def modify_subscriptions(self, jid, node, subscriptions=None, ifrom=None,
+ block=True, 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(block=block, callback=callback, timeout=timeout)
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..2bfe007c
--- /dev/null
+++ b/slixmpp/plugins/xep_0065/__init__.py
@@ -0,0 +1,7 @@
+from slixmpp.plugins.base import register_plugin
+
+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..654d9dce
--- /dev/null
+++ b/slixmpp/plugins/xep_0065/proxy.py
@@ -0,0 +1,292 @@
+import logging
+import threading
+import socket
+
+from hashlib import sha1
+from uuid import uuid4
+
+from slixmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5
+
+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 base_plugin
+
+from slixmpp.plugins.xep_0065 import stanza, Socks5
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0065(base_plugin):
+
+ name = 'xep_0065'
+ description = "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._sessions_lock = threading.Lock()
+
+ self._preauthed_sids_lock = threading.Lock()
+ 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 = self.discover_proxies()
+
+ if sid is None:
+ sid = uuid4().hex
+
+ used = 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
+
+ with self._sessions_lock:
+ self._sessions[sid] = self._connect_proxy(
+ sid,
+ self.xmpp.boundjid,
+ to,
+ self._proxies[proxy][0],
+ self._proxies[proxy][1],
+ peer=to)
+
+ # Request that the proxy activate the session with the target.
+ self.activate(proxy, sid, to, timeout=timeout)
+ socket = self.get_socket(sid)
+ self.xmpp.event('stream:%s:%s' % (sid, to), socket)
+ return socket
+
+ def request_stream(self, to, sid=None, ifrom=None, block=True, 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(block=block, 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 = self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
+
+ for item in disco_items['disco_items']['items']:
+ try:
+ disco_info = self.xmpp['xep_0030'].get_info(item[0], timeout=timeout)
+ 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 = 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, block=True, 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(block=block, timeout=timeout, callback=callback)
+
+ 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']
+ conn = None
+ used_streamhost = None
+
+ sender = iq['from']
+ for streamhost in streamhosts:
+ try:
+ conn = self._connect_proxy(sid,
+ sender,
+ self.xmpp.boundjid,
+ streamhost['host'],
+ streamhost['port'],
+ peer=sender)
+ used_streamhost = streamhost['jid']
+ break
+ except socket.error:
+ continue
+ else:
+ raise XMPPError(etype='cancel', condition='item-not-found')
+
+ iq.reply()
+ with self._sessions_lock:
+ 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, conn.peer_jid), conn)
+
+ def activate(self, proxy, sid, target, ifrom=None, block=True, 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
+ iq.send(block=block, 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
+ with self._sessions_lock:
+ 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()
+ with self._sessions_lock:
+ self._sessions = {}
+
+ def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None):
+ """ Establishes a connection between the client and the server-side
+ Socks5 proxy.
+
+ sid : The StreamID. <str>
+ requester : The JID of the requester. <str>
+ target : The JID of the target. <str>
+ proxy_host : The hostname or the IP of the proxy. <str>
+ proxy_port : The port of the proxy. <str> or <int>
+ peer : The JID for the other side of the stream, regardless
+ of target or requester status.
+ """
+ # Because the xep_0065 plugin uses the proxy_port as string,
+ # the Proxy class accepts the proxy_port argument as a string
+ # or an integer. Here, we force to use the port as an integer.
+ proxy_port = int(proxy_port)
+
+ sock = socksocket()
+ sock.setproxy(PROXY_TYPE_SOCKS5, proxy, port=proxy_port)
+
+ # The hostname MUST be SHA1(SID + Requester JID + Target JID)
+ # where the output is hexadecimal-encoded (not binary).
+ digest = sha1()
+ digest.update(sid)
+ digest.update(str(requester))
+ digest.update(str(target))
+
+ dest = digest.hexdigest()
+
+ # The port MUST be 0.
+ sock.connect((dest, 0))
+ log.info('Socket connected.')
+
+ _close = sock.close
+ def close(*args, **kwargs):
+ with self._sessions_lock:
+ if sid in self._sessions:
+ del self._sessions[sid]
+ _close()
+ log.info('Socket closed.')
+ sock.close = close
+
+ sock.peer_jid = peer
+ sock.self_jid = target if requester == peer else requester
+
+ self.xmpp.event('socks_connected', sid)
+ return sock
+
+ 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):
+ with self._preauthed_sids_lock:
+ 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)
+ with self._preauthed_sids_lock:
+ self._preauthed_sids[(jid, sid, ifrom)] = True
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..d997cb3a
--- /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 slixmpp.thirdparty 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..d83ff1a7
--- /dev/null
+++ b/slixmpp/plugins/xep_0077/register.py
@@ -0,0 +1,115 @@
+"""
+ 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, direct=True)
+ return True
+ return False
+
+ def get_registration(self, jid=None, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq.enable('register')
+ return iq.send(block=block, timeout=timeout,
+ callback=callback, now=True)
+
+ def cancel_registration(self, jid=None, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['register']['remove'] = True
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def change_password(self, password, jid=None, ifrom=None, block=True,
+ 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(block=block, 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..d3826e59
--- /dev/null
+++ b/slixmpp/plugins/xep_0078/legacyauth.py
@@ -0,0 +1,147 @@
+"""
+ 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
+import random
+import sys
+
+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(now=True)
+ except IqError as err:
+ log.info("Authentication failed: %s", err.iq['error']['condition'])
+ self.xmpp.event('failed_auth', direct=True)
+ self.xmpp.disconnect()
+ return True
+ except IqTimeout:
+ log.info("Authentication failed: %s", 'timeout')
+ self.xmpp.event('failed_auth', direct=True)
+ 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')
+ if sys.version_info < (3, 0):
+ stream_id = bytes(self.xmpp.stream_id)
+ password = bytes(self.xmpp.password)
+ else:
+ 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(now=True)
+ except IqError as err:
+ log.info("Authentication failed")
+ self.xmpp.event("failed_auth", direct=True)
+ self.xmpp.disconnect()
+ except IqTimeout:
+ log.info("Authentication failed")
+ self.xmpp.event("failed_auth", direct=True)
+ self.xmpp.disconnect()
+
+ self.xmpp.features.add('auth')
+
+ self.xmpp.authenticated = True
+
+ self.xmpp.boundjid = JID(self.xmpp.requested_jid,
+ resource=resource,
+ cache_lock=True)
+ self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
+
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.session_started_event.set()
+ 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..320bb958
--- /dev/null
+++ b/slixmpp/plugins/xep_0080/geoloc.py
@@ -0,0 +1,125 @@
+"""
+ 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.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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)
+ block = kwargs.get('block', None)
+ callback = kwargs.get('callback', None)
+ timeout = kwargs.get('timeout', None)
+ for param in ('ifrom', 'block', 'callback', 'timeout', 'options'):
+ if param in kwargs:
+ del kwargs[param]
+
+ geoloc = Geoloc()
+ geoloc.values = kwargs
+
+ return self.xmpp['xep_0163'].publish(geoloc,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user location information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ 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,
+ block=block,
+ callback=callback,
+ timeout=timeout)
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 <geol