Compare commits
441 Commits
sleek-0.9R
...
1.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d9e8df16 | ||
|
|
58aa944a5e | ||
|
|
dd41a85efc | ||
|
|
e2d18170b0 | ||
|
|
e219c0f976 | ||
|
|
4266ee0fa4 | ||
|
|
3a62908703 | ||
|
|
1469323350 | ||
|
|
a81162edd2 | ||
|
|
8080b4cae2 | ||
|
|
1735c194cd | ||
|
|
6997b2fbf8 | ||
|
|
b81ab97900 | ||
|
|
384e1a92b7 | ||
|
|
ec9aed5b75 | ||
|
|
7152d93dd0 | ||
|
|
4bb226147a | ||
|
|
6b274a2543 | ||
|
|
6a07e7cbe3 | ||
|
|
9f1648328f | ||
|
|
8e9b3d0760 | ||
|
|
5399fdd3a9 | ||
|
|
016aac69f6 | ||
|
|
1d891858b6 | ||
|
|
f02b0564e0 | ||
|
|
2e1befc8c6 | ||
|
|
87ccd804ff | ||
|
|
d7ba7cc72a | ||
|
|
d94811d81d | ||
|
|
6d45971411 | ||
|
|
84e2589f22 | ||
|
|
a3d111be12 | ||
|
|
4916a12b6f | ||
|
|
d6f2e51b05 | ||
|
|
feb7f892ea | ||
|
|
a420771665 | ||
|
|
f2449009d1 | ||
|
|
833f95b53a | ||
|
|
4b1fadde4b | ||
|
|
86a6b40fd8 | ||
|
|
7ef6abb2a3 | ||
|
|
dbf6780345 | ||
|
|
450c313340 | ||
|
|
996ca52471 | ||
|
|
6244857746 | ||
|
|
5635265203 | ||
|
|
45ccb31356 | ||
|
|
1a81b2f464 | ||
|
|
77251452c1 | ||
|
|
4df3aa569b | ||
|
|
2e2e16e281 | ||
|
|
d709f8db65 | ||
|
|
75584d7ad7 | ||
|
|
e0f9025e7c | ||
|
|
9004e8bbf2 | ||
|
|
8b5511c7ec | ||
|
|
34f6195ca5 | ||
|
|
70af52d74c | ||
|
|
ca2b4a188a | ||
|
|
0d32638379 | ||
|
|
c4b1212c44 | ||
|
|
3463bf46c6 | ||
|
|
13a01beb07 | ||
|
|
145f577bde | ||
|
|
30da68f47b | ||
|
|
72ead3d598 | ||
|
|
4b71fba64c | ||
|
|
1ed06bebcd | ||
|
|
aa1996eba6 | ||
|
|
683f717cf7 | ||
|
|
8dbe6f6546 | ||
|
|
5313338c3a | ||
|
|
cd800d636a | ||
|
|
40642b2cd1 | ||
|
|
35ef8f9090 | ||
|
|
38dc35840e | ||
|
|
b4004cd4d6 | ||
|
|
0c8a8314b2 | ||
|
|
4e757c2b56 | ||
|
|
c3be6ea0b2 | ||
|
|
da332365d4 | ||
|
|
f7e7bf601e | ||
|
|
6f4c2f22f3 | ||
|
|
493df57035 | ||
|
|
897a9ac333 | ||
|
|
acc2d071ac | ||
|
|
d3b1f8c476 | ||
|
|
f1db2fc156 | ||
|
|
2004ddd678 | ||
|
|
cb85d4a529 | ||
|
|
ead3af3135 | ||
|
|
a2891d7608 | ||
|
|
d7dea0c6cc | ||
|
|
632827f213 | ||
|
|
b71550cec7 | ||
|
|
b68e7bed40 | ||
|
|
4be6482ff3 | ||
|
|
a21178007f | ||
|
|
2e6c27f665 | ||
|
|
0a3a7b5a70 | ||
|
|
3a12cdbd13 | ||
|
|
7d93d1824b | ||
|
|
ba0d699d83 | ||
|
|
c6ac40c476 | ||
|
|
fe3f8dde4b | ||
|
|
acdf9e2d22 | ||
|
|
2076d506b4 | ||
|
|
68ce47c905 | ||
|
|
7c7fa0f008 | ||
|
|
a8e3657487 | ||
|
|
13a2f719f4 | ||
|
|
2908751020 | ||
|
|
8b29431cde | ||
|
|
4b145958fa | ||
|
|
e08b0054b2 | ||
|
|
596e135a03 | ||
|
|
e55e213c78 | ||
|
|
8749f5e09b | ||
|
|
b3353183f3 | ||
|
|
f97f6e5985 | ||
|
|
34c374a1e1 | ||
|
|
506eccf84d | ||
|
|
982bf3b2ec | ||
|
|
53a5026301 | ||
|
|
0aee445e69 | ||
|
|
cbc42c29fb | ||
|
|
874c51d74d | ||
|
|
f9ac95ddb7 | ||
|
|
0ea014fe41 | ||
|
|
62b190d0ff | ||
|
|
4b57b8131f | ||
|
|
988a90a176 | ||
|
|
67775fb8bd | ||
|
|
e81683beee | ||
|
|
d9c25ee65c | ||
|
|
1ebc7f4d4b | ||
|
|
2c5b77ae2e | ||
|
|
d8aae88526 | ||
|
|
2f4bdfee1b | ||
|
|
f4451fe6b7 | ||
|
|
8d4e77aba6 | ||
|
|
f474d378ef | ||
|
|
defc252c7d | ||
|
|
19bd1e0485 | ||
|
|
5f2fc67c40 | ||
|
|
8ead33fc3b | ||
|
|
ab25301953 | ||
|
|
291b118aca | ||
|
|
db7fb10e95 | ||
|
|
60d3afe6b6 | ||
|
|
afeb8f3f7c | ||
|
|
cdbc0570ca | ||
|
|
e648f08bad | ||
|
|
7ba6d5e02d | ||
|
|
ea48bb5ac5 | ||
|
|
6ee8a2980c | ||
|
|
b8114b25ed | ||
|
|
45991e47ee | ||
|
|
b8f40eb843 | ||
|
|
b73a859031 | ||
|
|
9dbf246f0b | ||
|
|
4fb77ac878 | ||
|
|
d0c506f930 | ||
|
|
7351fe1a02 | ||
|
|
38c2f51f83 | ||
|
|
1bf34caa5b | ||
|
|
5769935720 | ||
|
|
0214db7545 | ||
|
|
ffc6f031d9 | ||
|
|
9e248bb852 | ||
|
|
973890e2c9 | ||
|
|
9c08e56ed0 | ||
|
|
b888610525 | ||
|
|
6d68706326 | ||
|
|
5bdcd9ef9d | ||
|
|
2eff35cc7a | ||
|
|
ac330b5c6c | ||
|
|
46ffa8e9fe | ||
|
|
03847497cc | ||
|
|
185d7cf28e | ||
|
|
8aa3d0c047 | ||
|
|
9e3d506651 | ||
|
|
2f3ff37a24 | ||
|
|
1f09d60a52 | ||
|
|
d528884723 | ||
|
|
d9aff3d36f | ||
|
|
04cc48775d | ||
|
|
27ebb6e8f6 | ||
|
|
8f55704928 | ||
|
|
d88999691c | ||
|
|
c4699b92e6 | ||
|
|
ce69213a1e | ||
|
|
77eab6544f | ||
|
|
11264fe0a8 | ||
|
|
11a6e6d2e0 | ||
|
|
6e34b2cfdd | ||
|
|
e18354ae0e | ||
|
|
4375ac7d8b | ||
|
|
faec86b3be | ||
|
|
505a63da3a | ||
|
|
93fbcad277 | ||
|
|
3625573c7d | ||
|
|
d9e7f555e6 | ||
|
|
2755d732a4 | ||
|
|
2d18d905a5 | ||
|
|
4eb4d729ee | ||
|
|
8b5c1010de | ||
|
|
95ad8a1878 | ||
|
|
aeb7999e6a | ||
|
|
8468332381 | ||
|
|
dc001bb201 | ||
|
|
0d0b963fe5 | ||
|
|
a41a4369c6 | ||
|
|
7ad7a29a8f | ||
|
|
b0e036d03c | ||
|
|
a8b948cd33 | ||
|
|
e02ffe8547 | ||
|
|
42bfca1c87 | ||
|
|
0fffbb8200 | ||
|
|
21c32c6e1c | ||
|
|
75a051556f | ||
|
|
78141fe5f3 | ||
|
|
88d21d210c | ||
|
|
799645f13f | ||
|
|
f234dc02cf | ||
|
|
c294c1a85c | ||
|
|
cbe76c8a70 | ||
|
|
77b8f0f4bb | ||
|
|
259f91d2bd | ||
|
|
ed366b338d | ||
|
|
9e2cada19e | ||
|
|
d0ccbf6b7a | ||
|
|
e1866ab328 | ||
|
|
3ffa09ba7c | ||
|
|
a7410f2146 | ||
|
|
178608f4c0 | ||
|
|
19ee6979a5 | ||
|
|
9f0baec7b2 | ||
|
|
433c147627 | ||
|
|
9a34c9a9a1 | ||
|
|
2662131124 | ||
|
|
bb219595a7 | ||
|
|
fcdd57ce54 | ||
|
|
5522443e0e | ||
|
|
55cfe69fef | ||
|
|
6de87a1cbf | ||
|
|
7c10ff16fb | ||
|
|
c258d2f19d | ||
|
|
d576e32f7a | ||
|
|
4a2e7c5393 | ||
|
|
0b4320a196 | ||
|
|
9bef4b4d4d | ||
|
|
5c3066ba30 | ||
|
|
576eefb097 | ||
|
|
aebd115ba2 | ||
|
|
6dfea828be | ||
|
|
3749c1b88c | ||
|
|
998741b87e | ||
|
|
9c62bce206 | ||
|
|
f5ae27da4f | ||
|
|
89fb15e896 | ||
|
|
906aa0bd68 | ||
|
|
bb6f4af8e2 | ||
|
|
6677df39f2 | ||
|
|
a2c515bc97 | ||
|
|
ca6ce26b0d | ||
|
|
37ff17b0cb | ||
|
|
00d7952001 | ||
|
|
56766508b3 | ||
|
|
5c59f5baca | ||
|
|
e16b37d2be | ||
|
|
d68bc2ba07 | ||
|
|
10298a6eab | ||
|
|
a3580dcef9 | ||
|
|
1eaa9cb28c | ||
|
|
5d458bf6c2 | ||
|
|
2fa58a74ab | ||
|
|
c8f406d1b3 | ||
|
|
203986dd7c | ||
|
|
f4ecf0bac4 | ||
|
|
345656926e | ||
|
|
c05ddcb7f5 | ||
|
|
eb9e72fe3e | ||
|
|
8a0616b3e0 | ||
|
|
b71cfe0492 | ||
|
|
fac3bca1f6 | ||
|
|
d150b35464 | ||
|
|
21b7109c06 | ||
|
|
e4240dd593 | ||
|
|
2f6f4fc16d | ||
|
|
fe49b8c377 | ||
|
|
b580a3138d | ||
|
|
c20fab0f6c | ||
|
|
c721fb4126 | ||
|
|
415520200e | ||
|
|
747001d33c | ||
|
|
b0fb205c16 | ||
|
|
4b52007e8c | ||
|
|
5da7bd1866 | ||
|
|
22134c302b | ||
|
|
b40a489796 | ||
|
|
7a5ef28492 | ||
|
|
c09e9c702c | ||
|
|
48ba7292bc | ||
|
|
4d1f071f83 | ||
|
|
0d0c044a68 | ||
|
|
3c0dfb56e6 | ||
|
|
e077204a16 | ||
|
|
58f77d898f | ||
|
|
c54466596f | ||
|
|
aa1dbe97e0 | ||
|
|
fec69be731 | ||
|
|
956fdf6970 | ||
|
|
183a3f1b87 | ||
|
|
18683d2b75 | ||
|
|
41ab2b8460 | ||
|
|
939ae298c2 | ||
|
|
851e90c572 | ||
|
|
ecde696468 | ||
|
|
1cedea2804 | ||
|
|
cbed8029ba | ||
|
|
1da3e5b35e | ||
|
|
a96a046e27 | ||
|
|
60a183b011 | ||
|
|
a49f511a2f | ||
|
|
25f43bd219 | ||
|
|
d148f633f3 | ||
|
|
e8e934fa95 | ||
|
|
bd92ef6acf | ||
|
|
aa02ecd154 | ||
|
|
aad185fe29 | ||
|
|
2b6454786a | ||
|
|
a349a2a317 | ||
|
|
2cb82afc2c | ||
|
|
c8989c04f3 | ||
|
|
241aba8c76 | ||
|
|
ec860bf9e2 | ||
|
|
73a3d07ad9 | ||
|
|
07208a3eaf | ||
|
|
d0a5c539d8 | ||
|
|
d70a6e6f32 | ||
|
|
66e92c6c9f | ||
|
|
ca2c421e6c | ||
|
|
9fcd2e93a3 | ||
|
|
75afefb5c6 | ||
|
|
b67b930596 | ||
|
|
5c9b47afbd | ||
|
|
7ad0143687 | ||
|
|
de24e9ed45 | ||
|
|
9724efa123 | ||
|
|
690eaf8d3c | ||
|
|
f505e229d6 | ||
|
|
9ca4bba2de | ||
|
|
bb927c7e6a | ||
|
|
14f1c3ba51 | ||
|
|
278a8bb443 | ||
|
|
85ee30539d | ||
|
|
f74baf1c23 | ||
|
|
b5a14a0190 | ||
|
|
fec8578cf6 | ||
|
|
f80b3285d4 | ||
|
|
130a148d34 | ||
|
|
16104b6e56 | ||
|
|
d5e42ac0e7 | ||
|
|
e6bec8681e | ||
|
|
797e92a6a3 | ||
|
|
1ef112966b | ||
|
|
078c71ed3f | ||
|
|
bae082f437 | ||
|
|
35212c7991 | ||
|
|
48f0843ace | ||
|
|
b1c997be1d | ||
|
|
d0cb400c54 | ||
|
|
7f8179d91e | ||
|
|
37ada49802 | ||
|
|
5c76d969f7 | ||
|
|
059cc9ccc4 | ||
|
|
309c9e74eb | ||
|
|
6041cd1952 | ||
|
|
acb53ba371 | ||
|
|
646a609c0b | ||
|
|
8bb0f5e34c | ||
|
|
3c939313d2 | ||
|
|
9962f1a664 | ||
|
|
253de8518c | ||
|
|
a38735cb2a | ||
|
|
e700a54d11 | ||
|
|
6469cdb4ca | ||
|
|
18e27d65ce | ||
|
|
0c39567f20 | ||
|
|
f5491c901f | ||
|
|
f5cae85af5 | ||
|
|
01e8040a07 | ||
|
|
aa916c9ac8 | ||
|
|
332eea3b3b | ||
|
|
109af1b1b6 | ||
|
|
629f6e76a9 | ||
|
|
82a3918aa4 | ||
|
|
cff3079a04 | ||
|
|
4f864a07f5 | ||
|
|
938066bd50 | ||
|
|
9fee87c258 | ||
|
|
fd573880eb | ||
|
|
2f1ba368e2 | ||
|
|
bde1818400 | ||
|
|
3a28f9e5d2 | ||
|
|
0bda5fd3f2 | ||
|
|
1e3a6e1b5f | ||
|
|
fa92bc866b | ||
|
|
f4bc9d9722 | ||
|
|
9cfe19c1e1 | ||
|
|
f18c790824 | ||
|
|
f165b4b52b | ||
|
|
7ebc006516 | ||
|
|
5ca4ede5ac | ||
|
|
35f4ef3452 | ||
|
|
828cba875f | ||
|
|
3920ee3941 | ||
|
|
feaa7539af | ||
|
|
c004f042f9 | ||
|
|
ae41c08fec | ||
|
|
223507f36f | ||
|
|
8515cef117 | ||
|
|
9d76e7353a | ||
|
|
2f8c1954f0 | ||
|
|
d40e49397b | ||
|
|
c4fa3ab654 | ||
|
|
e2f841146a | ||
|
|
602a6d8491 | ||
|
|
37b571c55a | ||
|
|
2a30e3fe0c | ||
|
|
212660091f | ||
|
|
35c157f9d8 | ||
|
|
09aae38117 | ||
|
|
80e7e0d0ee | ||
|
|
2f9f649d98 | ||
|
|
fef511fd51 | ||
|
|
85c3d97d2a | ||
|
|
2384858f5e | ||
|
|
dd77d2165d | ||
|
|
ecd5a172ed |
10
INSTALL
10
INSTALL
@@ -1,8 +1,12 @@
|
||||
Pre-requisites:
|
||||
Python 3.1 or 2.6
|
||||
- Python 3.1 or 2.6
|
||||
|
||||
Install:
|
||||
python3 setup.py install
|
||||
> python3 setup.py install
|
||||
|
||||
Root install:
|
||||
sudo python3 setup.py install
|
||||
> sudo python3 setup.py install
|
||||
|
||||
To test:
|
||||
> cd examples
|
||||
> python echo_client.py -v -j [USER@example.com] -p [PASSWORD]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2010 ICRL
|
||||
Copyright (c) 2010 Nathanael C. Fritz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
39
MANIFEST
39
MANIFEST
@@ -1,39 +0,0 @@
|
||||
setup.py
|
||||
sleekxmpp/__init__.py
|
||||
sleekxmpp/basexmpp.py
|
||||
sleekxmpp/clientxmpp.py
|
||||
sleekxmpp/example.py
|
||||
sleekxmpp/plugins/__init__.py
|
||||
sleekxmpp/plugins/base.py
|
||||
sleekxmpp/plugins/gmail_notify.py
|
||||
sleekxmpp/plugins/xep_0004.py
|
||||
sleekxmpp/plugins/xep_0009.py
|
||||
sleekxmpp/plugins/xep_0030.py
|
||||
sleekxmpp/plugins/xep_0045.py
|
||||
sleekxmpp/plugins/xep_0050.py
|
||||
sleekxmpp/plugins/xep_0060.py
|
||||
sleekxmpp/plugins/xep_0078.py
|
||||
sleekxmpp/plugins/xep_0086.py
|
||||
sleekxmpp/plugins/xep_0092.py
|
||||
sleekxmpp/plugins/xep_0199.py
|
||||
sleekxmpp/stanza/__init__.py
|
||||
sleekxmpp/stanza/iq.py
|
||||
sleekxmpp/stanza/message.py
|
||||
sleekxmpp/stanza/presence.py
|
||||
sleekxmpp/xmlstream/__init__.py
|
||||
sleekxmpp/xmlstream/stanzabase.py
|
||||
sleekxmpp/xmlstream/statemachine.py
|
||||
sleekxmpp/xmlstream/test.py
|
||||
sleekxmpp/xmlstream/testclient.py
|
||||
sleekxmpp/xmlstream/xmlstream.py
|
||||
sleekxmpp/xmlstream/handler/__init__.py
|
||||
sleekxmpp/xmlstream/handler/base.py
|
||||
sleekxmpp/xmlstream/handler/callback.py
|
||||
sleekxmpp/xmlstream/handler/waiter.py
|
||||
sleekxmpp/xmlstream/handler/xmlcallback.py
|
||||
sleekxmpp/xmlstream/handler/xmlwaiter.py
|
||||
sleekxmpp/xmlstream/matcher/__init__.py
|
||||
sleekxmpp/xmlstream/matcher/base.py
|
||||
sleekxmpp/xmlstream/matcher/many.py
|
||||
sleekxmpp/xmlstream/matcher/xmlmask.py
|
||||
sleekxmpp/xmlstream/matcher/xpath.py
|
||||
14
README
14
README
@@ -1,5 +1,13 @@
|
||||
SleekXMPP is an XMPP library written for Python 3.x (with 2.6 compatibility).
|
||||
SleekXMPP is an XMPP library written for Python 3.1+ (with 2.6 compatibility).
|
||||
Hosted at http://wiki.github.com/fritzy/SleekXMPP/
|
||||
|
||||
Featured in examples in XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, and Peter Saint-Andre
|
||||
If you're coming here from The Definitive Guide, please read http://wiki.github.com/fritzy/SleekXMPP/xmpp-the-definitive-guide
|
||||
|
||||
Requirements:
|
||||
We try to keep requirements to a minimum, but we suggest that you install http://dnspython.org although it isn't strictly required.
|
||||
If you do not install this library, you may need to specify the server/port for services that use SRV records (like GTalk).
|
||||
"sudo pip install dnspython" on a *nix system with pip installed.
|
||||
|
||||
SleekXMPP has several design goals/philosophies:
|
||||
- Low number of dependencies.
|
||||
@@ -31,7 +39,9 @@ Since 0.2, here's the Changelog:
|
||||
Credits
|
||||
----------------
|
||||
Main Author: Nathan Fritz fritz@netflint.net
|
||||
XEP-0045 original implementation: Kevin Smith
|
||||
Contributors: Kevin Smith & Lance Stout
|
||||
Patches: Remko Tronçon
|
||||
|
||||
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
|
||||
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
|
||||
Join sleek@conference.jabber.org for groupchat discussion.
|
||||
|
||||
171
conn_tests/test_pubsubjobs.py
Normal file
171
conn_tests/test_pubsubjobs.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import logging
|
||||
import sleekxmpp
|
||||
from optparse import OptionParser
|
||||
from xml.etree import cElementTree as ET
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
import unittest
|
||||
import sleekxmpp.plugins.xep_0004
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from sleekxmpp.xmlstream.handler.waiter import Waiter
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
class TestClient(sleekxmpp.ClientXMPP):
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
self.add_event_handler("session_start", self.start)
|
||||
#self.add_event_handler("message", self.message)
|
||||
self.waitforstart = queue.Queue()
|
||||
|
||||
def start(self, event):
|
||||
self.getRoster()
|
||||
self.sendPresence()
|
||||
self.waitforstart.put(True)
|
||||
|
||||
|
||||
class TestPubsubServer(unittest.TestCase):
|
||||
statev = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test001getdefaultconfig(self):
|
||||
"""Get the default node config"""
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
|
||||
result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
|
||||
self.statev['defaultconfig'] = result
|
||||
self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
|
||||
|
||||
def test002createdefaultnode(self):
|
||||
"""Create a node without config"""
|
||||
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
|
||||
|
||||
def test003deletenode(self):
|
||||
"""Delete recently created node"""
|
||||
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
|
||||
|
||||
def test004createnode(self):
|
||||
"""Create a node with a config"""
|
||||
self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
|
||||
self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
|
||||
self.statev['defaultconfig'].field['pubsub#persist_items'].setValue(True)
|
||||
self.statev['defaultconfig'].field['pubsub#presence_based_delivery'].setValue(True)
|
||||
p = self.xmpp2.Presence()
|
||||
p['to'] = self.pshost
|
||||
p.send()
|
||||
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig'], ntype='job'))
|
||||
|
||||
def test005reconfigure(self):
|
||||
"""Retrieving node config and reconfiguring"""
|
||||
nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
|
||||
self.failUnless(nconfig, "No configuration returned")
|
||||
#print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
|
||||
self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
|
||||
self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
|
||||
|
||||
def test006subscribetonode(self):
|
||||
"""Subscribe to node from account 2"""
|
||||
self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
|
||||
|
||||
def test007publishitem(self):
|
||||
"""Publishing item"""
|
||||
item = ET.Element('{http://netflint.net/protocol/test}test')
|
||||
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
#result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
|
||||
result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test1', item)
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.failUnless(msg != False, "Account #2 did not get message event")
|
||||
#result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
|
||||
result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test2', item)
|
||||
w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test1')
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test2')
|
||||
self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test1')
|
||||
self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test2')
|
||||
print result
|
||||
#need to add check for update
|
||||
|
||||
def test900cleanup(self):
|
||||
"Cleaning up"
|
||||
#self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#parse command line arguements
|
||||
optp = OptionParser()
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
|
||||
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
|
||||
#load xml config
|
||||
logging.info("Loading config file: %s" % opts.configfile)
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(opts.configfile)
|
||||
|
||||
#init
|
||||
logging.info("Account 1 is %s" % config.get('account1', 'jid'))
|
||||
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
|
||||
logging.info("Account 2 is %s" % config.get('account2', 'jid'))
|
||||
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
|
||||
|
||||
xmpp1.registerPlugin('xep_0004')
|
||||
xmpp1.registerPlugin('xep_0030')
|
||||
xmpp1.registerPlugin('xep_0060')
|
||||
xmpp1.registerPlugin('xep_0199')
|
||||
xmpp1.registerPlugin('jobs')
|
||||
xmpp2.registerPlugin('xep_0004')
|
||||
xmpp2.registerPlugin('xep_0030')
|
||||
xmpp2.registerPlugin('xep_0060')
|
||||
xmpp2.registerPlugin('xep_0199')
|
||||
xmpp2.registerPlugin('jobs')
|
||||
|
||||
if not config.get('account1', 'server'):
|
||||
# we don't know the server, but the lib can probably figure it out
|
||||
xmpp1.connect()
|
||||
else:
|
||||
xmpp1.connect((config.get('account1', 'server'), 5222))
|
||||
xmpp1.process(threaded=True)
|
||||
|
||||
#init
|
||||
if not config.get('account2', 'server'):
|
||||
# we don't know the server, but the lib can probably figure it out
|
||||
xmpp2.connect()
|
||||
else:
|
||||
xmpp2.connect((config.get('account2', 'server'), 5222))
|
||||
xmpp2.process(threaded=True)
|
||||
|
||||
TestPubsubServer.xmpp1 = xmpp1
|
||||
TestPubsubServer.xmpp2 = xmpp2
|
||||
TestPubsubServer.pshost = config.get('settings', 'pubsub')
|
||||
xmpp1.waitforstart.get(True)
|
||||
xmpp2.waitforstart.get(True)
|
||||
testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
|
||||
|
||||
alltests_suite = unittest.TestSuite([testsuite])
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
|
||||
xmpp1.disconnect()
|
||||
xmpp2.disconnect()
|
||||
233
conn_tests/test_pubsubserver.py
Normal file
233
conn_tests/test_pubsubserver.py
Normal file
@@ -0,0 +1,233 @@
|
||||
import logging
|
||||
import sleekxmpp
|
||||
from optparse import OptionParser
|
||||
from xml.etree import cElementTree as ET
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
import unittest
|
||||
import sleekxmpp.plugins.xep_0004
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from sleekxmpp.xmlstream.handler.waiter import Waiter
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
class TestClient(sleekxmpp.ClientXMPP):
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
self.add_event_handler("session_start", self.start)
|
||||
#self.add_event_handler("message", self.message)
|
||||
self.waitforstart = queue.Queue()
|
||||
|
||||
def start(self, event):
|
||||
self.getRoster()
|
||||
self.sendPresence()
|
||||
self.waitforstart.put(True)
|
||||
|
||||
|
||||
class TestPubsubServer(unittest.TestCase):
|
||||
statev = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test001getdefaultconfig(self):
|
||||
"""Get the default node config"""
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
|
||||
result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
|
||||
self.statev['defaultconfig'] = result
|
||||
self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
|
||||
|
||||
def test002createdefaultnode(self):
|
||||
"""Create a node without config"""
|
||||
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
|
||||
|
||||
def test003deletenode(self):
|
||||
"""Delete recently created node"""
|
||||
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
|
||||
|
||||
def test004createnode(self):
|
||||
"""Create a node with a config"""
|
||||
self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
|
||||
self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
|
||||
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig']))
|
||||
|
||||
def test005reconfigure(self):
|
||||
"""Retrieving node config and reconfiguring"""
|
||||
nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
|
||||
self.failUnless(nconfig, "No configuration returned")
|
||||
#print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
|
||||
self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
|
||||
self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
|
||||
|
||||
def test006subscribetonode(self):
|
||||
"""Subscribe to node from account 2"""
|
||||
self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
|
||||
|
||||
def test007publishitem(self):
|
||||
"""Publishing item"""
|
||||
item = ET.Element('{http://netflint.net/protocol/test}test')
|
||||
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.failUnless(msg != False, "Account #2 did not get message event")
|
||||
self.failUnless(result)
|
||||
#need to add check for update
|
||||
|
||||
def test008updateitem(self):
|
||||
"""Updating item"""
|
||||
item = ET.Element('{http://netflint.net/protocol/test}test', {'someattr': 'hi there'})
|
||||
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.failUnless(msg != False, "Account #2 did not get message event")
|
||||
self.failUnless(result)
|
||||
#need to add check for update
|
||||
|
||||
def test009deleteitem(self):
|
||||
"""Deleting item"""
|
||||
w = Waiter('wait retract', StanzaPath('message/pubsub_event/items@node=testnode2'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
result = self.xmpp1['xep_0060'].deleteItem(self.pshost, "testnode2", "test1")
|
||||
self.failUnless(result, "Got error when deleting item.")
|
||||
msg = w.wait(1)
|
||||
self.failUnless(msg != False, "Did not get retract notice.")
|
||||
|
||||
def test010unsubscribenode(self):
|
||||
"Unsubscribing Account #2"
|
||||
self.failUnless(self.xmpp2['xep_0060'].unsubscribe(self.pshost, "testnode2"), "Got error response when unsubscribing.")
|
||||
|
||||
def test011createcollectionnode(self):
|
||||
"Create a collection node w/ Account #2"
|
||||
self.failUnless(self.xmpp2['xep_0060'].create_node(self.pshost, "testnode3", self.statev['defaultconfig'], True), "Could not create collection node")
|
||||
|
||||
def test012subscribecollection(self):
|
||||
"Subscribe Account #1 to collection"
|
||||
self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode3"))
|
||||
|
||||
def test013assignnodetocollection(self):
|
||||
"Assign node to collection"
|
||||
self.failUnless(self.xmpp2['xep_0060'].addNodeToCollection(self.pshost, 'testnode2', 'testnode3'))
|
||||
|
||||
def test014publishcollection(self):
|
||||
"""Publishing item to collection child"""
|
||||
item = ET.Element('{http://netflint.net/protocol/test}test')
|
||||
w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items@node=testnode2'))
|
||||
self.xmpp1.registerHandler(w)
|
||||
result = self.xmpp2['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.failUnless(msg != False, "Account #1 did not get message event: perhaps node was advertised incorrectly?")
|
||||
self.failUnless(result)
|
||||
|
||||
# def test016speedtest(self):
|
||||
# "Uncached speed test"
|
||||
# import time
|
||||
# start = time.time()
|
||||
# for y in range(0, 50000, 1000):
|
||||
# start2 = time.time()
|
||||
# for x in range(y, y+1000):
|
||||
# self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode4", subscribee="testuser%s@whatever" % x))
|
||||
# print time.time() - start2
|
||||
# seconds = time.time() - start
|
||||
# print "--", seconds
|
||||
# print "---------"
|
||||
# time.sleep(15)
|
||||
# self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4'), "Could not delete non-cached test node")
|
||||
|
||||
# def test015speedtest(self):
|
||||
# "cached speed test"
|
||||
# result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
|
||||
# self.statev['defaultconfig'] = result
|
||||
# self.statev['defaultconfig'].field['pubsub#node_type'].setValue("leaf")
|
||||
# self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(True)
|
||||
# self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode4', self.statev['defaultconfig']))
|
||||
# self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(False)
|
||||
# self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode5', self.statev['defaultconfig']))
|
||||
# start = time.time()
|
||||
# for y in range(0, 50000, 1000):
|
||||
# start2 = time.time()
|
||||
# for x in range(y, y+1000):
|
||||
# self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode5", subscribee="testuser%s@whatever" % x))
|
||||
# print time.time() - start2
|
||||
# seconds = time.time() - start
|
||||
# print "--", seconds
|
||||
|
||||
def test900cleanup(self):
|
||||
"Cleaning up"
|
||||
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
|
||||
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3'), "Could not delete collection node")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#parse command line arguements
|
||||
optp = OptionParser()
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
|
||||
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
|
||||
#load xml config
|
||||
logging.info("Loading config file: %s" % opts.configfile)
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(opts.configfile)
|
||||
|
||||
#init
|
||||
logging.info("Account 1 is %s" % config.get('account1', 'jid'))
|
||||
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
|
||||
logging.info("Account 2 is %s" % config.get('account2', 'jid'))
|
||||
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
|
||||
|
||||
xmpp1.registerPlugin('xep_0004')
|
||||
xmpp1.registerPlugin('xep_0030')
|
||||
xmpp1.registerPlugin('xep_0060')
|
||||
xmpp1.registerPlugin('xep_0199')
|
||||
xmpp2.registerPlugin('xep_0004')
|
||||
xmpp2.registerPlugin('xep_0030')
|
||||
xmpp2.registerPlugin('xep_0060')
|
||||
xmpp2.registerPlugin('xep_0199')
|
||||
|
||||
if not config.get('account1', 'server'):
|
||||
# we don't know the server, but the lib can probably figure it out
|
||||
xmpp1.connect()
|
||||
else:
|
||||
xmpp1.connect((config.get('account1', 'server'), 5222))
|
||||
xmpp1.process(threaded=True)
|
||||
|
||||
#init
|
||||
if not config.get('account2', 'server'):
|
||||
# we don't know the server, but the lib can probably figure it out
|
||||
xmpp2.connect()
|
||||
else:
|
||||
xmpp2.connect((config.get('account2', 'server'), 5222))
|
||||
xmpp2.process(threaded=True)
|
||||
|
||||
TestPubsubServer.xmpp1 = xmpp1
|
||||
TestPubsubServer.xmpp2 = xmpp2
|
||||
TestPubsubServer.pshost = config.get('settings', 'pubsub')
|
||||
xmpp1.waitforstart.get(True)
|
||||
xmpp2.waitforstart.get(True)
|
||||
testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
|
||||
|
||||
alltests_suite = unittest.TestSuite([testsuite])
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
|
||||
xmpp1.disconnect()
|
||||
xmpp2.disconnect()
|
||||
13
conn_tests/testconfig.ini
Normal file
13
conn_tests/testconfig.ini
Normal file
@@ -0,0 +1,13 @@
|
||||
[settings]
|
||||
enabled=true
|
||||
pubsub=pubsub.recon
|
||||
|
||||
[account1]
|
||||
jid=fritzy@recon
|
||||
pass=testing123
|
||||
server=
|
||||
|
||||
[account2]
|
||||
jid=fritzy2@recon
|
||||
pass=testing123
|
||||
server=
|
||||
@@ -1,19 +1,9 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -34,16 +24,16 @@ class testps(sleekxmpp.ClientXMPP):
|
||||
self.registerPlugin('xep_0030')
|
||||
self.registerPlugin('xep_0060')
|
||||
self.registerPlugin('xep_0092')
|
||||
self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, threaded=True)
|
||||
self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, name='Pubsub Event', threaded=True)
|
||||
self.add_event_handler("session_start", self.start, threaded=True)
|
||||
self.add_handler("<iq type='error' />", self.handleError)
|
||||
self.add_handler("<iq type='error' />", self.handleError, name='Iq Error')
|
||||
self.events = Queue.Queue()
|
||||
self.default_config = None
|
||||
self.ps = self.plugin['xep_0060']
|
||||
self.node = "pstestnode_%s"
|
||||
self.pshost = pshost
|
||||
if pshost is None:
|
||||
self.pshost = self.server
|
||||
self.pshost = self.boundjid.host
|
||||
self.nodenum = int(nodenum)
|
||||
self.leafnode = self.nodenum + 1
|
||||
self.collectnode = self.nodenum + 2
|
||||
48
example.py
48
example.py
@@ -1,48 +0,0 @@
|
||||
# coding=utf8
|
||||
|
||||
import sleekxmpp
|
||||
import logging
|
||||
from optparse import OptionParser
|
||||
import time
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3,0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class Example(sleekxmpp.ClientXMPP):
|
||||
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
self.add_event_handler("session_start", self.start)
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def start(self, event):
|
||||
self.getRoster()
|
||||
self.sendPresence()
|
||||
|
||||
def message(self, msg):
|
||||
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||
|
||||
if __name__ == '__main__':
|
||||
#parse command line arguements
|
||||
optp = OptionParser()
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
xmpp = Example('user@gmail.com/sleekxmpp', 'password')
|
||||
xmpp.registerPlugin('xep_0004')
|
||||
xmpp.registerPlugin('xep_0030')
|
||||
xmpp.registerPlugin('xep_0060')
|
||||
xmpp.registerPlugin('xep_0199')
|
||||
if xmpp.connect(('talk.google.com', 5222)):
|
||||
xmpp.process(threaded=False)
|
||||
print("done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
199
examples/adhoc_provider.py
Executable file
199
examples/adhoc_provider.py
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class CommandBot(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP bot that provides a basic
|
||||
adhoc command.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use. We want to
|
||||
# listen for this event so that we we can intialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
# We add the command after session_start has fired
|
||||
# to ensure that the correct full JID is used.
|
||||
|
||||
# If using a component, may also pass jid keyword parameter.
|
||||
|
||||
self['xep_0050'].add_command(node='greeting',
|
||||
name='Greeting',
|
||||
handler=self._handle_command)
|
||||
|
||||
def _handle_command(self, iq, session):
|
||||
"""
|
||||
Respond to the intial request for a command.
|
||||
|
||||
Arguments:
|
||||
iq -- The iq stanza containing the command request.
|
||||
session -- A dictionary of data relevant to the command
|
||||
session. Additional, custom data may be saved
|
||||
here to persist across handler callbacks.
|
||||
"""
|
||||
form = self['xep_0004'].makeForm('form', 'Greeting')
|
||||
form.addField(var='greeting',
|
||||
ftype='text-single',
|
||||
label='Your greeting')
|
||||
|
||||
session['payload'] = form
|
||||
session['next'] = self._handle_command_complete
|
||||
session['has_next'] = False
|
||||
|
||||
# Other useful session values:
|
||||
# session['to'] -- The JID that received the
|
||||
# command request.
|
||||
# session['from'] -- The JID that sent the
|
||||
# command request.
|
||||
# session['has_next'] = True -- There are more steps to complete
|
||||
# session['allow_complete'] = True -- Allow user to finish immediately
|
||||
# and possibly skip steps
|
||||
# session['cancel'] = handler -- Assign a handler for if the user
|
||||
# cancels the command.
|
||||
# session['notes'] = [ -- Add informative notes about the
|
||||
# ('info', 'Info message'), command's results.
|
||||
# ('warning', 'Warning message'),
|
||||
# ('error', 'Error message')]
|
||||
|
||||
return session
|
||||
|
||||
def _handle_command_complete(self, payload, session):
|
||||
"""
|
||||
Process a command result from the user.
|
||||
|
||||
Arguments:
|
||||
payload -- Either a single item, such as a form, or a list
|
||||
of items or forms if more than one form was
|
||||
provided to the user. The payload may be any
|
||||
stanza, such as jabber:x:oob for out of band
|
||||
data, or jabber:x:data for typical data forms.
|
||||
session -- A dictionary of data relevant to the command
|
||||
session. Additional, custom data may be saved
|
||||
here to persist across handler callbacks.
|
||||
"""
|
||||
|
||||
# In this case (as is typical), the payload is a form
|
||||
form = payload
|
||||
|
||||
greeting = form['values']['greeting']
|
||||
self.send_message(mto=session['from'],
|
||||
mbody="%s, World!" % greeting)
|
||||
|
||||
# Having no return statement is the same as unsetting the 'payload'
|
||||
# and 'next' session values and returning the session.
|
||||
|
||||
# Unless it is the final step, always return the session dictionary.
|
||||
|
||||
session['payload'] = None
|
||||
session['next'] = None
|
||||
|
||||
return session
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
|
||||
# Setup the CommandBot and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = CommandBot(opts.jid, opts.password)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0004') # Data Forms
|
||||
xmpp.register_plugin('xep_0050') # Adhoc Commands
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the pydns library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
208
examples/adhoc_user.py
Executable file
208
examples/adhoc_user.py
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class CommandUserBot(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP bot that uses the adhoc command
|
||||
provided by the adhoc_provider.py example.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, other, greeting):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.command_provider = other
|
||||
self.greeting = greeting
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use. We want to
|
||||
# listen for this event so that we we can intialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
# We first create a session dictionary containing:
|
||||
# 'next' -- the handler to execute on a successful response
|
||||
# 'error' -- the handler to execute if an error occurs
|
||||
|
||||
# The session may also contain custom data.
|
||||
|
||||
session = {'greeting': self.greeting,
|
||||
'next': self._command_start,
|
||||
'error': self._command_error}
|
||||
|
||||
self['xep_0050'].start_command(jid=self.command_provider,
|
||||
node='greeting',
|
||||
session=session)
|
||||
|
||||
def message(self, msg):
|
||||
"""
|
||||
Process incoming message stanzas.
|
||||
|
||||
Arguments:
|
||||
msg -- The received message stanza.
|
||||
"""
|
||||
logging.info(msg['body'])
|
||||
|
||||
def _command_start(self, iq, session):
|
||||
"""
|
||||
Process the initial command result.
|
||||
|
||||
Arguments:
|
||||
iq -- The iq stanza containing the command result.
|
||||
session -- A dictionary of data relevant to the command
|
||||
session. Additional, custom data may be saved
|
||||
here to persist across handler callbacks.
|
||||
"""
|
||||
|
||||
# The greeting command provides a form with a single field:
|
||||
# <x xmlns="jabber:x:data" type="form">
|
||||
# <field var="greeting"
|
||||
# type="text-single"
|
||||
# label="Your greeting" />
|
||||
# </x>
|
||||
|
||||
form = self['xep_0004'].makeForm(ftype='submit')
|
||||
form.addField(var='greeting',
|
||||
value=session['greeting'])
|
||||
|
||||
session['payload'] = form
|
||||
|
||||
# We don't need to process the next result.
|
||||
session['next'] = None
|
||||
|
||||
# Other options include using:
|
||||
# continue_command() -- Continue to the next step in the workflow
|
||||
# cancel_command() -- Stop command execution.
|
||||
|
||||
self['xep_0050'].complete_command(session)
|
||||
|
||||
def _command_error(self, iq, session):
|
||||
"""
|
||||
Process an error that occurs during command execution.
|
||||
|
||||
Arguments:
|
||||
iq -- The iq stanza containing the error.
|
||||
session -- A dictionary of data relevant to the command
|
||||
session. Additional, custom data may be saved
|
||||
here to persist across handler callbacks.
|
||||
"""
|
||||
logging.error("COMMAND: %s %s" % (iq['error']['condition'],
|
||||
iq['error']['text']))
|
||||
|
||||
# Terminate the command's execution and clear its session.
|
||||
# The session will automatically be cleared if no error
|
||||
# handler is provided.
|
||||
self['xep_0050'].terminate_command(session)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
optp.add_option("-o", "--other", dest="other",
|
||||
help="JID providing commands")
|
||||
optp.add_option("-g", "--greeting", dest="greeting",
|
||||
help="Greeting")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
if opts.other is None:
|
||||
opts.other = raw_input("JID Providing Commands: ")
|
||||
if opts.greeting is None:
|
||||
opts.other = raw_input("Greeting: ")
|
||||
|
||||
# Setup the CommandBot and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = CommandUserBot(opts.jid, opts.password, opts.other, opts.greeting)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0004') # Data Forms
|
||||
xmpp.register_plugin('xep_0050') # Adhoc Commands
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the pydns library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
10
examples/config.xml
Normal file
10
examples/config.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<config xmlns="sleekxmpp:config">
|
||||
<jid>component.localhost</jid>
|
||||
<secret>ssshh</secret>
|
||||
<server>localhost</server>
|
||||
<port>8888</port>
|
||||
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" subscription="both" />
|
||||
</query>
|
||||
</config>
|
||||
190
examples/config_component.py
Executable file
190
examples/config_component.py
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.componentxmpp import ComponentXMPP
|
||||
from sleekxmpp.stanza.roster import Roster
|
||||
from sleekxmpp.xmlstream import ElementBase
|
||||
from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class Config(ElementBase):
|
||||
|
||||
"""
|
||||
In order to make loading and manipulating an XML config
|
||||
file easier, we will create a custom stanza object for
|
||||
our config XML file contents. See the documentation
|
||||
on stanza objects for more information on how to create
|
||||
and use stanza objects and stanza plugins.
|
||||
|
||||
We will reuse the IQ roster query stanza to store roster
|
||||
information since it already exists.
|
||||
|
||||
Example config XML:
|
||||
<config xmlns="sleekxmpp:config">
|
||||
<jid>component.localhost</jid>
|
||||
<secret>ssshh</secret>
|
||||
<server>localhost</server>
|
||||
<port>8888</port>
|
||||
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" subscription="both" />
|
||||
</query>
|
||||
</config>
|
||||
"""
|
||||
|
||||
name = "config"
|
||||
namespace = "sleekxmpp:config"
|
||||
interfaces = set(('jid', 'secret', 'server', 'port'))
|
||||
sub_interfaces = interfaces
|
||||
|
||||
|
||||
registerStanzaPlugin(Config, Roster)
|
||||
|
||||
|
||||
class ConfigComponent(ComponentXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP component that uses an external XML
|
||||
file to store its configuration data. To make testing
|
||||
that the component works, it will also echo messages sent
|
||||
to it.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Create a ConfigComponent.
|
||||
|
||||
Arguments:
|
||||
config -- The XML contents of the config file.
|
||||
config_file -- The XML config file object itself.
|
||||
"""
|
||||
ComponentXMPP.__init__(self, config['jid'],
|
||||
config['secret'],
|
||||
config['server'],
|
||||
config['port'])
|
||||
|
||||
# Store the roster information.
|
||||
self.roster = config['roster']['items']
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the component establishes its connection with the
|
||||
# server and the XML streams are ready for use. We
|
||||
# want to listen for this event so that we we can
|
||||
# broadcast any needed initial presence stanzas.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
# The message event is triggered whenever a message
|
||||
# stanza is received. Be aware that that includes
|
||||
# MUC messages and error messages.
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
The typical action for the session_start event in a component
|
||||
is to broadcast presence stanzas to all subscribers to the
|
||||
component. Note that the component does not have a roster
|
||||
provided by the XMPP server. In this case, we have possibly
|
||||
saved a roster in the component's configuration file.
|
||||
|
||||
Since the component may use any number of JIDs, you should
|
||||
also include the JID that is sending the presence.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
for jid in self.roster:
|
||||
if self.roster[jid]['subscription'] != 'none':
|
||||
self.sendPresence(pfrom=self.jid, pto=jid)
|
||||
|
||||
def message(self, msg):
|
||||
"""
|
||||
Process incoming message stanzas. Be aware that this also
|
||||
includes MUC messages and error messages. It is usually
|
||||
a good idea to check the messages's type before processing
|
||||
or sending replies.
|
||||
|
||||
Since a component may send messages from any number of JIDs,
|
||||
it is best to always include a from JID.
|
||||
|
||||
Arguments:
|
||||
msg -- The received message stanza. See the documentation
|
||||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
# The reply method will use the messages 'to' JID as the
|
||||
# outgoing reply's 'from' JID.
|
||||
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# Component name and secret options.
|
||||
optp.add_option("-c", "--config", help="path to config file",
|
||||
dest="config", default="config.xml")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
# Load configuration data.
|
||||
config_file = open(opts.config, 'r+')
|
||||
config_data = "\n".join([line for line in config_file])
|
||||
config = Config(xml=ET.fromstring(config_data))
|
||||
config_file.close()
|
||||
|
||||
# Setup the ConfigComponent and register plugins. Note that while plugins
|
||||
# may have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = ConfigComponent(config)
|
||||
xmpp.registerPlugin('xep_0030') # Service Discovery
|
||||
xmpp.registerPlugin('xep_0004') # Data Forms
|
||||
xmpp.registerPlugin('xep_0060') # PubSub
|
||||
xmpp.registerPlugin('xep_0199') # XMPP Ping
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
198
examples/disco_browser.py
Executable file
198
examples/disco_browser.py
Executable file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class Disco(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A demonstration for using basic service discovery.
|
||||
|
||||
Send a disco#info and disco#items request to a JID/node combination,
|
||||
and print out the results.
|
||||
|
||||
May also request only particular info categories such as just features,
|
||||
or just items.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, target_jid, target_node='', get=''):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
# Using service discovery requires the XEP-0030 plugin.
|
||||
self.register_plugin('xep_0030')
|
||||
|
||||
self.get = get
|
||||
self.target_jid = target_jid
|
||||
self.target_node = target_node
|
||||
|
||||
# Values to control which disco entities are reported
|
||||
self.info_types = ['', 'all', 'info', 'identities', 'features']
|
||||
self.identity_types = ['', 'all', 'info', 'identities']
|
||||
self.feature_types = ['', 'all', 'info', 'features']
|
||||
self.items_types = ['', 'all', 'items']
|
||||
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use. We want to
|
||||
# listen for this event so that we we can intialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
presence stanza.
|
||||
|
||||
In this case, we send disco#info and disco#items
|
||||
stanzas to the requested JID and print the results.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.get_roster()
|
||||
self.send_presence()
|
||||
|
||||
if self.get in self.info_types:
|
||||
# By using block=True, the result stanza will be
|
||||
# returned. Execution will block until the reply is
|
||||
# received. Non-blocking options would be to listen
|
||||
# for the disco_info event, or passing a handler
|
||||
# function using the callback parameter.
|
||||
info = self['xep_0030'].get_info(jid=self.target_jid,
|
||||
node=self.target_node,
|
||||
block=True)
|
||||
if self.get in self.items_types:
|
||||
# The same applies from above. Listen for the
|
||||
# disco_items event or pass a callback function
|
||||
# if you need to process a non-blocking request.
|
||||
items = self['xep_0030'].get_items(jid=self.target_jid,
|
||||
node=self.target_node,
|
||||
block=True)
|
||||
else:
|
||||
logging.error("Invalid disco request type.")
|
||||
self.disconnect()
|
||||
return
|
||||
|
||||
header = 'XMPP Service Discovery: %s' % self.target_jid
|
||||
print(header)
|
||||
print('-' * len(header))
|
||||
if self.target_node != '':
|
||||
print('Node: %s' % self.target_node)
|
||||
print('-' * len(header))
|
||||
|
||||
if self.get in self.identity_types:
|
||||
print('Identities:')
|
||||
for identity in info['disco_info']['identities']:
|
||||
print(' - %s' % str(identity))
|
||||
|
||||
if self.get in self.feature_types:
|
||||
print('Features:')
|
||||
for feature in info['disco_info']['features']:
|
||||
print(' - %s' % feature)
|
||||
|
||||
if self.get in self.items_types:
|
||||
print('Items:')
|
||||
for item in items['disco_items']['items']:
|
||||
print(' - %s' % str(item))
|
||||
|
||||
self.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
optp.version = '%%prog 0.1'
|
||||
optp.usage = "Usage: %%prog [options] %s <jid> [<node>]" % \
|
||||
'all|info|items|identities|features'
|
||||
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.ERROR,
|
||||
default=logging.ERROR)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.DEBUG,
|
||||
default=logging.ERROR)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=5,
|
||||
default=logging.ERROR)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if len(args) < 2:
|
||||
optp.print_help()
|
||||
exit()
|
||||
|
||||
if len(args) == 2:
|
||||
args = (args[0], args[1], '')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
|
||||
# Setup the Disco browser.
|
||||
xmpp = Disco(opts.jid, opts.password, args[1], args[2], args[0])
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the pydns library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(threaded=False)
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
142
examples/echo_client.py
Executable file
142
examples/echo_client.py
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class EchoBot(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP bot that will echo messages it
|
||||
receives, along with a short thank you message.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use. We want to
|
||||
# listen for this event so that we we can intialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
# The message event is triggered whenever a message
|
||||
# stanza is received. Be aware that that includes
|
||||
# MUC messages and error messages.
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
def message(self, msg):
|
||||
"""
|
||||
Process incoming message stanzas. Be aware that this also
|
||||
includes MUC messages and error messages. It is usually
|
||||
a good idea to check the messages's type before processing
|
||||
or sending replies.
|
||||
|
||||
Arguments:
|
||||
msg -- The received message stanza. See the documentation
|
||||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
|
||||
# Setup the EchoBot and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = EchoBot(opts.jid, opts.password)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0004') # Data Forms
|
||||
xmpp.register_plugin('xep_0060') # PubSub
|
||||
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the pydns library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
186
examples/muc.py
Executable file
186
examples/muc.py
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class MUCBot(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP bot that will greets those
|
||||
who enter the room, and acknowledge any messages
|
||||
that mentions the bot's nickname.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, room, nick):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.room = room
|
||||
self.nick = nick
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use. We want to
|
||||
# listen for this event so that we we can intialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
# The groupchat_message event is triggered whenever a message
|
||||
# stanza is received from any chat room. If you also also
|
||||
# register a handler for the 'message' event, MUC messages
|
||||
# will be processed by both handlers.
|
||||
self.add_event_handler("groupchat_message", self.muc_message)
|
||||
|
||||
# The groupchat_presence event is triggered whenever a
|
||||
# presence stanza is received from any chat room, including
|
||||
# any presences you send yourself. To limit event handling
|
||||
# to a single room, use the events muc::room@server::presence,
|
||||
# muc::room@server::got_online, or muc::room@server::got_offline.
|
||||
self.add_event_handler("muc::%s::got_online" % self.room,
|
||||
self.muc_online)
|
||||
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.getRoster()
|
||||
self.sendPresence()
|
||||
self.plugin['xep_0045'].joinMUC(self.room,
|
||||
self.nick,
|
||||
# If a room password is needed, use:
|
||||
# password=the_room_password,
|
||||
wait=True)
|
||||
|
||||
def muc_message(self, msg):
|
||||
"""
|
||||
Process incoming message stanzas from any chat room. Be aware
|
||||
that if you also have any handlers for the 'message' event,
|
||||
message stanzas may be processed by both handlers, so check
|
||||
the 'type' attribute when using a 'message' event handler.
|
||||
|
||||
Whenever the bot's nickname is mentioned, respond to
|
||||
the message.
|
||||
|
||||
IMPORTANT: Always check that a message is not from yourself,
|
||||
otherwise you will create an infinite loop responding
|
||||
to your own messages.
|
||||
|
||||
This handler will reply to messages that mention
|
||||
the bot's nickname.
|
||||
|
||||
Arguments:
|
||||
msg -- The received message stanza. See the documentation
|
||||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
if msg['mucnick'] != self.nick and self.nick in msg['body']:
|
||||
self.send_message(mto=msg['from'].bare,
|
||||
mbody="I heard that, %s." % msg['mucnick'],
|
||||
mtype='groupchat')
|
||||
|
||||
def muc_online(self, presence):
|
||||
"""
|
||||
Process a presence stanza from a chat room. In this case,
|
||||
presences from users that have just come online are
|
||||
handled by sending a welcome message that includes
|
||||
the user's nickname and role in the room.
|
||||
|
||||
Arguments:
|
||||
presence -- The received presence stanza. See the
|
||||
documentation for the Presence stanza
|
||||
to see how else it may be used.
|
||||
"""
|
||||
if presence['muc']['nick'] != self.nick:
|
||||
self.send_message(mto=presence['from'].bare,
|
||||
mbody="Hello, %s %s" % (presence['muc']['role'],
|
||||
presence['muc']['nick']),
|
||||
mtype='groupchat')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
optp.add_option("-r", "--room", dest="room",
|
||||
help="MUC room to join")
|
||||
optp.add_option("-n", "--nick", dest="nick",
|
||||
help="MUC nickname")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if None in [opts.jid, opts.password, opts.room, opts.nick]:
|
||||
optp.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Setup the MUCBot and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = MUCBot(opts.jid, opts.password, opts.room, opts.nick)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0045') # Multi-User Chat
|
||||
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the pydns library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
140
examples/ping.py
Executable file
140
examples/ping.py
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class PingTest(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP bot that will send a ping request
|
||||
to a given JID.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, pingjid):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
if pingjid is None:
|
||||
pingjid = self.jid
|
||||
self.pingjid = pingjid
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the bot establishes its connection with the server
|
||||
# and the XML streams are ready for use. We want to
|
||||
# listen for this event so that we we can intialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
result = self['xep_0199'].send_ping(self.pingjid,
|
||||
timeout=10,
|
||||
errorfalse=True)
|
||||
logging.info("Pinging...")
|
||||
if result is False:
|
||||
logging.info("Couldn't ping.")
|
||||
self.disconnect()
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info("Success! RTT: %s" % str(result))
|
||||
self.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
optp.add_option('-t', '--pingto', help='set jid to ping',
|
||||
action='store', type='string', dest='pingjid',
|
||||
default=None)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
|
||||
# Setup the PingTest and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = PingTest(opts.jid, opts.password, opts.pingjid)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0004') # Data Forms
|
||||
xmpp.register_plugin('xep_0060') # PubSub
|
||||
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the pydns library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
44
examples/rpc_async.py
Normal file
44
examples/rpc_async.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Dann Martens
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \
|
||||
ANY_ALL, Future
|
||||
import time
|
||||
|
||||
class Boomerang(Endpoint):
|
||||
|
||||
def FQN(self):
|
||||
return 'boomerang'
|
||||
|
||||
@remote
|
||||
def throw(self):
|
||||
print "Duck!"
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
session = Remote.new_session('kangaroo@xmpp.org/rpc', '*****')
|
||||
|
||||
session.new_handler(ANY_ALL, Boomerang)
|
||||
|
||||
boomerang = session.new_proxy('kangaroo@xmpp.org/rpc', Boomerang)
|
||||
|
||||
callback = Future()
|
||||
|
||||
boomerang.async(callback).throw()
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
53
examples/rpc_client_side.py
Normal file
53
examples/rpc_client_side.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Dann Martens
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \
|
||||
ANY_ALL
|
||||
import threading
|
||||
import time
|
||||
|
||||
class Thermostat(Endpoint):
|
||||
|
||||
def FQN(self):
|
||||
return 'thermostat'
|
||||
|
||||
def __init(self, initial_temperature):
|
||||
self._temperature = initial_temperature
|
||||
self._event = threading.Event()
|
||||
|
||||
@remote
|
||||
def set_temperature(self, temperature):
|
||||
return NotImplemented
|
||||
|
||||
@remote
|
||||
def get_temperature(self):
|
||||
return NotImplemented
|
||||
|
||||
@remote(False)
|
||||
def release(self):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
session = Remote.new_session('operator@xmpp.org/rpc', '*****')
|
||||
|
||||
thermostat = session.new_proxy('thermostat@xmpp.org/rpc', Thermostat)
|
||||
|
||||
print("Current temperature is %s" % thermostat.get_temperature())
|
||||
|
||||
thermostat.set_temperature(20)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
session.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
52
examples/rpc_server_side.py
Normal file
52
examples/rpc_server_side.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Dann Martens
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \
|
||||
ANY_ALL
|
||||
import threading
|
||||
|
||||
class Thermostat(Endpoint):
|
||||
|
||||
def FQN(self):
|
||||
return 'thermostat'
|
||||
|
||||
def __init(self, initial_temperature):
|
||||
self._temperature = initial_temperature
|
||||
self._event = threading.Event()
|
||||
|
||||
@remote
|
||||
def set_temperature(self, temperature):
|
||||
print("Setting temperature to %s" % temperature)
|
||||
self._temperature = temperature
|
||||
|
||||
@remote
|
||||
def get_temperature(self):
|
||||
return self._temperature
|
||||
|
||||
@remote(False)
|
||||
def release(self):
|
||||
self._event.set()
|
||||
|
||||
def wait_for_release(self):
|
||||
self._event.wait()
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
session = Remote.new_session('sleek@xmpp.org/rpc', '*****')
|
||||
|
||||
thermostat = session.new_handler(ANY_ALL, Thermostat, 18)
|
||||
|
||||
thermostat.wait_for_release()
|
||||
|
||||
session.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
41
setup.py
41
setup.py
@@ -12,20 +12,22 @@
|
||||
from distutils.core import setup
|
||||
import sys
|
||||
|
||||
import sleekxmpp
|
||||
|
||||
# if 'cygwin' in sys.platform.lower():
|
||||
# min_version = '0.6c6'
|
||||
# else:
|
||||
# min_version = '0.6a9'
|
||||
#
|
||||
#
|
||||
# try:
|
||||
# use_setuptools(min_version=min_version)
|
||||
# except TypeError:
|
||||
# # locally installed ez_setup won't have min_version
|
||||
# use_setuptools()
|
||||
#
|
||||
#
|
||||
# from setuptools import setup, find_packages, Extension, Feature
|
||||
|
||||
VERSION = '0.2.3.1'
|
||||
VERSION = sleekxmpp.__version__
|
||||
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
||||
LONG_DESCRIPTION = """
|
||||
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
|
||||
@@ -37,17 +39,31 @@ CLASSIFIERS = [ 'Intended Audience :: Developers',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
]
|
||||
|
||||
packages = [ 'sleekxmpp',
|
||||
'sleekxmpp/plugins',
|
||||
'sleekxmpp/stanza',
|
||||
'sleekxmpp/xmlstream',
|
||||
'sleekxmpp/xmlstream/matcher',
|
||||
'sleekxmpp/xmlstream/handler' ]
|
||||
packages = [ 'sleekxmpp',
|
||||
'sleekxmpp/stanza',
|
||||
'sleekxmpp/test',
|
||||
'sleekxmpp/xmlstream',
|
||||
'sleekxmpp/xmlstream/matcher',
|
||||
'sleekxmpp/xmlstream/handler',
|
||||
'sleekxmpp/thirdparty',
|
||||
'sleekxmpp/plugins',
|
||||
'sleekxmpp/plugins/xep_0009',
|
||||
'sleekxmpp/plugins/xep_0009/stanza',
|
||||
'sleekxmpp/plugins/xep_0030',
|
||||
'sleekxmpp/plugins/xep_0030/stanza',
|
||||
'sleekxmpp/plugins/xep_0050',
|
||||
'sleekxmpp/plugins/xep_0059',
|
||||
'sleekxmpp/plugins/xep_0085',
|
||||
'sleekxmpp/plugins/xep_0086',
|
||||
'sleekxmpp/plugins/xep_0092',
|
||||
'sleekxmpp/plugins/xep_0128',
|
||||
'sleekxmpp/plugins/xep_0199',
|
||||
]
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
packages.append('sleekxmpp/xmlstream/tostring26')
|
||||
py_modules = ['sleekxmpp.xmlstream.tostring.tostring26']
|
||||
else:
|
||||
packages.append('sleekxmpp/xmlstream/tostring')
|
||||
py_modules = ['sleekxmpp.xmlstream.tostring.tostring']
|
||||
|
||||
setup(
|
||||
name = "sleekxmpp",
|
||||
@@ -59,7 +75,8 @@ setup(
|
||||
url = 'http://code.google.com/p/sleekxmpp',
|
||||
license = 'MIT',
|
||||
platforms = [ 'any' ],
|
||||
packages = packages,
|
||||
packages = packages,
|
||||
py_modules = py_modules,
|
||||
requires = [ 'tlslite', 'pythondns' ],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,245 +1,19 @@
|
||||
#!/usr/bin/python2.5
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . basexmpp import basexmpp
|
||||
from xml.etree import cElementTree as ET
|
||||
from . xmlstream.xmlstream import XMLStream
|
||||
from . xmlstream.xmlstream import RestartStream
|
||||
from . xmlstream.matcher.xmlmask import MatchXMLMask
|
||||
from . xmlstream.matcher.xpath import MatchXPath
|
||||
from . xmlstream.matcher.many import MatchMany
|
||||
from . xmlstream.handler.callback import Callback
|
||||
from . xmlstream.stanzabase import StanzaBase
|
||||
from . xmlstream import xmlstream as xmlstreammod
|
||||
from . stanza.message import Message
|
||||
from . stanza.iq import Iq
|
||||
import time
|
||||
import logging
|
||||
import base64
|
||||
import sys
|
||||
import random
|
||||
import copy
|
||||
from . import plugins
|
||||
#from . import stanza
|
||||
srvsupport = True
|
||||
try:
|
||||
import dns.resolver
|
||||
except ImportError:
|
||||
srvsupport = False
|
||||
|
||||
from sleekxmpp.basexmpp import BaseXMPP
|
||||
from sleekxmpp.clientxmpp import ClientXMPP
|
||||
from sleekxmpp.componentxmpp import ComponentXMPP
|
||||
from sleekxmpp.stanza import Message, Presence, Iq
|
||||
from sleekxmpp.xmlstream.handler import *
|
||||
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||
from sleekxmpp.xmlstream.matcher import *
|
||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
||||
|
||||
|
||||
#class PresenceStanzaType(object):
|
||||
#
|
||||
# def fromXML(self, xml):
|
||||
# self.ptype = xml.get('type')
|
||||
|
||||
|
||||
class ClientXMPP(basexmpp, XMLStream):
|
||||
"""SleekXMPP's client class. Use only for good, not evil."""
|
||||
|
||||
def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True):
|
||||
global srvsupport
|
||||
XMLStream.__init__(self)
|
||||
self.default_ns = 'jabber:client'
|
||||
basexmpp.__init__(self)
|
||||
self.plugin_config = plugin_config
|
||||
self.escape_quotes = escape_quotes
|
||||
self.set_jid(jid)
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.auto_reconnect = True
|
||||
self.srvsupport = srvsupport
|
||||
self.password = password
|
||||
self.registered_features = []
|
||||
self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.server,self.default_ns)
|
||||
self.stream_footer = "</stream:stream>"
|
||||
#self.map_namespace('http://etherx.jabber.org/streams', 'stream')
|
||||
#self.map_namespace('jabber:client', '')
|
||||
self.features = []
|
||||
#TODO: Use stream state here
|
||||
self.authenticated = False
|
||||
self.sessionstarted = False
|
||||
self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
|
||||
self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True))
|
||||
#self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
|
||||
self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
|
||||
self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
|
||||
self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
|
||||
self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
|
||||
|
||||
#self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
|
||||
#self.register_plugins()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self.plugin:
|
||||
return self.plugin[key]
|
||||
else:
|
||||
logging.warning("""Plugin "%s" is not loaded.""" % key)
|
||||
return False
|
||||
|
||||
def get(self, key, default):
|
||||
return self.plugin.get(key, default)
|
||||
|
||||
def connect(self, address=tuple()):
|
||||
"""Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
|
||||
the JID server."""
|
||||
if not address or len(address) < 2:
|
||||
if not self.srvsupport:
|
||||
logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.")
|
||||
else:
|
||||
logging.debug("Since no address is supplied, attempting SRV lookup.")
|
||||
try:
|
||||
answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, "SRV")
|
||||
except dns.resolver.NXDOMAIN:
|
||||
logging.debug("No appropriate SRV record found. Using JID server name.")
|
||||
else:
|
||||
# pick a random answer, weighted by priority
|
||||
# there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
|
||||
# suggestions are welcome
|
||||
addresses = {}
|
||||
intmax = 0
|
||||
priorities = []
|
||||
for answer in answers:
|
||||
intmax += answer.priority
|
||||
addresses[intmax] = (answer.target.to_text()[:-1], answer.port)
|
||||
priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n priorities.sort()
|
||||
picked = random.randint(0, intmax)
|
||||
for priority in priorities:
|
||||
if picked <= priority:
|
||||
address = addresses[priority]
|
||||
break
|
||||
if not address:
|
||||
# if all else fails take server from JID.
|
||||
address = (self.server, 5222)
|
||||
result = XMLStream.connect(self, address[0], address[1], use_tls=True)
|
||||
if result:
|
||||
self.event("connected")
|
||||
else:
|
||||
logging.warning("Failed to connect")
|
||||
self.event("disconnected")
|
||||
return result
|
||||
|
||||
# overriding reconnect and disconnect so that we can get some events
|
||||
# should events be part of or required by xmlstream? Maybe that would be cleaner
|
||||
def reconnect(self):
|
||||
logging.info("Reconnecting")
|
||||
self.event("disconnected")
|
||||
XMLStream.reconnect(self)
|
||||
|
||||
def disconnect(self, init=True, close=False, reconnect=False):
|
||||
self.event("disconnected")
|
||||
XMLStream.disconnect(self, reconnect)
|
||||
|
||||
def registerFeature(self, mask, pointer, breaker = False):
|
||||
"""Register a stream feature."""
|
||||
self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
|
||||
|
||||
def updateRoster(self, jid, name=None, subscription=None, groups=[]):
|
||||
"""Add or change a roster item."""
|
||||
iq = self.Iq().setValues({'type': 'set'})
|
||||
iq['roster'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}}
|
||||
#self.send(iq, self.Iq().setValues({'id': iq['id']}))
|
||||
r = iq.send()
|
||||
return r['type'] == 'result'
|
||||
|
||||
def getRoster(self):
|
||||
"""Request the roster be sent."""
|
||||
iq = self.Iq().setValues({'type': 'get'}).enable('roster').send()
|
||||
self._handleRoster(iq, request=True)
|
||||
|
||||
def _handleStreamFeatures(self, features):
|
||||
self.features = []
|
||||
for sub in features.xml:
|
||||
self.features.append(sub.tag)
|
||||
for subelement in features.xml:
|
||||
for feature in self.registered_features:
|
||||
if feature[0].match(subelement):
|
||||
#if self.maskcmp(subelement, feature[0], True):
|
||||
if feature[1](subelement) and feature[2]: #if breaker, don't continue
|
||||
return True
|
||||
|
||||
def handler_starttls(self, xml):
|
||||
if not self.authenticated and self.ssl_support:
|
||||
self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, instream=True)
|
||||
self.sendXML(xml)
|
||||
return True
|
||||
else:
|
||||
logging.warning("The module tlslite is required in to some servers, and has not been found.")
|
||||
return False
|
||||
|
||||
def handler_tls_start(self, xml):
|
||||
logging.debug("Starting TLS")
|
||||
if self.startTLS():
|
||||
raise RestartStream()
|
||||
|
||||
def handler_sasl_auth(self, xml):
|
||||
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
|
||||
return False
|
||||
logging.debug("Starting SASL Auth")
|
||||
self.add_handler("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_success, instream=True)
|
||||
self.add_handler("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_fail, instream=True)
|
||||
sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism')
|
||||
if len(sasl_mechs):
|
||||
for sasl_mech in sasl_mechs:
|
||||
self.features.append("sasl:%s" % sasl_mech.text)
|
||||
if 'sasl:PLAIN' in self.features:
|
||||
if sys.version_info < (3,0):
|
||||
self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8'))
|
||||
else:
|
||||
self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8'))
|
||||
else:
|
||||
logging.error("No appropriate login method.")
|
||||
self.disconnect()
|
||||
#if 'sasl:DIGEST-MD5' in self.features:
|
||||
# self._auth_digestmd5()
|
||||
return True
|
||||
|
||||
def handler_auth_success(self, xml):
|
||||
self.authenticated = True
|
||||
self.features = []
|
||||
raise RestartStream()
|
||||
|
||||
def handler_auth_fail(self, xml):
|
||||
logging.info("Authentication failed.")
|
||||
self.disconnect()
|
||||
self.event("failed_auth")
|
||||
|
||||
def handler_bind_resource(self, xml):
|
||||
logging.debug("Requesting resource: %s" % self.resource)
|
||||
iq = self.Iq(stype='set')
|
||||
res = ET.Element('resource')
|
||||
res.text = self.resource
|
||||
xml.append(res)
|
||||
iq.append(xml)
|
||||
response = iq.send()
|
||||
#response = self.send(iq, self.Iq(sid=iq['id']))
|
||||
self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text)
|
||||
logging.info("Node set to: %s" % self.fulljid)
|
||||
if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features:
|
||||
logging.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.event("session_start")
|
||||
|
||||
def handler_start_session(self, xml):
|
||||
if self.authenticated:
|
||||
response = self.send(self.makeIqSet(xml), self.makeIq(self.getId()))
|
||||
logging.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.event("session_start")
|
||||
|
||||
def _handleRoster(self, iq, request=False):
|
||||
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
|
||||
for jid in iq['roster']['items']:
|
||||
if not jid in self.roster:
|
||||
self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': True}
|
||||
self.roster[jid].update(iq['roster']['items'][jid])
|
||||
if iq['type'] == 'set':
|
||||
self.send(self.Iq().setValues({'type': 'result', 'id': iq['id']}).enable('roster'))
|
||||
self.event("roster_update", iq)
|
||||
__version__ = '1.0beta5'
|
||||
__version_info__ = (1, 0, 0, 'beta5', 0)
|
||||
|
||||
@@ -3,291 +3,704 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement, unicode_literals
|
||||
|
||||
|
||||
from xml.etree import cElementTree as ET
|
||||
from . xmlstream.xmlstream import XMLStream
|
||||
from . xmlstream.matcher.xmlmask import MatchXMLMask
|
||||
from . xmlstream.matcher.many import MatchMany
|
||||
from . xmlstream.handler.xmlcallback import XMLCallback
|
||||
from . xmlstream.handler.xmlwaiter import XMLWaiter
|
||||
from . xmlstream.handler.callback import Callback
|
||||
from . import plugins
|
||||
from . stanza.message import Message
|
||||
from . stanza.iq import Iq
|
||||
from . stanza.presence import Presence
|
||||
from . stanza.roster import Roster
|
||||
from . stanza.nick import Nick
|
||||
from . stanza.htmlim import HTMLIM
|
||||
from . stanza.error import Error
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import logging
|
||||
|
||||
if sys.version_info < (3,0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
import sleekxmpp
|
||||
from sleekxmpp import plugins
|
||||
|
||||
from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError
|
||||
from sleekxmpp.stanza.roster import Roster
|
||||
from sleekxmpp.stanza.nick import Nick
|
||||
from sleekxmpp.stanza.htmlim import HTMLIM
|
||||
|
||||
from sleekxmpp.xmlstream import XMLStream, JID, tostring
|
||||
from sleekxmpp.xmlstream import ET, register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.matcher import *
|
||||
from sleekxmpp.xmlstream.handler import *
|
||||
|
||||
|
||||
def stanzaPlugin(stanza, plugin):
|
||||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = 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):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class basexmpp(object):
|
||||
def __init__(self):
|
||||
self.id = 0
|
||||
self.id_lock = threading.Lock()
|
||||
self.sentpresence = False
|
||||
self.fulljid = ''
|
||||
self.resource = ''
|
||||
self.jid = ''
|
||||
self.username = ''
|
||||
self.server = ''
|
||||
self.plugin = {}
|
||||
self.auto_authorize = True
|
||||
self.auto_subscribe = True
|
||||
self.event_handlers = {}
|
||||
self.roster = {}
|
||||
self.registerHandler(Callback('IM', MatchXMLMask("<message xmlns='%s'><body /></message>" % self.default_ns), self._handleMessage))
|
||||
self.registerHandler(Callback('Presence', MatchXMLMask("<presence xmlns='%s' />" % self.default_ns), self._handlePresence))
|
||||
self.add_event_handler('presence_subscribe', self._handlePresenceSubscribe)
|
||||
self.registerStanza(Message)
|
||||
self.registerStanza(Iq)
|
||||
self.registerStanza(Presence)
|
||||
self.stanzaPlugin(Iq, Roster)
|
||||
self.stanzaPlugin(Message, Nick)
|
||||
self.stanzaPlugin(Message, HTMLIM)
|
||||
class BaseXMPP(XMLStream):
|
||||
|
||||
def stanzaPlugin(self, stanza, plugin):
|
||||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
|
||||
|
||||
def Message(self, *args, **kwargs):
|
||||
return Message(self, *args, **kwargs)
|
||||
"""
|
||||
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.
|
||||
|
||||
def Iq(self, *args, **kwargs):
|
||||
return Iq(self, *args, **kwargs)
|
||||
Attributes:
|
||||
auto_authorize -- Manage automatically accepting roster
|
||||
subscriptions.
|
||||
auto_subscribe -- Manage automatically requesting mutual
|
||||
subscriptions.
|
||||
is_component -- Indicates if this stream is for an XMPP component.
|
||||
jid -- The XMPP JID for this stream.
|
||||
plugin -- A dictionary of loaded plugins.
|
||||
plugin_config -- A dictionary of plugin configurations.
|
||||
plugin_whitelist -- A list of approved plugins.
|
||||
sentpresence -- Indicates if an initial presence has been sent.
|
||||
roster -- A dictionary containing subscribed JIDs and
|
||||
their presence statuses.
|
||||
|
||||
def Presence(self, *args, **kwargs):
|
||||
return Presence(self, *args, **kwargs)
|
||||
|
||||
def set_jid(self, jid):
|
||||
"""Rip a JID apart and claim it as our own."""
|
||||
self.fulljid = jid
|
||||
self.resource = self.getjidresource(jid)
|
||||
self.jid = self.getjidbare(jid)
|
||||
self.username = jid.split('@', 1)[0]
|
||||
self.server = jid.split('@',1)[-1].split('/', 1)[0]
|
||||
|
||||
def registerPlugin(self, plugin, pconfig = {}):
|
||||
"""Register a plugin not in plugins.__init__.__all__ but in the plugins
|
||||
directory."""
|
||||
# discover relative "path" to the plugins module from the main app, and import it.
|
||||
# TODO:
|
||||
# gross, this probably isn't necessary anymore, especially for an installed module
|
||||
__import__("%s.%s" % (globals()['plugins'].__name__, plugin))
|
||||
# init the plugin class
|
||||
self.plugin[plugin] = getattr(getattr(plugins, plugin), plugin)(self, pconfig) # eek
|
||||
# all of this for a nice debug? sure.
|
||||
xep = ''
|
||||
if hasattr(self.plugin[plugin], 'xep'):
|
||||
xep = "(XEP-%s) " % self.plugin[plugin].xep
|
||||
logging.debug("Loaded Plugin %s%s" % (xep, self.plugin[plugin].description))
|
||||
|
||||
def register_plugins(self):
|
||||
"""Initiates all plugins in the plugins/__init__.__all__"""
|
||||
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.registerPlugin(plugin, self.plugin_config.get(plugin, {}))
|
||||
else:
|
||||
raise NameError("No plugin by the name of %s listed in plugins.__all__." % plugin)
|
||||
# run post_init() for cross-plugin interaction
|
||||
for plugin in self.plugin:
|
||||
self.plugin[plugin].post_init()
|
||||
|
||||
def getNewId(self):
|
||||
with self.id_lock:
|
||||
self.id += 1
|
||||
return self.getId()
|
||||
|
||||
def add_handler(self, mask, pointer, disposable=False, threaded=False, filter=False, instream=False):
|
||||
#logging.warning("Deprecated add_handler used for %s: %s." % (mask, pointer))
|
||||
self.registerHandler(XMLCallback('add_handler_%s' % self.getNewId(), MatchXMLMask(mask), pointer, threaded, disposable, instream))
|
||||
|
||||
def getId(self):
|
||||
return "%x".upper() % self.id
|
||||
Methods:
|
||||
Iq -- Factory for creating an Iq stanzas.
|
||||
Message -- Factory for creating Message stanzas.
|
||||
Presence -- Factory for creating Presence stanzas.
|
||||
get -- Return a plugin given its name.
|
||||
make_iq -- Create and initialize an Iq stanza.
|
||||
make_iq_error -- Create an Iq stanza of type 'error'.
|
||||
make_iq_get -- Create an Iq stanza of type 'get'.
|
||||
make_iq_query -- Create an Iq stanza with a given query.
|
||||
make_iq_result -- Create an Iq stanza of type 'result'.
|
||||
make_iq_set -- Create an Iq stanza of type 'set'.
|
||||
make_message -- Create and initialize a Message stanza.
|
||||
make_presence -- Create and initialize a Presence stanza.
|
||||
make_query_roster -- Create a roster query.
|
||||
process -- Overrides XMLStream.process.
|
||||
register_plugin -- Load and configure a plugin.
|
||||
register_plugins -- Load and configure multiple plugins.
|
||||
send_message -- Create and send a Message stanza.
|
||||
send_presence -- Create and send a Presence stanza.
|
||||
send_presence_subscribe -- Send a subscription request.
|
||||
"""
|
||||
|
||||
def sendXML(self, data, mask=None, timeout=10):
|
||||
return self.send(self.tostring(data), mask, timeout)
|
||||
|
||||
def send(self, data, mask=None, timeout=10):
|
||||
#logging.warning("Deprecated send used for \"%s\"" % (data,))
|
||||
#if not type(data) == type(''):
|
||||
# data = self.tostring(data)
|
||||
if hasattr(mask, 'xml'):
|
||||
mask = mask.xml
|
||||
data = str(data)
|
||||
if mask is not None:
|
||||
waitfor = XMLWaiter('SendWait_%s' % self.getNewId(), MatchXMLMask(mask))
|
||||
self.registerHandler(waitfor)
|
||||
self.sendRaw(data)
|
||||
if mask is not None:
|
||||
return waitfor.wait(timeout)
|
||||
|
||||
def makeIq(self, id=0, ifrom=None):
|
||||
return self.Iq().setValues({'id': id, 'from': ifrom})
|
||||
|
||||
def makeIqGet(self, queryxmlns = None):
|
||||
iq = self.Iq().setValues({'type': 'get'})
|
||||
if queryxmlns:
|
||||
iq.append(ET.Element("{%s}query" % queryxmlns))
|
||||
return iq
|
||||
|
||||
def makeIqResult(self, id):
|
||||
return self.Iq().setValues({'id': id, 'type': 'result'})
|
||||
|
||||
def makeIqSet(self, sub=None):
|
||||
iq = self.Iq().setValues({'type': 'set'})
|
||||
if sub != None:
|
||||
iq.append(sub)
|
||||
return iq
|
||||
def __init__(self, default_ns='jabber:client'):
|
||||
"""
|
||||
Adapt an XML stream for use with XMPP.
|
||||
|
||||
def makeIqError(self, id, type='cancel', condition='feature-not-implemented', text=None):
|
||||
iq = self.Iq().setValues({'id': id})
|
||||
iq['error'].setValues({'type': type, 'condition': condition, 'text': text})
|
||||
return iq
|
||||
Arguments:
|
||||
default_ns -- Ensure that the correct default XML namespace
|
||||
is used during initialization.
|
||||
"""
|
||||
XMLStream.__init__(self)
|
||||
|
||||
def makeIqQuery(self, iq, xmlns):
|
||||
query = ET.Element("{%s}query" % xmlns)
|
||||
iq.append(query)
|
||||
return iq
|
||||
|
||||
def makeQueryRoster(self, iq=None):
|
||||
query = ET.Element("{jabber:iq:roster}query")
|
||||
if iq:
|
||||
iq.append(query)
|
||||
return query
|
||||
|
||||
def add_event_handler(self, name, pointer, threaded=False, disposable=False):
|
||||
if not name in self.event_handlers:
|
||||
self.event_handlers[name] = []
|
||||
self.event_handlers[name].append((pointer, threaded, disposable))
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.default_ns = default_ns
|
||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||
|
||||
def event(self, name, eventdata = {}): # called on an event
|
||||
for handler in self.event_handlers.get(name, []):
|
||||
if handler[1]: #if threaded
|
||||
#thread.start_new(handler[0], (eventdata,))
|
||||
x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(eventdata,))
|
||||
x.start()
|
||||
else:
|
||||
handler[0](eventdata)
|
||||
if handler[2]: #disposable
|
||||
with self.lock:
|
||||
self.event_handlers[name].pop(self.event_handlers[name].index(handler))
|
||||
|
||||
def makeMessage(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None):
|
||||
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'] = mhtml
|
||||
return message
|
||||
|
||||
def makePresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None):
|
||||
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
|
||||
if pshow is not None: presence['type'] = pshow
|
||||
if pfrom is None: #maybe this should be done in stanzabase
|
||||
presence['from'] = self.fulljid
|
||||
presence['priority'] = ppriority
|
||||
presence['status'] = pstatus
|
||||
return presence
|
||||
|
||||
def sendMessage(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None):
|
||||
self.send(self.makeMessage(mto,mbody,msubject,mtype,mhtml,mfrom,mnick))
|
||||
|
||||
def sendPresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None):
|
||||
self.send(self.makePresence(pshow,pstatus,ppriority,pto, ptype=ptype, pfrom=pfrom))
|
||||
if not self.sentpresence:
|
||||
self.event('sent_presence')
|
||||
self.sentpresence = True
|
||||
self.boundjid = JID("")
|
||||
|
||||
def sendPresenceSubscription(self, pto, pfrom=None, ptype='subscribe', pnick=None) :
|
||||
presence = self.makePresence(ptype=ptype, pfrom=pfrom, pto=self.getjidbare(pto))
|
||||
if pnick :
|
||||
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
|
||||
nick.text = pnick
|
||||
presence.append(nick)
|
||||
self.send(presence)
|
||||
|
||||
def getjidresource(self, fulljid):
|
||||
if '/' in fulljid:
|
||||
return fulljid.split('/', 1)[-1]
|
||||
else:
|
||||
return ''
|
||||
|
||||
def getjidbare(self, fulljid):
|
||||
return fulljid.split('/', 1)[0]
|
||||
self.plugin = {}
|
||||
self.plugin_config = {}
|
||||
self.plugin_whitelist = []
|
||||
self.roster = {}
|
||||
self.is_component = False
|
||||
self.auto_authorize = True
|
||||
self.auto_subscribe = True
|
||||
|
||||
def _handleMessage(self, msg):
|
||||
self.event('message', msg)
|
||||
|
||||
def _handlePresence(self, presence):
|
||||
"""Update roster items based on presence"""
|
||||
self.event("presence_%s" % presence['type'], presence)
|
||||
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
|
||||
jid = presence['from'].bare
|
||||
resource = presence['from'].resource
|
||||
show = presence['type']
|
||||
status = presence['status']
|
||||
priority = presence['priority']
|
||||
wasoffline = False
|
||||
oldroster = self.roster.get(jid, {}).get(resource, {})
|
||||
if not presence['from'].bare in self.roster:
|
||||
self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': False}
|
||||
if not resource in self.roster[jid]['presence']:
|
||||
wasoffline = True
|
||||
self.roster[jid]['presence'][resource] = {'show': show, 'status': status, 'priority': priority}
|
||||
else:
|
||||
if self.roster[jid]['presence'][resource].get('show', 'unavailable') == 'unavailable':
|
||||
wasoffline = True
|
||||
self.roster[jid]['presence'][resource] = {'show': show, 'status': status}
|
||||
self.roster[jid]['presence'][resource]['priority'] = priority
|
||||
name = self.roster[jid].get('name', '')
|
||||
if wasoffline and (show == 'available' or show in presence.showtypes):
|
||||
self.event("got_online", presence)
|
||||
elif show == 'unavailable':
|
||||
logging.debug("%s %s got offline" % (jid, resource))
|
||||
if len(self.roster[jid]['presence']) > 1:
|
||||
del self.roster[jid]['presence'][resource]
|
||||
else:
|
||||
del self.roster[jid]
|
||||
self.event("got_offline", presence)
|
||||
elif oldroster != self.roster.get(jid, {'presence': {}})['presence'].get(resource, {}):
|
||||
self.event("changed_status", presence)
|
||||
name = ''
|
||||
if name:
|
||||
name = "(%s) " % name
|
||||
logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show,status))
|
||||
|
||||
def _handlePresenceSubscribe(self, presence):
|
||||
"""Handling subscriptions automatically."""
|
||||
if self.auto_authorize == True:
|
||||
self.send(self.makePresence(ptype='subscribed', pto=presence['from'].bare))
|
||||
if self.auto_subscribe:
|
||||
self.send(self.makePresence(ptype='subscribe', pto=presence['from'].bare))
|
||||
elif self.auto_authorize == False:
|
||||
self.send(self.makePresence(ptype='unsubscribed', pto=presence['from'].bare))
|
||||
self.sentpresence = False
|
||||
|
||||
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('presence_subscribe',
|
||||
self._handle_subscribe)
|
||||
self.add_event_handler('disconnected',
|
||||
self._handle_disconnected)
|
||||
|
||||
# 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)
|
||||
register_stanza_plugin(Message, HTMLIM)
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
"""
|
||||
Ensure that plugin inter-dependencies are handled before starting
|
||||
event processing.
|
||||
|
||||
Overrides XMLStream.process.
|
||||
"""
|
||||
for name in self.plugin:
|
||||
if not self.plugin[name].post_inited:
|
||||
self.plugin[name].post_init()
|
||||
return XMLStream.process(self, *args, **kwargs)
|
||||
|
||||
def register_plugin(self, plugin, pconfig={}, module=None):
|
||||
"""
|
||||
Register and configure a plugin for use in this stream.
|
||||
|
||||
Arguments:
|
||||
plugin -- The name of the plugin class. Plugin names must
|
||||
be unique.
|
||||
pconfig -- A dictionary of configuration data for the plugin.
|
||||
Defaults to an empty dictionary.
|
||||
module -- Optional refence to the module containing the plugin
|
||||
class if using custom plugins.
|
||||
"""
|
||||
try:
|
||||
# Import the given module that contains the plugin.
|
||||
if not module:
|
||||
module = sleekxmpp.plugins
|
||||
module = __import__("%s.%s" % (module.__name__, plugin),
|
||||
globals(), locals(), [plugin])
|
||||
if isinstance(module, str):
|
||||
# We probably want to load a module from outside
|
||||
# the sleekxmpp package, so leave out the globals().
|
||||
module = __import__(module, fromlist=[plugin])
|
||||
|
||||
# Load the plugin class from the module.
|
||||
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
|
||||
|
||||
# Let XEP implementing plugins have some extra logging info.
|
||||
xep = ''
|
||||
if hasattr(self.plugin[plugin], 'xep'):
|
||||
xep = "(XEP-%s) " % self.plugin[plugin].xep
|
||||
|
||||
desc = (xep, self.plugin[plugin].description)
|
||||
log.debug("Loaded Plugin %s%s" % desc)
|
||||
except:
|
||||
log.exception("Unable to load plugin: %s", plugin)
|
||||
|
||||
def register_plugins(self):
|
||||
"""
|
||||
Register and initialize all built-in plugins.
|
||||
|
||||
Optionally, the list of plugins loaded may be limited to those
|
||||
contained in self.plugin_whitelist.
|
||||
|
||||
Plugin configurations stored in self.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,
|
||||
self.plugin_config.get(plugin, {}))
|
||||
else:
|
||||
raise NameError("Plugin %s not in plugins.__all__." % plugin)
|
||||
|
||||
# Resolve plugin inter-dependencies.
|
||||
for plugin in self.plugin:
|
||||
self.plugin[plugin].post_init()
|
||||
|
||||
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."""
|
||||
return Message(self, *args, **kwargs)
|
||||
|
||||
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."""
|
||||
return Presence(self, *args, **kwargs)
|
||||
|
||||
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.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value for this stanza thread.
|
||||
Defaults to 0.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
ito -- The destination JID for this stanza.
|
||||
itype -- The Iq's type, one of: get, set, result, or error.
|
||||
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 Iq stanza of type 'get'.
|
||||
|
||||
Optionally, a query element may be added.
|
||||
|
||||
Arguments:
|
||||
queryxmlns -- The namespace of the query to use.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
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 Iq stanza of type 'result' with the given ID value.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value. May use self.new_id().
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
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 Iq stanza of type 'set'.
|
||||
|
||||
Optionally, a substanza may be given to use as the
|
||||
stanza's payload.
|
||||
|
||||
Arguments:
|
||||
sub -- A stanza or XML object to use as the Iq's payload.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
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 Iq stanza of type 'error'.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value. May use self.new_id().
|
||||
type -- The type of the error, such as 'cancel' or 'modify'.
|
||||
Defaults to 'cancel'.
|
||||
condition -- The error condition.
|
||||
Defaults to 'feature-not-implemented'.
|
||||
text -- A message describing the cause of the error.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
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 Iq stanza to use the given
|
||||
query namespace.
|
||||
|
||||
Arguments:
|
||||
iq -- Optional Iq stanza to modify. A new
|
||||
stanza is created otherwise.
|
||||
xmlns -- The query's namespace.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from 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.
|
||||
|
||||
Arguments:
|
||||
iq -- Optional Iq stanza to modify. A new stanza
|
||||
is created otherwise.
|
||||
"""
|
||||
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 Message stanza.
|
||||
|
||||
Arguments:
|
||||
mto -- The recipient of the message.
|
||||
mbody -- The main contents of the message.
|
||||
msubject -- Optional subject for the message.
|
||||
mtype -- The message's type, such as 'chat' or 'groupchat'.
|
||||
mhtml -- Optional HTML body content.
|
||||
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.
|
||||
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):
|
||||
"""
|
||||
Create and initialize a new Presence stanza.
|
||||
|
||||
Arguments:
|
||||
pshow -- The presence's show value.
|
||||
pstatus -- The presence's status message.
|
||||
ppriority -- This connections' priority.
|
||||
pto -- The recipient of a directed presence.
|
||||
ptype -- The type of presence, such as 'subscribe'.
|
||||
pfrom -- The sender of the presence.
|
||||
"""
|
||||
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
|
||||
if pshow is not None:
|
||||
presence['type'] = pshow
|
||||
if pfrom is None:
|
||||
presence['from'] = self.boundjid.full
|
||||
presence['priority'] = ppriority
|
||||
presence['status'] = pstatus
|
||||
return presence
|
||||
|
||||
def send_message(self, mto, mbody, msubject=None, mtype=None,
|
||||
mhtml=None, mfrom=None, mnick=None):
|
||||
"""
|
||||
Create, initialize, and send a Message stanza.
|
||||
|
||||
|
||||
"""
|
||||
self.makeMessage(mto, mbody, msubject, mtype,
|
||||
mhtml, mfrom, mnick).send()
|
||||
|
||||
def send_presence(self, pshow=None, pstatus=None, ppriority=None,
|
||||
pto=None, pfrom=None, ptype=None):
|
||||
"""
|
||||
Create, initialize, and send a Presence stanza.
|
||||
|
||||
Arguments:
|
||||
pshow -- The presence's show value.
|
||||
pstatus -- The presence's status message.
|
||||
ppriority -- This connections' priority.
|
||||
pto -- The recipient of a directed presence.
|
||||
ptype -- The type of presence, such as 'subscribe'.
|
||||
pfrom -- The sender of the presence.
|
||||
"""
|
||||
self.makePresence(pshow, pstatus, ppriority, pto,
|
||||
ptype=ptype, pfrom=pfrom).send()
|
||||
# Unexpected errors may occur if
|
||||
if not self.sentpresence:
|
||||
self.event('sent_presence')
|
||||
self.sentpresence = True
|
||||
|
||||
def send_presence_subscription(self, pto, pfrom=None,
|
||||
ptype='subscribe', pnick=None):
|
||||
"""
|
||||
Create, initialize, and send a Presence stanza of type 'subscribe'.
|
||||
|
||||
Arguments:
|
||||
pto -- The recipient of a directed presence.
|
||||
pfrom -- The sender of the presence.
|
||||
ptype -- The type of presence. Defaults to 'subscribe'.
|
||||
pnick -- Nickname of the presence's sender.
|
||||
"""
|
||||
presence = self.makePresence(ptype=ptype,
|
||||
pfrom=pfrom,
|
||||
pto=self.getjidbare(pto))
|
||||
if pnick:
|
||||
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
|
||||
nick.text = pnick
|
||||
presence.append(nick)
|
||||
presence.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.full")
|
||||
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
|
||||
|
||||
def set_jid(self, jid):
|
||||
"""Rip a JID apart and claim it as our own."""
|
||||
log.debug("setting jid to %s" % jid)
|
||||
self.boundjid.full = jid
|
||||
|
||||
def getjidresource(self, fulljid):
|
||||
if '/' in fulljid:
|
||||
return fulljid.split('/', 1)[-1]
|
||||
else:
|
||||
return ''
|
||||
|
||||
def getjidbare(self, fulljid):
|
||||
return fulljid.split('/', 1)[0]
|
||||
|
||||
def _handle_disconnected(self, event):
|
||||
"""When disconnected, reset the roster"""
|
||||
self.roster = {}
|
||||
|
||||
def _handle_stream_error(self, error):
|
||||
self.event('stream_error', error)
|
||||
|
||||
def _handle_message(self, msg):
|
||||
"""Process incoming message stanzas."""
|
||||
self.event('message', msg)
|
||||
|
||||
def _handle_presence(self, presence):
|
||||
"""
|
||||
Process incoming presence stanzas.
|
||||
|
||||
Update the roster with presence information.
|
||||
"""
|
||||
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
|
||||
|
||||
# Strip the information from the stanza.
|
||||
jid = presence['from'].bare
|
||||
resource = presence['from'].resource
|
||||
show = presence['type']
|
||||
status = presence['status']
|
||||
priority = presence['priority']
|
||||
|
||||
was_offline = False
|
||||
got_online = False
|
||||
old_roster = self.roster.get(jid, {}).get(resource, {})
|
||||
|
||||
# Create a new roster entry if needed.
|
||||
if not jid in self.roster:
|
||||
self.roster[jid] = {'groups': [],
|
||||
'name': '',
|
||||
'subscription': 'none',
|
||||
'presence': {},
|
||||
'in_roster': False}
|
||||
|
||||
# Alias to simplify some references.
|
||||
connections = self.roster[jid].get('presence', {})
|
||||
|
||||
# Determine if the user has just come online.
|
||||
if not resource in connections:
|
||||
if show == 'available' or show in presence.showtypes:
|
||||
got_online = True
|
||||
was_offline = True
|
||||
connections[resource] = {}
|
||||
|
||||
if connections[resource].get('show', 'unavailable') == 'unavailable':
|
||||
was_offline = True
|
||||
|
||||
# Update the roster's state for this JID's resource.
|
||||
connections[resource] = {'show': show,
|
||||
'status': status,
|
||||
'priority': priority}
|
||||
|
||||
name = self.roster[jid].get('name', '')
|
||||
|
||||
# Remove unneeded state information after a resource
|
||||
# disconnects. Determine if this was the last connection
|
||||
# for the JID.
|
||||
if show == 'unavailable':
|
||||
log.debug("%s %s got offline" % (jid, resource))
|
||||
del connections[resource]
|
||||
|
||||
if not connections and \
|
||||
not self.roster[jid].get('in_roster', False):
|
||||
del self.roster[jid]
|
||||
if not was_offline:
|
||||
self.event("got_offline", presence)
|
||||
else:
|
||||
return False
|
||||
|
||||
name = '(%s) ' % name if name else ''
|
||||
|
||||
# Presence state has changed.
|
||||
self.event("changed_status", presence)
|
||||
if got_online:
|
||||
self.event("got_online", presence)
|
||||
log.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource,
|
||||
show, status))
|
||||
|
||||
def _handle_subscribe(self, presence):
|
||||
"""
|
||||
Automatically managage subscription requests.
|
||||
|
||||
Subscription behavior is controlled by the settings
|
||||
self.auto_authorize and self.auto_subscribe.
|
||||
|
||||
auto_auth auto_sub Result:
|
||||
True True Create bi-directional subsriptions.
|
||||
True False Create only directed subscriptions.
|
||||
False * Decline all subscriptions.
|
||||
None * Disable automatic handling and use
|
||||
a custom handler.
|
||||
"""
|
||||
presence.reply()
|
||||
presence['to'] = presence['to'].bare
|
||||
|
||||
# We are using trinary logic, so conditions have to be
|
||||
# more explicit than usual.
|
||||
if self.auto_authorize == True:
|
||||
presence['type'] = 'subscribed'
|
||||
presence.send()
|
||||
if self.auto_subscribe:
|
||||
presence['type'] = 'subscribe'
|
||||
presence.send()
|
||||
elif self.auto_authorize == False:
|
||||
presence['type'] = 'unsubscribed'
|
||||
presence.send()
|
||||
|
||||
# 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
|
||||
|
||||
483
sleekxmpp/clientxmpp.py
Normal file
483
sleekxmpp/clientxmpp.py
Normal file
@@ -0,0 +1,483 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import base64
|
||||
import sys
|
||||
import hashlib
|
||||
import random
|
||||
import threading
|
||||
|
||||
from sleekxmpp import plugins
|
||||
from sleekxmpp import stanza
|
||||
from sleekxmpp.basexmpp import BaseXMPP
|
||||
from sleekxmpp.stanza import Message, Presence, Iq
|
||||
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||
from sleekxmpp.xmlstream.matcher import *
|
||||
from sleekxmpp.xmlstream.handler import *
|
||||
|
||||
# Flag indicating if DNS SRV records are available for use.
|
||||
SRV_SUPPORT = True
|
||||
try:
|
||||
import dns.resolver
|
||||
except:
|
||||
SRV_SUPPORT = False
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientXMPP(BaseXMPP):
|
||||
|
||||
"""
|
||||
SleekXMPP's client class.
|
||||
|
||||
Use only for good, not for evil.
|
||||
|
||||
Attributes:
|
||||
|
||||
Methods:
|
||||
connect -- Overrides XMLStream.connect.
|
||||
del_roster_item -- Delete a roster item.
|
||||
get_roster -- Retrieve the roster from the server.
|
||||
register_feature -- Register a stream feature.
|
||||
update_roster -- Update a roster item.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, ssl=False, plugin_config={},
|
||||
plugin_whitelist=[], escape_quotes=True):
|
||||
"""
|
||||
Create a new SleekXMPP client.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the XMPP user account.
|
||||
password -- The password for the XMPP user account.
|
||||
ssl -- Deprecated.
|
||||
plugin_config -- A dictionary of plugin configurations.
|
||||
plugin_whitelist -- A list of approved plugins that will be loaded
|
||||
when calling register_plugins.
|
||||
escape_quotes -- Deprecated.
|
||||
"""
|
||||
BaseXMPP.__init__(self, 'jabber:client')
|
||||
|
||||
self.set_jid(jid)
|
||||
self.password = password
|
||||
self.escape_quotes = escape_quotes
|
||||
self.plugin_config = plugin_config
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.srv_support = SRV_SUPPORT
|
||||
|
||||
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
|
||||
self.boundjid.host,
|
||||
"xmlns:stream='%s'" % self.stream_ns,
|
||||
"xmlns='%s'" % self.default_ns)
|
||||
self.stream_footer = "</stream:stream>"
|
||||
|
||||
self.features = []
|
||||
self.registered_features = []
|
||||
|
||||
#TODO: Use stream state here
|
||||
self.authenticated = False
|
||||
self.sessionstarted = False
|
||||
self.bound = False
|
||||
self.bindfail = False
|
||||
self.add_event_handler('connected', self.handle_connected)
|
||||
|
||||
self.register_handler(
|
||||
Callback('Stream Features',
|
||||
MatchXPath('{%s}features' % self.stream_ns),
|
||||
self._handle_stream_features))
|
||||
self.register_handler(
|
||||
Callback('Roster Update',
|
||||
MatchXPath('{%s}iq/{%s}query' % (
|
||||
self.default_ns,
|
||||
'jabber:iq:roster')),
|
||||
self._handle_roster))
|
||||
|
||||
self.register_feature(
|
||||
"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
|
||||
self._handle_starttls, True)
|
||||
self.register_feature(
|
||||
"<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
|
||||
self._handle_sasl_auth, True)
|
||||
self.register_feature(
|
||||
"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
|
||||
self._handle_bind_resource)
|
||||
self.register_feature(
|
||||
"<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
|
||||
self._handle_start_session)
|
||||
|
||||
def handle_connected(self, event=None):
|
||||
#TODO: Use stream state here
|
||||
self.authenticated = False
|
||||
self.sessionstarted = False
|
||||
self.bound = False
|
||||
self.bindfail = False
|
||||
self.schedule("session timeout checker", 15,
|
||||
self._session_timeout_check)
|
||||
|
||||
def _session_timeout_check(self):
|
||||
if not self.session_started_event.isSet():
|
||||
log.debug("Session start has taken more than 15 seconds")
|
||||
self.disconnect(reconnect=self.auto_reconnect)
|
||||
|
||||
def connect(self, address=tuple(), reattempt=True, use_tls=True):
|
||||
"""
|
||||
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.
|
||||
|
||||
Arguments:
|
||||
address -- A tuple containing the server's host and port.
|
||||
reattempt -- If True, reattempt the connection if an
|
||||
error occurs. Defaults to True.
|
||||
use_tls -- Indicates if TLS should be used for the
|
||||
connection. Defaults to True.
|
||||
"""
|
||||
self.session_started_event.clear()
|
||||
if not address or len(address) < 2:
|
||||
if not self.srv_support:
|
||||
log.debug("Did not supply (address, port) to connect" + \
|
||||
" to and no SRV support is installed" + \
|
||||
" (http://www.dnspython.org)." + \
|
||||
" Continuing to attempt connection, using" + \
|
||||
" server hostname from JID.")
|
||||
else:
|
||||
log.debug("Since no address is supplied," + \
|
||||
"attempting SRV lookup.")
|
||||
try:
|
||||
xmpp_srv = "_xmpp-client._tcp.%s" % self.boundjid.host
|
||||
answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV)
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
log.debug("No appropriate SRV record found." + \
|
||||
" Using JID server name.")
|
||||
except (dns.exception.Timeout,):
|
||||
log.debug("DNS resolution timed out.")
|
||||
else:
|
||||
# Pick a random server, weighted by priority.
|
||||
|
||||
addresses = {}
|
||||
intmax = 0
|
||||
for answer in answers:
|
||||
intmax += answer.priority
|
||||
addresses[intmax] = (answer.target.to_text()[:-1],
|
||||
answer.port)
|
||||
#python3 returns a generator for dictionary keys
|
||||
priorities = [x for x in addresses.keys()]
|
||||
priorities.sort()
|
||||
|
||||
picked = random.randint(0, intmax)
|
||||
for priority in priorities:
|
||||
if picked <= priority:
|
||||
address = addresses[priority]
|
||||
break
|
||||
|
||||
if not address:
|
||||
# If all else fails, use the server from the JID.
|
||||
address = (self.boundjid.host, 5222)
|
||||
|
||||
return XMLStream.connect(self, address[0], address[1],
|
||||
use_tls=use_tls, reattempt=reattempt)
|
||||
|
||||
def register_feature(self, mask, pointer, breaker=False):
|
||||
"""
|
||||
Register a stream feature.
|
||||
|
||||
Arguments:
|
||||
mask -- An XML string matching the feature's element.
|
||||
pointer -- The function to execute if the feature is received.
|
||||
breaker -- Indicates if feature processing should halt with
|
||||
this feature. Defaults to False.
|
||||
"""
|
||||
self.registered_features.append((MatchXMLMask(mask),
|
||||
pointer,
|
||||
breaker))
|
||||
|
||||
def update_roster(self, jid, name=None, subscription=None, groups=[],
|
||||
block=True, timeout=None, callback=None):
|
||||
"""
|
||||
Add or change a roster item.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the entry to modify.
|
||||
name -- The user's nickname for this JID.
|
||||
subscription -- The subscription status. May be one of
|
||||
'to', 'from', 'both', or 'none'. If set
|
||||
to 'remove', the entry will be deleted.
|
||||
groups -- The roster groups that contain this item.
|
||||
block -- Specify if the roster request 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 continuing if blocking
|
||||
is used. Defaults to self.response_timeout.
|
||||
callback -- Optional reference to a stream handler function.
|
||||
Will be executed when the roster is received.
|
||||
Implies block=False.
|
||||
"""
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['roster']['items'] = {jid: {'name': name,
|
||||
'subscription': subscription,
|
||||
'groups': groups}}
|
||||
response = iq.send(block, timeout, callback)
|
||||
if response in [False, None] or not isinstance(response, Iq):
|
||||
return response
|
||||
return response['type'] == 'result'
|
||||
|
||||
def del_roster_item(self, jid):
|
||||
"""
|
||||
Remove an item from the roster by setting its subscription
|
||||
status to 'remove'.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the item to remove.
|
||||
"""
|
||||
return self.update_roster(jid, subscription='remove')
|
||||
|
||||
def get_roster(self, block=True, timeout=None, callback=None):
|
||||
"""
|
||||
Request the roster from the server.
|
||||
|
||||
Arguments:
|
||||
block -- Specify if the roster request 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 continuing if blocking is used.
|
||||
Defaults to self.response_timeout.
|
||||
callback -- Optional reference to a stream handler function. Will
|
||||
be executed when the roster is received.
|
||||
Implies block=False.
|
||||
"""
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq.enable('roster')
|
||||
response = iq.send(block, timeout, callback)
|
||||
|
||||
if response == False:
|
||||
self.event('roster_timeout')
|
||||
|
||||
if response in [False, None] or not isinstance(response, Iq):
|
||||
return response
|
||||
else:
|
||||
return self._handle_roster(response, request=True)
|
||||
|
||||
def _handle_stream_features(self, features):
|
||||
"""
|
||||
Process the received stream features.
|
||||
|
||||
Arguments:
|
||||
features -- The features stanza.
|
||||
"""
|
||||
# Record all of the features.
|
||||
self.features = []
|
||||
for sub in features.xml:
|
||||
self.features.append(sub.tag)
|
||||
|
||||
# Process the features.
|
||||
for sub in features.xml:
|
||||
for feature in self.registered_features:
|
||||
mask, handler, halt = feature
|
||||
if mask.match(sub):
|
||||
if handler(sub) and halt:
|
||||
# Don't continue if the feature was
|
||||
# marked as a breaker.
|
||||
return True
|
||||
|
||||
def _handle_starttls(self, xml):
|
||||
"""
|
||||
Handle notification that the server supports TLS.
|
||||
|
||||
Arguments:
|
||||
xml -- The STARTLS proceed element.
|
||||
"""
|
||||
if not self.use_tls:
|
||||
return False
|
||||
elif not self.authenticated and self.ssl_support:
|
||||
tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||
self.add_handler("<proceed xmlns='%s' />" % tls_ns,
|
||||
self._handle_tls_start,
|
||||
name='TLS Proceed',
|
||||
instream=True)
|
||||
self.send_xml(xml, now=True)
|
||||
return True
|
||||
else:
|
||||
log.warning("The module tlslite is required to log in" +\
|
||||
" to some servers, and has not been found.")
|
||||
return False
|
||||
|
||||
def _handle_tls_start(self, xml):
|
||||
"""
|
||||
Handle encrypting the stream using TLS.
|
||||
|
||||
Restarts the stream.
|
||||
"""
|
||||
log.debug("Starting TLS")
|
||||
if self.start_tls():
|
||||
raise RestartStream()
|
||||
|
||||
def _handle_sasl_auth(self, xml):
|
||||
"""
|
||||
Handle authenticating using SASL.
|
||||
|
||||
Arguments:
|
||||
xml -- The SASL mechanisms stanza.
|
||||
"""
|
||||
if self.use_tls and \
|
||||
'{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
|
||||
return False
|
||||
|
||||
log.debug("Starting SASL Auth")
|
||||
sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
self.add_handler("<success xmlns='%s' />" % sasl_ns,
|
||||
self._handle_auth_success,
|
||||
name='SASL Sucess',
|
||||
instream=True)
|
||||
self.add_handler("<failure xmlns='%s' />" % sasl_ns,
|
||||
self._handle_auth_fail,
|
||||
name='SASL Failure',
|
||||
instream=True)
|
||||
|
||||
sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
|
||||
if sasl_mechs:
|
||||
for sasl_mech in sasl_mechs:
|
||||
self.features.append("sasl:%s" % sasl_mech.text)
|
||||
if 'sasl:PLAIN' in self.features and self.boundjid.user:
|
||||
if sys.version_info < (3, 0):
|
||||
user = bytes(self.boundjid.user)
|
||||
password = bytes(self.password)
|
||||
else:
|
||||
user = bytes(self.boundjid.user, 'utf-8')
|
||||
password = bytes(self.password, 'utf-8')
|
||||
|
||||
auth = base64.b64encode(b'\x00' + user + \
|
||||
b'\x00' + password).decode('utf-8')
|
||||
|
||||
self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
|
||||
sasl_ns,
|
||||
auth),
|
||||
now=True)
|
||||
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
|
||||
self.send("<auth xmlns='%s' mechanism='%s' />" % (
|
||||
sasl_ns,
|
||||
'ANONYMOUS'),
|
||||
now=True)
|
||||
else:
|
||||
log.error("No appropriate login method.")
|
||||
self.disconnect()
|
||||
return True
|
||||
|
||||
def _handle_auth_success(self, xml):
|
||||
"""
|
||||
SASL authentication succeeded. Restart the stream.
|
||||
|
||||
Arguments:
|
||||
xml -- The SASL authentication success element.
|
||||
"""
|
||||
self.authenticated = True
|
||||
self.features = []
|
||||
raise RestartStream()
|
||||
|
||||
def _handle_auth_fail(self, xml):
|
||||
"""
|
||||
SASL authentication failed. Disconnect and shutdown.
|
||||
|
||||
Arguments:
|
||||
xml -- The SASL authentication failure element.
|
||||
"""
|
||||
log.info("Authentication failed.")
|
||||
self.event("failed_auth", direct=True)
|
||||
self.disconnect()
|
||||
|
||||
def _handle_bind_resource(self, xml):
|
||||
"""
|
||||
Handle requesting a specific resource.
|
||||
|
||||
Arguments:
|
||||
xml -- The bind feature element.
|
||||
"""
|
||||
log.debug("Requesting resource: %s" % self.boundjid.resource)
|
||||
xml.clear()
|
||||
iq = self.Iq(stype='set')
|
||||
if self.boundjid.resource:
|
||||
res = ET.Element('resource')
|
||||
res.text = self.boundjid.resource
|
||||
xml.append(res)
|
||||
iq.append(xml)
|
||||
response = iq.send(now=True)
|
||||
|
||||
bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
|
||||
self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
|
||||
bind_ns)).text)
|
||||
self.bound = True
|
||||
log.info("Node set to: %s" % self.boundjid.full)
|
||||
session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
|
||||
if "{%s}session" % session_ns not in self.features or self.bindfail:
|
||||
log.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.session_started_event.set()
|
||||
self.event("session_start")
|
||||
|
||||
def _handle_start_session(self, xml):
|
||||
"""
|
||||
Handle the start of the session.
|
||||
|
||||
Arguments:
|
||||
xml -- The session feature element.
|
||||
"""
|
||||
if self.authenticated and self.bound:
|
||||
iq = self.makeIqSet(xml)
|
||||
response = iq.send(now=True)
|
||||
log.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.session_started_event.set()
|
||||
self.event("session_start")
|
||||
else:
|
||||
# Bind probably hasn't happened yet.
|
||||
self.bindfail = True
|
||||
|
||||
def _handle_roster(self, iq, request=False):
|
||||
"""
|
||||
Update the roster after receiving a roster stanza.
|
||||
|
||||
Arguments:
|
||||
iq -- The roster stanza.
|
||||
request -- Indicates if this stanza is a response
|
||||
to a request for the roster.
|
||||
"""
|
||||
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
|
||||
for jid in iq['roster']['items']:
|
||||
if not jid in self.roster:
|
||||
self.roster[jid] = {'groups': [],
|
||||
'name': '',
|
||||
'subscription': 'none',
|
||||
'presence': {},
|
||||
'in_roster': True}
|
||||
self.roster[jid].update(iq['roster']['items'][jid])
|
||||
self.event('roster_received', iq)
|
||||
|
||||
self.event("roster_update", iq)
|
||||
if iq['type'] == 'set':
|
||||
iq.reply()
|
||||
iq.enable('roster')
|
||||
iq.send()
|
||||
return True
|
||||
|
||||
|
||||
# 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
|
||||
@@ -1,41 +0,0 @@
|
||||
import sleekxmpp.componentxmpp
|
||||
import logging
|
||||
from optparse import OptionParser
|
||||
import time
|
||||
|
||||
class Example(sleekxmpp.componentxmpp.ComponentXMPP):
|
||||
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'vm1', 5230)
|
||||
self.add_event_handler("session_start", self.start)
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def start(self, event):
|
||||
#self.getRoster()
|
||||
#self.sendPresence(pto='admin@tigase.netflint.net/sarkozy')
|
||||
#self.sendPresence(pto='tigase.netflint.net')
|
||||
pass
|
||||
|
||||
def message(self, event):
|
||||
self.sendMessage("%s/%s" % (event['jid'], event['resource']), "Thanks for sending me, \"%s\"." % event['message'], mtype=event['type'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
#parse command line arguements
|
||||
optp = OptionParser()
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
xmpp = Example('component.vm1', 'secreteating')
|
||||
xmpp.registerPlugin('xep_0004')
|
||||
xmpp.registerPlugin('xep_0030')
|
||||
xmpp.registerPlugin('xep_0060')
|
||||
xmpp.registerPlugin('xep_0199')
|
||||
if xmpp.connect():
|
||||
xmpp.process(threaded=False)
|
||||
print("done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
190
sleekxmpp/componentxmpp.py
Executable file → Normal file
190
sleekxmpp/componentxmpp.py
Executable file → Normal file
@@ -1,78 +1,142 @@
|
||||
#!/usr/bin/python2.5
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from . basexmpp import basexmpp
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
from . xmlstream.xmlstream import XMLStream
|
||||
from . xmlstream.xmlstream import RestartStream
|
||||
from . xmlstream.matcher.xmlmask import MatchXMLMask
|
||||
from . xmlstream.matcher.xpath import MatchXPath
|
||||
from . xmlstream.matcher.many import MatchMany
|
||||
from . xmlstream.handler.callback import Callback
|
||||
from . xmlstream.stanzabase import StanzaBase
|
||||
from . xmlstream import xmlstream as xmlstreammod
|
||||
import time
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import base64
|
||||
import sys
|
||||
import random
|
||||
import copy
|
||||
from . import plugins
|
||||
from . import stanza
|
||||
import hashlib
|
||||
srvsupport = True
|
||||
try:
|
||||
import dns.resolver
|
||||
except ImportError:
|
||||
srvsupport = False
|
||||
|
||||
from sleekxmpp import plugins
|
||||
from sleekxmpp import stanza
|
||||
from sleekxmpp.basexmpp import BaseXMPP
|
||||
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||
from sleekxmpp.xmlstream.matcher import *
|
||||
from sleekxmpp.xmlstream.handler import *
|
||||
|
||||
|
||||
class ComponentXMPP(basexmpp, XMLStream):
|
||||
"""SleekXMPP's client class. Use only for good, not evil."""
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False):
|
||||
XMLStream.__init__(self)
|
||||
if use_jc_ns:
|
||||
self.default_ns = 'jabber:client'
|
||||
else:
|
||||
self.default_ns = 'jabber:component:accept'
|
||||
basexmpp.__init__(self)
|
||||
self.auto_authorize = None
|
||||
self.stream_header = "<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>" % jid
|
||||
self.stream_footer = "</stream:stream>"
|
||||
self.server_host = host
|
||||
self.server_port = port
|
||||
self.set_jid(jid)
|
||||
self.secret = secret
|
||||
self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake))
|
||||
|
||||
def incoming_filter(self, xmlobj):
|
||||
if xmlobj.tag.startswith('{jabber:client}'):
|
||||
xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns)
|
||||
for sub in xmlobj:
|
||||
self.incoming_filter(sub)
|
||||
return xmlobj
|
||||
|
||||
def start_stream_handler(self, xml):
|
||||
sid = xml.get('id', '')
|
||||
handshake = ET.Element('{jabber:component:accept}handshake')
|
||||
if sys.version_info < (3,0):
|
||||
handshake.text = hashlib.sha1("%s%s" % (sid, self.secret)).hexdigest().lower()
|
||||
else:
|
||||
handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower()
|
||||
self.sendXML(handshake)
|
||||
|
||||
def _handleHandshake(self, xml):
|
||||
self.event("session_start")
|
||||
|
||||
def connect(self):
|
||||
logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port))
|
||||
return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port)
|
||||
class ComponentXMPP(BaseXMPP):
|
||||
|
||||
"""
|
||||
SleekXMPP's basic XMPP server component.
|
||||
|
||||
Use only for good, not for evil.
|
||||
|
||||
Methods:
|
||||
connect -- Overrides XMLStream.connect.
|
||||
incoming_filter -- Overrides XMLStream.incoming_filter.
|
||||
start_stream_handler -- Overrides XMLStream.start_stream_handler.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, secret, host, port,
|
||||
plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
|
||||
"""
|
||||
Arguments:
|
||||
jid -- The JID of the component.
|
||||
secret -- The secret or password for the component.
|
||||
host -- The server accepting the component.
|
||||
port -- The port used to connect to the server.
|
||||
plugin_config -- A dictionary of plugin configurations.
|
||||
plugin_whitelist -- A list of desired plugins to load
|
||||
when using register_plugins.
|
||||
use_js_ns -- Indicates if the 'jabber:client' namespace
|
||||
should be used instead of the standard
|
||||
'jabber:component:accept' namespace.
|
||||
Defaults to False.
|
||||
"""
|
||||
if use_jc_ns:
|
||||
default_ns = 'jabber:client'
|
||||
else:
|
||||
default_ns = 'jabber:component:accept'
|
||||
BaseXMPP.__init__(self, 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.set_jid(jid)
|
||||
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))
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connect to the server.
|
||||
|
||||
Overrides XMLStream.connect.
|
||||
"""
|
||||
log.debug("Connecting to %s:%s" % (self.server_host,
|
||||
self.server_port))
|
||||
return XMLStream.connect(self, self.server_host,
|
||||
self.server_port)
|
||||
|
||||
def incoming_filter(self, xml):
|
||||
"""
|
||||
Pre-process incoming XML stanzas by converting any 'jabber:client'
|
||||
namespaced elements to the component's default namespace.
|
||||
|
||||
Overrides XMLStream.incoming_filter.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML stanza to pre-process.
|
||||
"""
|
||||
if xml.tag.startswith('{jabber:client}'):
|
||||
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
|
||||
|
||||
# The incoming_filter call is only made on top level stanza
|
||||
# elements. So we manually continue filtering on sub-elements.
|
||||
for sub in xml:
|
||||
self.incoming_filter(sub)
|
||||
|
||||
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.
|
||||
|
||||
Overrides XMLStream.start_stream_handler.
|
||||
|
||||
Arguments:
|
||||
xml -- The incoming stream's root element.
|
||||
"""
|
||||
# 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.
|
||||
|
||||
Arguments:
|
||||
xml -- The reply handshake stanza.
|
||||
"""
|
||||
self.session_started_event.set()
|
||||
self.event("session_start")
|
||||
|
||||
@@ -3,14 +3,52 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
|
||||
class XMPPError(Exception):
|
||||
def __init__(self, condition='undefined-condition', text=None, etype=None, extension=None, extension_ns=None, extension_args=None):
|
||||
self.condition = condition
|
||||
self.text = text
|
||||
self.etype = etype
|
||||
self.extension = extension
|
||||
self.extension_ns = extension_ns
|
||||
self.extension_args = extension_args
|
||||
|
||||
"""
|
||||
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 RootStanza will create
|
||||
an error stanza and initialize any additional substanzas using the
|
||||
extension information included in the exception.
|
||||
|
||||
Meant for use in SleekXMPP plugins and applications using SleekXMPP.
|
||||
"""
|
||||
|
||||
def __init__(self, condition='undefined-condition', text=None, etype=None,
|
||||
extension=None, extension_ns=None, extension_args=None,
|
||||
clear=True):
|
||||
"""
|
||||
Create a new XMPPError exception.
|
||||
|
||||
Extension information can be included to add additional XML elements
|
||||
to the generated error stanza.
|
||||
|
||||
Arguments:
|
||||
condition -- The XMPP defined error condition.
|
||||
text -- Human readable text describing the error.
|
||||
etype -- The XMPP error type, such as cancel or modify.
|
||||
extension -- Tag name of the extension's XML content.
|
||||
extension_ns -- XML namespace of the extensions' XML content.
|
||||
extension_args -- Content and attributes for the extension
|
||||
element. Same as the additional arguments to
|
||||
the ET.Element constructor.
|
||||
clear -- Indicates if the stanza's contents should be
|
||||
removed before replying with an error.
|
||||
Defaults to 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
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060']
|
||||
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
|
||||
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086',
|
||||
'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify']
|
||||
|
||||
@@ -1,35 +1,90 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
|
||||
class base_plugin(object):
|
||||
|
||||
def __init__(self, xmpp, config):
|
||||
self.xep = 'base'
|
||||
self.description = 'Base Plugin'
|
||||
self.xmpp = xmpp
|
||||
self.config = config
|
||||
self.enable = config.get('enable', True)
|
||||
if self.enable:
|
||||
self.plugin_init()
|
||||
|
||||
def plugin_init(self):
|
||||
pass
|
||||
|
||||
def post_init(self):
|
||||
pass
|
||||
|
||||
"""
|
||||
The base_plugin class serves as a base for user created plugins
|
||||
that provide support for existing or experimental XEPS.
|
||||
|
||||
Each plugin has a dictionary for configuration options, as well
|
||||
as a name and description.
|
||||
|
||||
The lifecycle of a plugin is:
|
||||
1. The plugin is instantiated during registration.
|
||||
2. Once the XML stream begins processing, the method
|
||||
plugin_init() is called (if the plugin is configured
|
||||
as enabled with {'enable': True}).
|
||||
3. After all plugins have been initialized, the
|
||||
method post_init() is called.
|
||||
|
||||
Recommended event handlers:
|
||||
session_start -- Plugins which require the use of the current
|
||||
bound JID SHOULD wait for the session_start
|
||||
event to perform any initialization (or
|
||||
resetting). This is a transitive recommendation,
|
||||
plugins that use other plugins which use the
|
||||
bound JID should also wait for session_start
|
||||
before making such calls.
|
||||
session_end -- If the plugin keeps any per-session state,
|
||||
such as joined MUC rooms, such state SHOULD
|
||||
be cleared when the session_end event is raised.
|
||||
|
||||
Attributes:
|
||||
xep -- The XEP number the plugin implements, if any.
|
||||
description -- A short description of the plugin, typically
|
||||
the long name of the implemented XEP.
|
||||
xmpp -- The main SleekXMPP instance.
|
||||
config -- A dictionary of custom configuration values.
|
||||
The value 'enable' is special and controls
|
||||
whether or not the plugin is initialized
|
||||
after registration.
|
||||
post_initted -- Executed after all plugins have been initialized
|
||||
to handle any cross-plugin interactions, such as
|
||||
registering service discovery items.
|
||||
enable -- Indicates that the plugin is enabled for use and
|
||||
will be initialized after registration.
|
||||
|
||||
Methods:
|
||||
plugin_init -- Initialize the plugin state.
|
||||
post_init -- Handle any cross-plugin concerns.
|
||||
"""
|
||||
|
||||
def __init__(self, xmpp, config=None):
|
||||
"""
|
||||
Instantiate a new plugin and store the given configuration.
|
||||
|
||||
Arguments:
|
||||
xmpp -- The main SleekXMPP instance.
|
||||
config -- A dictionary of configuration values.
|
||||
"""
|
||||
if config is None:
|
||||
config = {}
|
||||
self.xep = 'base'
|
||||
self.description = 'Base Plugin'
|
||||
self.xmpp = xmpp
|
||||
self.config = config
|
||||
self.post_inited = False
|
||||
self.enable = config.get('enable', True)
|
||||
if self.enable:
|
||||
self.plugin_init()
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Initialize plugin state, such as registering any stream or
|
||||
event handlers, or new stanza types.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_init(self):
|
||||
"""
|
||||
Perform any cross-plugin interactions, such as registering
|
||||
service discovery identities or items.
|
||||
"""
|
||||
self.post_inited = True
|
||||
|
||||
@@ -1,57 +1,149 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
from . import base
|
||||
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
import traceback
|
||||
import time
|
||||
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):
|
||||
|
||||
def plugin_init(self):
|
||||
self.description = 'Google Talk Gmail Notification'
|
||||
self.xmpp.add_event_handler('sent_presence', self.handler_gmailcheck, threaded=True)
|
||||
self.emails = []
|
||||
|
||||
def handler_gmailcheck(self, payload):
|
||||
#TODO XEP 30 should cache results and have getFeature
|
||||
result = self.xmpp['xep_0030'].getInfo(self.xmpp.server)
|
||||
features = []
|
||||
for feature in result.findall('{http://jabber.org/protocol/disco#info}query/{http://jabber.org/protocol/disco#info}feature'):
|
||||
features.append(feature.get('var'))
|
||||
if 'google:mail:notify' in features:
|
||||
logging.debug("Server supports Gmail Notify")
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><new-mail xmlns='google:mail:notify' /></iq>" % self.xmpp.default_ns, self.handler_notify)
|
||||
self.getEmail()
|
||||
|
||||
def handler_notify(self, xml):
|
||||
logging.info("New Gmail recieved!")
|
||||
self.xmpp.event('gmail_notify')
|
||||
|
||||
def getEmail(self):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['to'] = self.xmpp.jid
|
||||
self.xmpp.makeIqQuery(iq, 'google:mail:notify')
|
||||
emails = self.xmpp.send(iq, self.xmpp.makeIq(self.xmpp.id))
|
||||
mailbox = emails.find('{google:mail:notify}mailbox')
|
||||
total = int(mailbox.get('total-matched', 0))
|
||||
logging.info("%s New Gmail Messages" % total)
|
||||
"""
|
||||
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()
|
||||
|
||||
49
sleekxmpp/plugins/jobs.py
Normal file
49
sleekxmpp/plugins/jobs.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class jobs(base.base_plugin):
|
||||
def plugin_init(self):
|
||||
self.xep = 'pubsubjob'
|
||||
self.description = "Job distribution over Pubsub"
|
||||
|
||||
def post_init(self):
|
||||
pass
|
||||
#TODO add event
|
||||
|
||||
def createJobNode(self, host, jid, node, config=None):
|
||||
pass
|
||||
|
||||
def createJob(self, host, node, jobid=None, payload=None):
|
||||
return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),))
|
||||
|
||||
def claimJob(self, host, node, jobid, ifrom=None):
|
||||
return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed'))
|
||||
|
||||
def unclaimJob(self, host, node, jobid):
|
||||
return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed'))
|
||||
|
||||
def finishJob(self, host, node, jobid, payload=None):
|
||||
finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished')
|
||||
if payload is not None:
|
||||
finished.append(payload)
|
||||
return self._setState(host, node, jobid, finished)
|
||||
|
||||
def _setState(self, host, node, jobid, state, ifrom=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['to'] = host
|
||||
if ifrom: iq['from'] = ifrom
|
||||
iq['type'] = 'set'
|
||||
iq['psstate']['node'] = node
|
||||
iq['psstate']['item'] = jobid
|
||||
iq['psstate']['payload'] = state
|
||||
result = iq.send()
|
||||
if result is None or type(result) == bool or result['type'] != 'result':
|
||||
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
|
||||
return False
|
||||
return True
|
||||
|
||||
421
sleekxmpp/plugins/old_0004.py
Normal file
421
sleekxmpp/plugins/old_0004.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
import copy
|
||||
import logging
|
||||
#TODO support item groups and results
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class old_0004(base.base_plugin):
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0004'
|
||||
self.description = '*Deprecated Data Forms'
|
||||
self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form')
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
|
||||
log.warning("This implementation of XEP-0004 is deprecated.")
|
||||
|
||||
def handler_message_xform(self, xml):
|
||||
object = self.handle_form(xml)
|
||||
self.xmpp.event("message_form", object)
|
||||
|
||||
def handler_presence_xform(self, xml):
|
||||
object = self.handle_form(xml)
|
||||
self.xmpp.event("presence_form", object)
|
||||
|
||||
def handle_form(self, xml):
|
||||
xmlform = xml.find('{jabber:x:data}x')
|
||||
object = self.buildForm(xmlform)
|
||||
self.xmpp.event("message_xform", object)
|
||||
return object
|
||||
|
||||
def buildForm(self, xml):
|
||||
form = Form(ftype=xml.attrib['type'])
|
||||
form.fromXML(xml)
|
||||
return form
|
||||
|
||||
def makeForm(self, ftype='form', title='', instructions=''):
|
||||
return Form(self.xmpp, ftype, title, instructions)
|
||||
|
||||
class FieldContainer(object):
|
||||
def __init__(self, stanza = 'form'):
|
||||
self.fields = []
|
||||
self.field = {}
|
||||
self.stanza = stanza
|
||||
|
||||
def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
|
||||
self.field[var] = FormField(var, ftype, label, desc, required, value)
|
||||
self.fields.append(self.field[var])
|
||||
return self.field[var]
|
||||
|
||||
def buildField(self, xml):
|
||||
self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
|
||||
self.fields.append(self.field[xml.get('var', '__unnamed__')])
|
||||
self.field[xml.get('var', '__unnamed__')].buildField(xml)
|
||||
|
||||
def buildContainer(self, xml):
|
||||
self.stanza = xml.tag
|
||||
for field in xml.findall('{jabber:x:data}field'):
|
||||
self.buildField(field)
|
||||
|
||||
def getXML(self, ftype):
|
||||
container = ET.Element(self.stanza)
|
||||
for field in self.fields:
|
||||
container.append(field.getXML(ftype))
|
||||
return container
|
||||
|
||||
class Form(FieldContainer):
|
||||
types = ('form', 'submit', 'cancel', 'result')
|
||||
def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
|
||||
if not ftype in self.types:
|
||||
raise ValueError("Invalid Form Type")
|
||||
FieldContainer.__init__(self)
|
||||
self.xmpp = xmpp
|
||||
self.type = ftype
|
||||
self.title = title
|
||||
self.instructions = instructions
|
||||
self.reported = []
|
||||
self.items = []
|
||||
|
||||
def merge(self, form2):
|
||||
form1 = Form(ftype=self.type)
|
||||
form1.fromXML(self.getXML(self.type))
|
||||
for field in form2.fields:
|
||||
if not field.var in form1.field:
|
||||
form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
|
||||
else:
|
||||
form1.field[field.var].value = field.value
|
||||
for option, label in field.options:
|
||||
if (option, label) not in form1.field[field.var].options:
|
||||
form1.fields[field.var].addOption(option, label)
|
||||
return form1
|
||||
|
||||
def copy(self):
|
||||
newform = Form(ftype=self.type)
|
||||
newform.fromXML(self.getXML(self.type))
|
||||
return newform
|
||||
|
||||
def update(self, form):
|
||||
values = form.getValues()
|
||||
for var in values:
|
||||
if var in self.fields:
|
||||
self.fields[var].setValue(self.fields[var])
|
||||
|
||||
def getValues(self):
|
||||
result = {}
|
||||
for field in self.fields:
|
||||
value = field.value
|
||||
if len(value) == 1:
|
||||
value = value[0]
|
||||
result[field.var] = value
|
||||
return result
|
||||
|
||||
def setValues(self, values={}):
|
||||
for field in values:
|
||||
if field in self.field:
|
||||
if isinstance(values[field], list) or isinstance(values[field], tuple):
|
||||
for value in values[field]:
|
||||
self.field[field].setValue(value)
|
||||
else:
|
||||
self.field[field].setValue(values[field])
|
||||
|
||||
def fromXML(self, xml):
|
||||
self.buildForm(xml)
|
||||
|
||||
def addItem(self):
|
||||
newitem = FieldContainer('item')
|
||||
self.items.append(newitem)
|
||||
return newitem
|
||||
|
||||
def buildItem(self, xml):
|
||||
newitem = self.addItem()
|
||||
newitem.buildContainer(xml)
|
||||
|
||||
def addReported(self):
|
||||
reported = FieldContainer('reported')
|
||||
self.reported.append(reported)
|
||||
return reported
|
||||
|
||||
def buildReported(self, xml):
|
||||
reported = self.addReported()
|
||||
reported.buildContainer(xml)
|
||||
|
||||
def setTitle(self, title):
|
||||
self.title = title
|
||||
|
||||
def setInstructions(self, instructions):
|
||||
self.instructions = instructions
|
||||
|
||||
def setType(self, ftype):
|
||||
self.type = ftype
|
||||
|
||||
def getXMLMessage(self, to):
|
||||
msg = self.xmpp.makeMessage(to)
|
||||
msg.append(self.getXML())
|
||||
return msg
|
||||
|
||||
def buildForm(self, xml):
|
||||
self.type = xml.get('type', 'form')
|
||||
if xml.find('{jabber:x:data}title') is not None:
|
||||
self.setTitle(xml.find('{jabber:x:data}title').text)
|
||||
if xml.find('{jabber:x:data}instructions') is not None:
|
||||
self.setInstructions(xml.find('{jabber:x:data}instructions').text)
|
||||
for field in xml.findall('{jabber:x:data}field'):
|
||||
self.buildField(field)
|
||||
for reported in xml.findall('{jabber:x:data}reported'):
|
||||
self.buildReported(reported)
|
||||
for item in xml.findall('{jabber:x:data}item'):
|
||||
self.buildItem(item)
|
||||
|
||||
#def getXML(self, tostring = False):
|
||||
def getXML(self, ftype=None):
|
||||
if ftype:
|
||||
self.type = ftype
|
||||
form = ET.Element('{jabber:x:data}x')
|
||||
form.attrib['type'] = self.type
|
||||
if self.title and self.type in ('form', 'result'):
|
||||
title = ET.Element('{jabber:x:data}title')
|
||||
title.text = self.title
|
||||
form.append(title)
|
||||
if self.instructions and self.type == 'form':
|
||||
instructions = ET.Element('{jabber:x:data}instructions')
|
||||
instructions.text = self.instructions
|
||||
form.append(instructions)
|
||||
for field in self.fields:
|
||||
form.append(field.getXML(self.type))
|
||||
for reported in self.reported:
|
||||
form.append(reported.getXML('{jabber:x:data}reported'))
|
||||
for item in self.items:
|
||||
form.append(item.getXML(self.type))
|
||||
#if tostring:
|
||||
# form = self.xmpp.tostring(form)
|
||||
return form
|
||||
|
||||
def getXHTML(self):
|
||||
form = ET.Element('{http://www.w3.org/1999/xhtml}form')
|
||||
if self.title:
|
||||
title = ET.Element('h2')
|
||||
title.text = self.title
|
||||
form.append(title)
|
||||
if self.instructions:
|
||||
instructions = ET.Element('p')
|
||||
instructions.text = self.instructions
|
||||
form.append(instructions)
|
||||
for field in self.fields:
|
||||
form.append(field.getXHTML())
|
||||
for field in self.reported:
|
||||
form.append(field.getXHTML())
|
||||
for field in self.items:
|
||||
form.append(field.getXHTML())
|
||||
return form
|
||||
|
||||
|
||||
def makeSubmit(self):
|
||||
self.setType('submit')
|
||||
|
||||
class FormField(object):
|
||||
types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
|
||||
listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
|
||||
lbtypes = ('fixed', 'text-multi')
|
||||
def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
|
||||
if not ftype in self.types:
|
||||
raise ValueError("Invalid Field Type")
|
||||
self.type = ftype
|
||||
self.var = var
|
||||
self.label = label
|
||||
self.desc = desc
|
||||
self.options = []
|
||||
self.required = False
|
||||
self.value = []
|
||||
if self.type in self.listtypes:
|
||||
self.islist = True
|
||||
else:
|
||||
self.islist = False
|
||||
if self.type in self.lbtypes:
|
||||
self.islinebreak = True
|
||||
else:
|
||||
self.islinebreak = False
|
||||
if value:
|
||||
self.setValue(value)
|
||||
|
||||
def addOption(self, value, label):
|
||||
if self.islist:
|
||||
self.options.append((value, label))
|
||||
else:
|
||||
raise ValueError("Cannot add options to non-list type field.")
|
||||
|
||||
def setTrue(self):
|
||||
if self.type == 'boolean':
|
||||
self.value = [True]
|
||||
|
||||
def setFalse(self):
|
||||
if self.type == 'boolean':
|
||||
self.value = [False]
|
||||
|
||||
def require(self):
|
||||
self.required = True
|
||||
|
||||
def setDescription(self, desc):
|
||||
self.desc = desc
|
||||
|
||||
def setValue(self, value):
|
||||
if self.type == 'boolean':
|
||||
if value in ('1', 1, True, 'true', 'True', 'yes'):
|
||||
value = True
|
||||
else:
|
||||
value = False
|
||||
if self.islinebreak and value is not None:
|
||||
self.value += value.split('\n')
|
||||
else:
|
||||
if len(self.value) and (not self.islist or self.type == 'list-single'):
|
||||
self.value = [value]
|
||||
else:
|
||||
self.value.append(value)
|
||||
|
||||
def delValue(self, value):
|
||||
if type(self.value) == type([]):
|
||||
try:
|
||||
idx = self.value.index(value)
|
||||
if idx != -1:
|
||||
self.value.pop(idx)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.value = ''
|
||||
|
||||
def setAnswer(self, value):
|
||||
self.setValue(value)
|
||||
|
||||
def buildField(self, xml):
|
||||
self.type = xml.get('type', 'text-single')
|
||||
self.label = xml.get('label', '')
|
||||
for option in xml.findall('{jabber:x:data}option'):
|
||||
self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
|
||||
for value in xml.findall('{jabber:x:data}value'):
|
||||
self.setValue(value.text)
|
||||
if xml.find('{jabber:x:data}required') is not None:
|
||||
self.require()
|
||||
if xml.find('{jabber:x:data}desc') is not None:
|
||||
self.setDescription(xml.find('{jabber:x:data}desc').text)
|
||||
|
||||
def getXML(self, ftype):
|
||||
field = ET.Element('{jabber:x:data}field')
|
||||
if ftype != 'result':
|
||||
field.attrib['type'] = self.type
|
||||
if self.type != 'fixed':
|
||||
if self.var:
|
||||
field.attrib['var'] = self.var
|
||||
if self.label:
|
||||
field.attrib['label'] = self.label
|
||||
if ftype == 'form':
|
||||
for option in self.options:
|
||||
optionxml = ET.Element('{jabber:x:data}option')
|
||||
optionxml.attrib['label'] = option[1]
|
||||
optionval = ET.Element('{jabber:x:data}value')
|
||||
optionval.text = option[0]
|
||||
optionxml.append(optionval)
|
||||
field.append(optionxml)
|
||||
if self.required:
|
||||
required = ET.Element('{jabber:x:data}required')
|
||||
field.append(required)
|
||||
if self.desc:
|
||||
desc = ET.Element('{jabber:x:data}desc')
|
||||
desc.text = self.desc
|
||||
field.append(desc)
|
||||
for value in self.value:
|
||||
valuexml = ET.Element('{jabber:x:data}value')
|
||||
if value is True or value is False:
|
||||
if value:
|
||||
valuexml.text = '1'
|
||||
else:
|
||||
valuexml.text = '0'
|
||||
else:
|
||||
valuexml.text = value
|
||||
field.append(valuexml)
|
||||
return field
|
||||
|
||||
def getXHTML(self):
|
||||
field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
|
||||
if self.label:
|
||||
label = ET.Element('p')
|
||||
label.text = "%s: " % self.label
|
||||
else:
|
||||
label = ET.Element('p')
|
||||
label.text = "%s: " % self.var
|
||||
field.append(label)
|
||||
if self.type == 'boolean':
|
||||
formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
|
||||
if len(self.value) and self.value[0] in (True, 'true', '1'):
|
||||
formf.attrib['checked'] = 'checked'
|
||||
elif self.type == 'fixed':
|
||||
formf = ET.Element('p')
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
field.append(formf)
|
||||
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
elif self.type == 'hidden':
|
||||
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
elif self.type in ('jid-multi', 'list-multi'):
|
||||
formf = ET.Element('select', {'name': self.var})
|
||||
for option in self.options:
|
||||
optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
|
||||
optf.text = option[1]
|
||||
if option[1] in self.value:
|
||||
optf.attrib['selected'] = 'selected'
|
||||
formf.append(option)
|
||||
elif self.type in ('jid-single', 'text-single'):
|
||||
formf = ET.Element('input', {'type': 'text', 'name': self.var})
|
||||
try:
|
||||
formf.attrib['value'] = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
elif self.type == 'list-single':
|
||||
formf = ET.Element('select', {'name': self.var})
|
||||
for option in self.options:
|
||||
optf = ET.Element('option', {'value': option[0]})
|
||||
optf.text = option[1]
|
||||
if not optf.text:
|
||||
optf.text = option[0]
|
||||
if option[1] in self.value:
|
||||
optf.attrib['selected'] = 'selected'
|
||||
formf.append(optf)
|
||||
elif self.type == 'text-multi':
|
||||
formf = ET.Element('textarea', {'name': self.var})
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
if not formf.text:
|
||||
formf.text = ' '
|
||||
elif self.type == 'text-private':
|
||||
formf = ET.Element('input', {'type': 'password', 'name': self.var})
|
||||
try:
|
||||
formf.attrib['value'] = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
label.append(formf)
|
||||
return field
|
||||
|
||||
@@ -178,15 +178,19 @@ class xep_0009(base.base_plugin):
|
||||
def plugin_init(self):
|
||||
self.xep = '0009'
|
||||
self.description = 'Jabber-RPC'
|
||||
self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>", self._callMethod)
|
||||
self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>", self._callResult)
|
||||
self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>", self._callError)
|
||||
self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
|
||||
self._callMethod, name='Jabber RPC Call')
|
||||
self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
|
||||
self._callResult, name='Jabber RPC Result')
|
||||
self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
|
||||
self._callError, name='Jabber RPC Error')
|
||||
self.entries = {}
|
||||
self.activeCalls = []
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
|
||||
self.xmpp['xep_0030'].add_identity('automatition','rpc')
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
|
||||
self.xmpp.plugin['xep_0030'].add_identity('automatition','rpc')
|
||||
|
||||
def register_call(self, method, name=None):
|
||||
#@returns an string that can be used in acl commands.
|
||||
@@ -1,30 +1,17 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
import traceback
|
||||
import time
|
||||
|
||||
class xep_0050(base.base_plugin):
|
||||
class old_0050(base.base_plugin):
|
||||
"""
|
||||
XEP-0050 Ad-Hoc Commands
|
||||
"""
|
||||
@@ -32,16 +19,17 @@ class xep_0050(base.base_plugin):
|
||||
def plugin_init(self):
|
||||
self.xep = '0050'
|
||||
self.description = 'Ad-Hoc Commands'
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command)
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command)
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, threaded=True)
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel)
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete)
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None')
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute')
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True)
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel')
|
||||
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete')
|
||||
self.commands = {}
|
||||
self.sessions = {}
|
||||
self.sd = self.xmpp.plugin['xep_0030']
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.sd.add_feature('http://jabber.org/protocol/commands')
|
||||
|
||||
def addCommand(self, node, name, form, pointer=None, multi=False):
|
||||
@@ -82,7 +70,7 @@ class xep_0050(base.base_plugin):
|
||||
in_command = xml.find('{http://jabber.org/protocol/commands}command')
|
||||
sessionid = in_command.get('sessionid', None)
|
||||
pointer = self.sessions[sessionid]['next']
|
||||
results = self.xmpp.plugin['xep_0004'].makeForm('result')
|
||||
results = self.xmpp.plugin['old_0004'].makeForm('result')
|
||||
results.fromXML(in_command.find('{jabber:x:data}x'))
|
||||
pointer(results,sessionid)
|
||||
self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[]))
|
||||
@@ -93,7 +81,7 @@ class xep_0050(base.base_plugin):
|
||||
in_command = xml.find('{http://jabber.org/protocol/commands}command')
|
||||
sessionid = in_command.get('sessionid', None)
|
||||
pointer = self.sessions[sessionid]['next']
|
||||
results = self.xmpp.plugin['xep_0004'].makeForm('result')
|
||||
results = self.xmpp.plugin['old_0004'].makeForm('result')
|
||||
results.fromXML(in_command.find('{jabber:x:data}x'))
|
||||
form, npointer, next = pointer(results,sessionid)
|
||||
self.sessions[sessionid]['next'] = npointer
|
||||
@@ -122,7 +110,7 @@ class xep_0050(base.base_plugin):
|
||||
if not id:
|
||||
id = self.xmpp.getNewId()
|
||||
iq = self.xmpp.makeIqResult(id)
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
iq.attrib['to'] = to
|
||||
command = ET.Element('{http://jabber.org/protocol/commands}command')
|
||||
command.attrib['node'] = node
|
||||
@@ -1,12 +1,44 @@
|
||||
from .. xmlstream.stanzabase import ElementBase, ET, JID
|
||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||
from .. stanza.iq import Iq
|
||||
from .. stanza.message import Message
|
||||
from .. basexmpp import basexmpp
|
||||
from .. xmlstream.xmlstream import XMLStream
|
||||
import logging
|
||||
from . import xep_0004
|
||||
|
||||
def stanzaPlugin(stanza, plugin):
|
||||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
|
||||
|
||||
class PubsubState(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/psstate'
|
||||
name = 'state'
|
||||
plugin_attrib = 'psstate'
|
||||
interfaces = set(('node', 'item', 'payload'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def setPayload(self, value):
|
||||
self.xml.append(value)
|
||||
|
||||
def getPayload(self):
|
||||
childs = self.xml.getchildren()
|
||||
if len(childs) > 0:
|
||||
return childs[0]
|
||||
|
||||
def delPayload(self):
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
|
||||
registerStanzaPlugin(Iq, PubsubState)
|
||||
|
||||
class PubsubStateEvent(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/psstate#event'
|
||||
name = 'event'
|
||||
plugin_attrib = 'psstate_event'
|
||||
intefaces = set(tuple())
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(Message, PubsubStateEvent)
|
||||
registerStanzaPlugin(PubsubStateEvent, PubsubState)
|
||||
|
||||
class Pubsub(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -16,7 +48,7 @@ class Pubsub(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Iq, Pubsub)
|
||||
registerStanzaPlugin(Iq, Pubsub)
|
||||
|
||||
class PubsubOwner(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -26,7 +58,7 @@ class PubsubOwner(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Iq, PubsubOwner)
|
||||
registerStanzaPlugin(Iq, PubsubOwner)
|
||||
|
||||
class Affiliation(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -43,7 +75,7 @@ class Affiliations(ElementBase):
|
||||
interfaces = set(tuple())
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
subitem = Affiliation
|
||||
subitem = (Affiliation,)
|
||||
|
||||
def append(self, affiliation):
|
||||
if not isinstance(affiliation, Affiliation):
|
||||
@@ -51,7 +83,7 @@ class Affiliations(ElementBase):
|
||||
self.xml.append(affiliation.xml)
|
||||
return self.iterables.append(affiliation)
|
||||
|
||||
stanzaPlugin(Pubsub, Affiliations)
|
||||
registerStanzaPlugin(Pubsub, Affiliations)
|
||||
|
||||
|
||||
class Subscription(ElementBase):
|
||||
@@ -62,13 +94,13 @@ class Subscription(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def setJid(self, value):
|
||||
self._setAttr('jid', str(value))
|
||||
def setjid(self, value):
|
||||
self._setattr('jid', str(value))
|
||||
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
def getjid(self):
|
||||
return jid(self._getattr('jid'))
|
||||
|
||||
stanzaPlugin(Pubsub, Subscription)
|
||||
registerStanzaPlugin(Pubsub, Subscription)
|
||||
|
||||
class Subscriptions(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -77,42 +109,9 @@ class Subscriptions(ElementBase):
|
||||
interfaces = set(tuple())
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
subitem = Subscription
|
||||
subitem = (Subscription,)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
ElementBase.__init__(self, *args, **kwargs)
|
||||
self.subscriptions = []
|
||||
self.idx = 0
|
||||
|
||||
def __iter__(self):
|
||||
self.idx = 0
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
self.idx += 1
|
||||
if self.idx + 1 > len(self.subscriptions):
|
||||
self.idx = 0
|
||||
raise StopIteration
|
||||
return self.subscriptions[self.idx]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.subscriptions)
|
||||
|
||||
def append(self, subscription):
|
||||
if not isinstance(subscription, Subscription):
|
||||
raise TypeError
|
||||
self.xml.append(subscription.xml)
|
||||
return self.subscriptions.append(subscription)
|
||||
|
||||
def pop(self, idx=0):
|
||||
aff = self.subscriptions.pop(idx)
|
||||
self.xml.remove(aff.xml)
|
||||
return aff
|
||||
|
||||
def find(self, subscription):
|
||||
return self.subscriptions.find(subscription)
|
||||
|
||||
stanzaPlugin(Pubsub, Subscriptions)
|
||||
registerStanzaPlugin(Pubsub, Subscriptions)
|
||||
|
||||
class OptionalSetting(object):
|
||||
interfaces = set(('required',))
|
||||
@@ -145,7 +144,7 @@ class SubscribeOptions(ElementBase, OptionalSetting):
|
||||
plugin_tag_map = {}
|
||||
interfaces = set(('required',))
|
||||
|
||||
stanzaPlugin(Subscription, SubscribeOptions)
|
||||
registerStanzaPlugin(Subscription, SubscribeOptions)
|
||||
|
||||
class Item(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -171,12 +170,12 @@ class Items(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
name = 'items'
|
||||
plugin_attrib = 'items'
|
||||
interfaces = set(tuple())
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
subitem = Item
|
||||
subitem = (Item,)
|
||||
|
||||
stanzaPlugin(Pubsub, Items)
|
||||
registerStanzaPlugin(Pubsub, Items)
|
||||
|
||||
class Create(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -186,7 +185,7 @@ class Create(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Pubsub, Create)
|
||||
registerStanzaPlugin(Pubsub, Create)
|
||||
|
||||
#class Default(ElementBase):
|
||||
# namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -201,7 +200,7 @@ stanzaPlugin(Pubsub, Create)
|
||||
# if not t: t == 'leaf'
|
||||
# return t
|
||||
#
|
||||
#stanzaPlugin(Pubsub, Default)
|
||||
#registerStanzaPlugin(Pubsub, Default)
|
||||
|
||||
class Publish(Items):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -210,9 +209,9 @@ class Publish(Items):
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
subitem = Item
|
||||
subitem = (Item,)
|
||||
|
||||
stanzaPlugin(Pubsub, Publish)
|
||||
registerStanzaPlugin(Pubsub, Publish)
|
||||
|
||||
class Retract(Items):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -222,7 +221,7 @@ class Retract(Items):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Pubsub, Retract)
|
||||
registerStanzaPlugin(Pubsub, Retract)
|
||||
|
||||
class Unsubscribe(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -236,7 +235,9 @@ class Unsubscribe(ElementBase):
|
||||
self._setAttr('jid', str(value))
|
||||
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('from'))
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
registerStanzaPlugin(Pubsub, Unsubscribe)
|
||||
|
||||
class Subscribe(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -252,13 +253,13 @@ class Subscribe(ElementBase):
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
stanzaPlugin(Pubsub, Subscribe)
|
||||
registerStanzaPlugin(Pubsub, Subscribe)
|
||||
|
||||
class Configure(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
name = 'configure'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node', 'type', 'config'))
|
||||
interfaces = set(('node', 'type'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
@@ -267,26 +268,11 @@ class Configure(ElementBase):
|
||||
if not t: t == 'leaf'
|
||||
return t
|
||||
|
||||
def getConfig(self):
|
||||
config = self.xml.find('{jabber:x:data}x')
|
||||
form = xep_0004.Form()
|
||||
if config is not None:
|
||||
form.fromXML(config)
|
||||
return form
|
||||
|
||||
def setConfig(self, value):
|
||||
self.xml.append(value.getXML())
|
||||
return self
|
||||
|
||||
def delConfig(self):
|
||||
config = self.xml.find('{jabber:x:data}x')
|
||||
self.xml.remove(config)
|
||||
|
||||
stanzaPlugin(Pubsub, Configure)
|
||||
stanzaPlugin(Create, Configure)
|
||||
registerStanzaPlugin(Pubsub, Configure)
|
||||
registerStanzaPlugin(Configure, xep_0004.Form)
|
||||
|
||||
class DefaultConfig(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
name = 'default'
|
||||
plugin_attrib = 'default'
|
||||
interfaces = set(('node', 'type', 'config'))
|
||||
@@ -295,28 +281,21 @@ class DefaultConfig(ElementBase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
ElementBase.__init__(self, *args, **kwargs)
|
||||
|
||||
def getConfig(self):
|
||||
config = self.xml.find('{jabber:x:data}x')
|
||||
form = xep_0004.Form()
|
||||
if config is not None:
|
||||
form.fromXML(config)
|
||||
return form
|
||||
|
||||
def setConfig(self, value):
|
||||
self.xml.append(value.getXML())
|
||||
return self
|
||||
|
||||
def delConfig(self):
|
||||
config = self.xml.find('{jabber:x:data}x')
|
||||
self.xml.remove(config)
|
||||
|
||||
def getType(self):
|
||||
t = self._getAttr('type')
|
||||
if not t: t == 'leaf'
|
||||
if not t: t = 'leaf'
|
||||
return t
|
||||
|
||||
def getConfig(self):
|
||||
return self['form']
|
||||
|
||||
def setConfig(self, value):
|
||||
self['form'].setStanzaValues(value.getStanzaValues())
|
||||
return self
|
||||
|
||||
stanzaPlugin(Pubsub, DefaultConfig)
|
||||
registerStanzaPlugin(PubsubOwner, DefaultConfig)
|
||||
registerStanzaPlugin(DefaultConfig, xep_0004.Form)
|
||||
|
||||
class Options(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -350,21 +329,9 @@ class Options(ElementBase):
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
stanzaPlugin(Pubsub, Options)
|
||||
stanzaPlugin(Subscribe, Options)
|
||||
registerStanzaPlugin(Pubsub, Options)
|
||||
registerStanzaPlugin(Subscribe, Options)
|
||||
|
||||
#iq = Iq()
|
||||
#iq['pubsub']['defaultconfig']
|
||||
#print(iq)
|
||||
|
||||
#from xml.etree import cElementTree as ET
|
||||
#iq = Iq()
|
||||
#item = Item()
|
||||
#item['payload'] = ET.Element("{http://netflint.net/p/crap}stupidshit")
|
||||
#item['id'] = 'aa11bbcc'
|
||||
#iq['pubsub']['items'].append(item)
|
||||
#print(iq)
|
||||
|
||||
class OwnerAffiliations(Affiliations):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
interfaces = set(('node'))
|
||||
@@ -377,7 +344,7 @@ class OwnerAffiliations(Affiliations):
|
||||
self.xml.append(affiliation.xml)
|
||||
return self.affiliations.append(affiliation)
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerAffiliations)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
|
||||
|
||||
class OwnerAffiliation(Affiliation):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -391,15 +358,23 @@ class OwnerConfigure(Configure):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerConfigure)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
|
||||
|
||||
class OwnerDefault(OwnerConfigure):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
interfaces = set(('node', 'config'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def getConfig(self):
|
||||
return self['form']
|
||||
|
||||
def setConfig(self, value):
|
||||
self['form'].setStanzaValues(value.getStanzaValues())
|
||||
return self
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerDefault)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerDefault)
|
||||
registerStanzaPlugin(OwnerDefault, xep_0004.Form)
|
||||
|
||||
class OwnerDelete(ElementBase, OptionalSetting):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -407,8 +382,9 @@ class OwnerDelete(ElementBase, OptionalSetting):
|
||||
plugin_attrib = 'delete'
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
interfaces = set(('node',))
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerDelete)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerDelete)
|
||||
|
||||
class OwnerPurge(ElementBase, OptionalSetting):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -417,7 +393,7 @@ class OwnerPurge(ElementBase, OptionalSetting):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerPurge)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerPurge)
|
||||
|
||||
class OwnerRedirect(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -431,9 +407,9 @@ class OwnerRedirect(ElementBase):
|
||||
self._setAttr('jid', str(value))
|
||||
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('from'))
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
stanzaPlugin(OwnerDelete, OwnerRedirect)
|
||||
registerStanzaPlugin(OwnerDelete, OwnerRedirect)
|
||||
|
||||
class OwnerSubscriptions(Subscriptions):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -447,7 +423,7 @@ class OwnerSubscriptions(Subscriptions):
|
||||
self.xml.append(subscription.xml)
|
||||
return self.subscriptions.append(subscription)
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerSubscriptions)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
|
||||
|
||||
class OwnerSubscription(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -462,3 +438,120 @@ class OwnerSubscription(ElementBase):
|
||||
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('from'))
|
||||
|
||||
class Event(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'event'
|
||||
plugin_attrib = 'pubsub_event'
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(Message, Event)
|
||||
|
||||
class EventItem(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'item'
|
||||
plugin_attrib = 'item'
|
||||
interfaces = set(('id', 'payload'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def setPayload(self, value):
|
||||
self.xml.append(value)
|
||||
|
||||
def getPayload(self):
|
||||
childs = self.xml.getchildren()
|
||||
if len(childs) > 0:
|
||||
return childs[0]
|
||||
|
||||
def delPayload(self):
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
|
||||
|
||||
class EventRetract(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'retract'
|
||||
plugin_attrib = 'retract'
|
||||
interfaces = set(('id',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
class EventItems(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'items'
|
||||
plugin_attrib = 'items'
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
subitem = (EventItem, EventRetract)
|
||||
|
||||
registerStanzaPlugin(Event, EventItems)
|
||||
|
||||
class EventCollection(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'collection'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(Event, EventCollection)
|
||||
|
||||
class EventAssociate(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'associate'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(EventCollection, EventAssociate)
|
||||
|
||||
class EventDisassociate(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'disassociate'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(EventCollection, EventDisassociate)
|
||||
|
||||
class EventConfiguration(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'configuration'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node', 'config'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(Event, EventConfiguration)
|
||||
registerStanzaPlugin(EventConfiguration, xep_0004.Form)
|
||||
|
||||
class EventPurge(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'purge'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
registerStanzaPlugin(Event, EventPurge)
|
||||
|
||||
class EventSubscription(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
name = 'subscription'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('node','expiry', 'jid', 'subid', 'subscription'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def setJid(self, value):
|
||||
self._setAttr('jid', str(value))
|
||||
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
registerStanzaPlugin(Event, EventSubscription)
|
||||
|
||||
@@ -1,427 +1,395 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from . import base
|
||||
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
import copy
|
||||
#TODO support item groups and results
|
||||
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.message import Message
|
||||
|
||||
|
||||
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
|
||||
self.field = FieldAccessor(self)
|
||||
|
||||
def setup(self, xml=None):
|
||||
if ElementBase.setup(self, xml): #if we had to generate xml
|
||||
self['type'] = 'form'
|
||||
|
||||
def addField(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(parent=self)
|
||||
field['var'] = var
|
||||
field['type'] = kwtype
|
||||
field['label'] = label
|
||||
field['desc'] = desc
|
||||
field['required'] = required
|
||||
field['value'] = value
|
||||
if options is not None:
|
||||
field['options'] = options
|
||||
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 addItem(self, values):
|
||||
itemXML = ET.Element('{%s}item' % self.namespace)
|
||||
self.xml.append(itemXML)
|
||||
reported_vars = self['reported'].keys()
|
||||
for var in reported_vars:
|
||||
fieldXML = ET.Element('{%s}field' % FormField.namespace)
|
||||
itemXML.append(fieldXML)
|
||||
field = FormField(xml=fieldXML)
|
||||
field['var'] = var
|
||||
field['value'] = values.get(var, None)
|
||||
|
||||
def addReported(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 delFields(self):
|
||||
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
|
||||
for fieldXML in fieldsXML:
|
||||
self.xml.remove(fieldXML)
|
||||
|
||||
def delInstructions(self):
|
||||
instsXML = self.xml.findall('{%s}instructions')
|
||||
for instXML in instsXML:
|
||||
self.xml.remove(instXML)
|
||||
|
||||
def delItems(self):
|
||||
itemsXML = self.xml.find('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
self.xml.remove(itemXML)
|
||||
|
||||
def delReported(self):
|
||||
reportedXML = self.xml.find('{%s}reported' % self.namespace)
|
||||
if reportedXML is not None:
|
||||
self.xml.remove(reportedXML)
|
||||
|
||||
def getFields(self, use_dict=False):
|
||||
fields = {} if use_dict else []
|
||||
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
|
||||
for fieldXML in fieldsXML:
|
||||
field = FormField(xml=fieldXML)
|
||||
if use_dict:
|
||||
fields[field['var']] = field
|
||||
else:
|
||||
fields.append((field['var'], field))
|
||||
return fields
|
||||
|
||||
def getInstructions(self):
|
||||
instructions = ''
|
||||
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
|
||||
return "\n".join([instXML.text for instXML in instsXML])
|
||||
|
||||
def getItems(self):
|
||||
items = []
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
item = {}
|
||||
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 getReported(self):
|
||||
fields = {}
|
||||
fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
|
||||
FormField.namespace))
|
||||
for fieldXML in fieldsXML:
|
||||
field = FormField(xml=fieldXML)
|
||||
fields[field['var']] = field
|
||||
return fields
|
||||
|
||||
def getValues(self):
|
||||
values = {}
|
||||
fields = self.getFields(use_dict=True)
|
||||
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 setFields(self, fields, default=None):
|
||||
del self['fields']
|
||||
for field_data in fields:
|
||||
var = field_data[0]
|
||||
field = field_data[1]
|
||||
field['var'] = var
|
||||
|
||||
self.addField(**field)
|
||||
|
||||
def setInstructions(self, instructions):
|
||||
del self['instructions']
|
||||
if instructions in [None, '']:
|
||||
return
|
||||
instructions = instructions.split('\n')
|
||||
for instruction in instructions:
|
||||
inst = ET.Element('{%s}instructions' % self.namespace)
|
||||
inst.text = instruction
|
||||
self.xml.append(inst)
|
||||
|
||||
def setItems(self, items):
|
||||
for item in items:
|
||||
self.addItem(item)
|
||||
|
||||
def setReported(self, reported, default=None):
|
||||
for var in reported:
|
||||
field = reported[var]
|
||||
field['var'] = var
|
||||
self.addReported(var, **field)
|
||||
|
||||
def setValues(self, values):
|
||||
fields = self.getFields(use_dict=True)
|
||||
for field in values:
|
||||
fields[field]['value'] = values[field]
|
||||
|
||||
def merge(self, other):
|
||||
new = copy.copy(self)
|
||||
if type(other) == dict:
|
||||
new.setValues(other)
|
||||
return new
|
||||
nfields = new.getFields(use_dict=True)
|
||||
ofields = other.getFields(use_dict=True)
|
||||
nfields.update(ofields)
|
||||
new.setFields([(x, nfields[x]) for x in nfields])
|
||||
return new
|
||||
|
||||
class FieldAccessor(object):
|
||||
def __init__(self, form):
|
||||
self.form = form
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.form.getFields(use_dict=True)[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.form.getFields(use_dict=True)
|
||||
|
||||
def has_key(self, key):
|
||||
return key in self.form.getFields(use_dict=True)
|
||||
|
||||
|
||||
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',))
|
||||
field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi',
|
||||
'list-single', 'text-multi', 'text-private', 'text-single'))
|
||||
multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi'))
|
||||
multi_line_types = set(('hidden', 'text-multi'))
|
||||
option_types = set(('list-multi', 'list-single'))
|
||||
true_values = set((True, '1', 'true'))
|
||||
|
||||
def addOption(self, label='', value=''):
|
||||
if self['type'] in self.option_types:
|
||||
opt = FieldOption(parent=self)
|
||||
opt['label'] = label
|
||||
opt['value'] = value
|
||||
else:
|
||||
raise ValueError("Cannot add options to a %s field." % self['type'])
|
||||
|
||||
def delOptions(self):
|
||||
optsXML = self.xml.findall('{%s}option' % self.namespace)
|
||||
for optXML in optsXML:
|
||||
self.xml.remove(optXML)
|
||||
|
||||
def delRequired(self):
|
||||
reqXML = self.xml.find('{%s}required' % self.namespace)
|
||||
if reqXML is not None:
|
||||
self.xml.remove(reqXML)
|
||||
|
||||
def delValue(self):
|
||||
valsXML = self.xml.findall('{%s}value' % self.namespace)
|
||||
for valXML in valsXML:
|
||||
self.xml.remove(valXML)
|
||||
|
||||
def getAnswer(self):
|
||||
return self.getValue()
|
||||
|
||||
def getOptions(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 getRequired(self):
|
||||
reqXML = self.xml.find('{%s}required' % self.namespace)
|
||||
return reqXML is not None
|
||||
|
||||
def getValue(self):
|
||||
valsXML = self.xml.findall('{%s}value' % self.namespace)
|
||||
if len(valsXML) == 0:
|
||||
return None
|
||||
elif self['type'] == 'boolean':
|
||||
return valsXML[0].text in self.true_values
|
||||
elif self['type'] in self.multi_value_types:
|
||||
values = []
|
||||
for valXML in valsXML:
|
||||
if valXML.text is None:
|
||||
valXML.text = ''
|
||||
values.append(valXML.text)
|
||||
if self['type'] == 'text-multi':
|
||||
values = "\n".join(values)
|
||||
return values
|
||||
else:
|
||||
return valsXML[0].text
|
||||
|
||||
def setAnswer(self, answer):
|
||||
self.setValue(answer)
|
||||
|
||||
def setFalse(self):
|
||||
self.setValue(False)
|
||||
|
||||
def setOptions(self, options):
|
||||
for value in options:
|
||||
if isinstance(value, dict):
|
||||
self.addOption(**value)
|
||||
else:
|
||||
self.addOption(value=value)
|
||||
|
||||
def setRequired(self, required):
|
||||
exists = self.getRequired()
|
||||
if not exists and required:
|
||||
self.xml.append(ET.Element('{%s}required' % self.namespace))
|
||||
elif exists and not required:
|
||||
self.delRequired()
|
||||
|
||||
def setTrue(self):
|
||||
self.setValue(True)
|
||||
|
||||
def setValue(self, value):
|
||||
self.delValue()
|
||||
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 self['type'] in self.multi_line_types and isinstance(value, str):
|
||||
value = value.split('\n')
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
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',))
|
||||
|
||||
|
||||
class xep_0004(base.base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0004: Data Forms
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0004'
|
||||
self.description = 'Data Forms'
|
||||
self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform)
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('jabber:x:data')
|
||||
|
||||
def handler_message_xform(self, xml):
|
||||
object = self.handle_form(xml)
|
||||
self.xmpp.event("message_form", object)
|
||||
|
||||
def handler_presence_xform(self, xml):
|
||||
object = self.handle_form(xml)
|
||||
self.xmpp.event("presence_form", object)
|
||||
|
||||
def handle_form(self, xml):
|
||||
xmlform = xml.find('{jabber:x:data}x')
|
||||
object = self.buildForm(xmlform)
|
||||
self.xmpp.event("message_xform", object)
|
||||
return object
|
||||
|
||||
def buildForm(self, xml):
|
||||
form = Form(ftype=xml.attrib['type'])
|
||||
form.fromXML(xml)
|
||||
return form
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Data Form',
|
||||
MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns,
|
||||
Form.namespace)),
|
||||
self.handle_form))
|
||||
|
||||
registerStanzaPlugin(FormField, FieldOption)
|
||||
registerStanzaPlugin(Form, FormField)
|
||||
registerStanzaPlugin(Message, Form)
|
||||
|
||||
def makeForm(self, ftype='form', title='', instructions=''):
|
||||
return Form(self.xmpp, ftype, title, instructions)
|
||||
f = Form()
|
||||
f['type'] = ftype
|
||||
f['title'] = title
|
||||
f['instructions'] = instructions
|
||||
return f
|
||||
|
||||
class FieldContainer(object):
|
||||
def __init__(self, stanza = 'form'):
|
||||
self.fields = []
|
||||
self.field = {}
|
||||
self.stanza = stanza
|
||||
|
||||
def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
|
||||
self.field[var] = FormField(var, ftype, label, desc, required, value)
|
||||
self.fields.append(self.field[var])
|
||||
return self.field[var]
|
||||
|
||||
def buildField(self, xml):
|
||||
self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
|
||||
self.fields.append(self.field[xml.get('var', '__unnamed__')])
|
||||
self.field[xml.get('var', '__unnamed__')].buildField(xml)
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
|
||||
|
||||
def buildContainer(self, xml):
|
||||
self.stanza = xml.tag
|
||||
for field in xml.findall('{jabber:x:data}field'):
|
||||
self.buildField(field)
|
||||
|
||||
def getXML(self, ftype):
|
||||
container = ET.Element(self.stanza)
|
||||
for field in self.fields:
|
||||
container.append(field.getXML(ftype))
|
||||
return container
|
||||
|
||||
class Form(FieldContainer):
|
||||
types = ('form', 'submit', 'cancel', 'result')
|
||||
def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
|
||||
if not ftype in self.types:
|
||||
raise ValueError("Invalid Form Type")
|
||||
FieldContainer.__init__(self)
|
||||
self.xmpp = xmpp
|
||||
self.type = ftype
|
||||
self.title = title
|
||||
self.instructions = instructions
|
||||
self.reported = []
|
||||
self.items = []
|
||||
|
||||
def merge(self, form2):
|
||||
form1 = Form(ftype=self.type)
|
||||
form1.fromXML(self.getXML(self.type))
|
||||
for field in form2.fields:
|
||||
if not field.var in form1.field:
|
||||
form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
|
||||
else:
|
||||
form1.field[field.var].value = field.value
|
||||
for option, label in field.options:
|
||||
if (option, label) not in form1.field[field.var].options:
|
||||
form1.fields[field.var].addOption(option, label)
|
||||
return form1
|
||||
|
||||
def copy(self):
|
||||
newform = Form(ftype=self.type)
|
||||
newform.fromXML(self.getXML(self.type))
|
||||
return newform
|
||||
|
||||
def update(self, form):
|
||||
values = form.getValues()
|
||||
for var in values:
|
||||
if var in self.fields:
|
||||
self.fields[var].setValue(self.fields[var])
|
||||
|
||||
def getValues(self):
|
||||
result = {}
|
||||
for field in self.fields:
|
||||
value = field.value
|
||||
if len(value) == 1:
|
||||
value = value[0]
|
||||
result[field.var] = value
|
||||
return result
|
||||
|
||||
def setValues(self, values={}):
|
||||
for field in values:
|
||||
if field in self.field:
|
||||
if isinstance(values[field], list) or isinstance(values[field], tuple):
|
||||
for value in values[field]:
|
||||
self.field[field].setValue(value)
|
||||
else:
|
||||
self.field[field].setValue(values[field])
|
||||
|
||||
def fromXML(self, xml):
|
||||
self.buildForm(xml)
|
||||
|
||||
def addItem(self):
|
||||
newitem = FieldContainer('item')
|
||||
self.items.append(newitem)
|
||||
return newitem
|
||||
def handle_form(self, message):
|
||||
self.xmpp.event("message_xform", message)
|
||||
|
||||
def buildItem(self, xml):
|
||||
newitem = self.addItem()
|
||||
newitem.buildContainer(xml)
|
||||
|
||||
def addReported(self):
|
||||
reported = FieldContainer('reported')
|
||||
self.reported.append(reported)
|
||||
return reported
|
||||
|
||||
def buildReported(self, xml):
|
||||
reported = self.addReported()
|
||||
reported.buildContainer(xml)
|
||||
|
||||
def setTitle(self, title):
|
||||
self.title = title
|
||||
|
||||
def setInstructions(self, instructions):
|
||||
self.instructions = instructions
|
||||
|
||||
def setType(self, ftype):
|
||||
self.type = ftype
|
||||
|
||||
def getXMLMessage(self, to):
|
||||
msg = self.xmpp.makeMessage(to)
|
||||
msg.append(self.getXML())
|
||||
return msg
|
||||
|
||||
def buildForm(self, xml):
|
||||
self.type = xml.get('type', 'form')
|
||||
if xml.find('{jabber:x:data}title') is not None:
|
||||
self.setTitle(xml.find('{jabber:x:data}title').text)
|
||||
if xml.find('{jabber:x:data}instructions') is not None:
|
||||
self.setInstructions(xml.find('{jabber:x:data}instructions').text)
|
||||
for field in xml.findall('{jabber:x:data}field'):
|
||||
self.buildField(field)
|
||||
for reported in xml.findall('{jabber:x:data}reported'):
|
||||
self.buildReported(reported)
|
||||
for item in xml.findall('{jabber:x:data}item'):
|
||||
self.buildItem(item)
|
||||
|
||||
#def getXML(self, tostring = False):
|
||||
def getXML(self, ftype=None):
|
||||
logging.debug("creating form as %s" % ftype)
|
||||
if ftype:
|
||||
self.type = ftype
|
||||
form = ET.Element('{jabber:x:data}x')
|
||||
form.attrib['type'] = self.type
|
||||
if self.title and self.type in ('form', 'result'):
|
||||
title = ET.Element('{jabber:x:data}title')
|
||||
title.text = self.title
|
||||
form.append(title)
|
||||
if self.instructions and self.type == 'form':
|
||||
instructions = ET.Element('{jabber:x:data}instructions')
|
||||
instructions.text = self.instructions
|
||||
form.append(instructions)
|
||||
for field in self.fields:
|
||||
form.append(field.getXML(self.type))
|
||||
for reported in self.reported:
|
||||
form.append(reported.getXML('{jabber:x:data}reported'))
|
||||
for item in self.items:
|
||||
form.append(item.getXML(self.type))
|
||||
#if tostring:
|
||||
# form = self.xmpp.tostring(form)
|
||||
return form
|
||||
|
||||
def getXHTML(self):
|
||||
form = ET.Element('{http://www.w3.org/1999/xhtml}form')
|
||||
if self.title:
|
||||
title = ET.Element('h2')
|
||||
title.text = self.title
|
||||
form.append(title)
|
||||
if self.instructions:
|
||||
instructions = ET.Element('p')
|
||||
instructions.text = self.instructions
|
||||
form.append(instructions)
|
||||
for field in self.fields:
|
||||
form.append(field.getXHTML())
|
||||
for field in self.reported:
|
||||
form.append(field.getXHTML())
|
||||
for field in self.items:
|
||||
form.append(field.getXHTML())
|
||||
return form
|
||||
|
||||
|
||||
def makeSubmit(self):
|
||||
self.setType('submit')
|
||||
|
||||
class FormField(object):
|
||||
types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
|
||||
listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
|
||||
lbtypes = ('fixed', 'text-multi')
|
||||
def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
|
||||
if not ftype in self.types:
|
||||
raise ValueError("Invalid Field Type")
|
||||
self.type = ftype
|
||||
self.var = var
|
||||
self.label = label
|
||||
self.desc = desc
|
||||
self.options = []
|
||||
self.required = False
|
||||
self.value = []
|
||||
if self.type in self.listtypes:
|
||||
self.islist = True
|
||||
else:
|
||||
self.islist = False
|
||||
if self.type in self.lbtypes:
|
||||
self.islinebreak = True
|
||||
else:
|
||||
self.islinebreak = False
|
||||
if value:
|
||||
self.setValue(value)
|
||||
|
||||
def addOption(self, value, label):
|
||||
if self.islist:
|
||||
self.options.append((value, label))
|
||||
else:
|
||||
raise ValueError("Cannot add options to non-list type field.")
|
||||
|
||||
def setTrue(self):
|
||||
if self.type == 'boolean':
|
||||
self.value = [True]
|
||||
|
||||
def setFalse(self):
|
||||
if self.type == 'boolean':
|
||||
self.value = [False]
|
||||
|
||||
def require(self):
|
||||
self.required = True
|
||||
|
||||
def setDescription(self, desc):
|
||||
self.desc = desc
|
||||
|
||||
def setValue(self, value):
|
||||
if self.type == 'boolean':
|
||||
if value in ('1', 1, True, 'true', 'True', 'yes'):
|
||||
value = True
|
||||
else:
|
||||
value = False
|
||||
if self.islinebreak and value is not None:
|
||||
self.value += value.split('\n')
|
||||
else:
|
||||
if len(self.value) and (not self.islist or self.type == 'list-single'):
|
||||
self.value = [value]
|
||||
else:
|
||||
self.value.append(value)
|
||||
|
||||
def delValue(self, value):
|
||||
if type(self.value) == type([]):
|
||||
try:
|
||||
idx = self.value.index(value)
|
||||
if idx != -1:
|
||||
self.value.pop(idx)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.value = ''
|
||||
|
||||
def setAnswer(self, value):
|
||||
self.setValue(value)
|
||||
|
||||
def buildField(self, xml):
|
||||
self.type = xml.get('type', 'text-single')
|
||||
self.label = xml.get('label', '')
|
||||
for option in xml.findall('{jabber:x:data}option'):
|
||||
self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
|
||||
for value in xml.findall('{jabber:x:data}value'):
|
||||
self.setValue(value.text)
|
||||
if xml.find('{jabber:x:data}required') is not None:
|
||||
self.require()
|
||||
if xml.find('{jabber:x:data}desc') is not None:
|
||||
self.setDescription(xml.find('{jabber:x:data}desc').text)
|
||||
|
||||
def getXML(self, ftype):
|
||||
field = ET.Element('{jabber:x:data}field')
|
||||
if ftype != 'result':
|
||||
field.attrib['type'] = self.type
|
||||
if self.type != 'fixed':
|
||||
if self.var:
|
||||
field.attrib['var'] = self.var
|
||||
if self.label:
|
||||
field.attrib['label'] = self.label
|
||||
if ftype == 'form':
|
||||
for option in self.options:
|
||||
optionxml = ET.Element('{jabber:x:data}option')
|
||||
optionxml.attrib['label'] = option[1]
|
||||
optionval = ET.Element('{jabber:x:data}value')
|
||||
optionval.text = option[0]
|
||||
optionxml.append(optionval)
|
||||
field.append(optionxml)
|
||||
if self.required:
|
||||
required = ET.Element('{jabber:x:data}required')
|
||||
field.append(required)
|
||||
if self.desc:
|
||||
desc = ET.Element('{jabber:x:data}desc')
|
||||
desc.text = self.desc
|
||||
field.append(desc)
|
||||
for value in self.value:
|
||||
valuexml = ET.Element('{jabber:x:data}value')
|
||||
if value is True or value is False:
|
||||
if value:
|
||||
valuexml.text = '1'
|
||||
else:
|
||||
valuexml.text = '0'
|
||||
else:
|
||||
valuexml.text = value
|
||||
field.append(valuexml)
|
||||
return field
|
||||
|
||||
def getXHTML(self):
|
||||
field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
|
||||
if self.label:
|
||||
label = ET.Element('p')
|
||||
label.text = "%s: " % self.label
|
||||
else:
|
||||
label = ET.Element('p')
|
||||
label.text = "%s: " % self.var
|
||||
field.append(label)
|
||||
if self.type == 'boolean':
|
||||
formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
|
||||
if len(self.value) and self.value[0] in (True, 'true', '1'):
|
||||
formf.attrib['checked'] = 'checked'
|
||||
elif self.type == 'fixed':
|
||||
formf = ET.Element('p')
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
field.append(formf)
|
||||
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
elif self.type == 'hidden':
|
||||
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
elif self.type in ('jid-multi', 'list-multi'):
|
||||
formf = ET.Element('select', {'name': self.var})
|
||||
for option in self.options:
|
||||
optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
|
||||
optf.text = option[1]
|
||||
if option[1] in self.value:
|
||||
optf.attrib['selected'] = 'selected'
|
||||
formf.append(option)
|
||||
elif self.type in ('jid-single', 'text-single'):
|
||||
formf = ET.Element('input', {'type': 'text', 'name': self.var})
|
||||
try:
|
||||
formf.attrib['value'] = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
elif self.type == 'list-single':
|
||||
formf = ET.Element('select', {'name': self.var})
|
||||
for option in self.options:
|
||||
optf = ET.Element('option', {'value': option[0]})
|
||||
optf.text = option[1]
|
||||
if not optf.text:
|
||||
optf.text = option[0]
|
||||
if option[1] in self.value:
|
||||
optf.attrib['selected'] = 'selected'
|
||||
formf.append(optf)
|
||||
elif self.type == 'text-multi':
|
||||
formf = ET.Element('textarea', {'name': self.var})
|
||||
try:
|
||||
formf.text = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
if not formf.text:
|
||||
formf.text = ' '
|
||||
elif self.type == 'text-private':
|
||||
formf = ET.Element('input', {'type': 'password', 'name': self.var})
|
||||
try:
|
||||
formf.attrib['value'] = ', '.join(self.value)
|
||||
except:
|
||||
pass
|
||||
label.append(formf)
|
||||
return field
|
||||
|
||||
return Form(xml=xml)
|
||||
|
||||
11
sleekxmpp/plugins/xep_0009/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0009/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0009 import stanza
|
||||
from sleekxmpp.plugins.xep_0009.rpc import xep_0009
|
||||
from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
|
||||
166
sleekxmpp/plugins/xep_0009/binding.py
Normal file
166
sleekxmpp/plugins/xep_0009/binding.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from xml.etree import cElementTree as ET
|
||||
import base64
|
||||
import logging
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_namespace = 'jabber:iq:rpc'
|
||||
|
||||
def fault2xml(fault):
|
||||
value = dict()
|
||||
value['faultCode'] = fault['code']
|
||||
value['faultString'] = fault['string']
|
||||
fault = ET.Element("fault", {'xmlns': _namespace})
|
||||
fault.append(_py2xml((value)))
|
||||
return fault
|
||||
|
||||
def xml2fault(params):
|
||||
vals = []
|
||||
for value in params.findall('{%s}value' % _namespace):
|
||||
vals.append(_xml2py(value))
|
||||
fault = dict()
|
||||
fault['code'] = vals[0]['faultCode']
|
||||
fault['string'] = vals[0]['faultString']
|
||||
return fault
|
||||
|
||||
def py2xml(*args):
|
||||
params = ET.Element("{%s}params" % _namespace)
|
||||
for x in args:
|
||||
param = ET.Element("{%s}param" % _namespace)
|
||||
param.append(_py2xml(x))
|
||||
params.append(param) #<params><param>...
|
||||
return params
|
||||
|
||||
def _py2xml(*args):
|
||||
for x in args:
|
||||
val = ET.Element("value")
|
||||
if x is None:
|
||||
nil = ET.Element("nil")
|
||||
val.append(nil)
|
||||
elif type(x) is int:
|
||||
i4 = ET.Element("i4")
|
||||
i4.text = str(x)
|
||||
val.append(i4)
|
||||
elif type(x) is bool:
|
||||
boolean = ET.Element("boolean")
|
||||
boolean.text = str(int(x))
|
||||
val.append(boolean)
|
||||
elif type(x) is str:
|
||||
string = ET.Element("string")
|
||||
string.text = x
|
||||
val.append(string)
|
||||
elif type(x) is float:
|
||||
double = ET.Element("double")
|
||||
double.text = str(x)
|
||||
val.append(double)
|
||||
elif type(x) is rpcbase64:
|
||||
b64 = ET.Element("Base64")
|
||||
b64.text = x.encoded()
|
||||
val.append(b64)
|
||||
elif type(x) is rpctime:
|
||||
iso = ET.Element("dateTime.iso8601")
|
||||
iso.text = str(x)
|
||||
val.append(iso)
|
||||
elif type(x) in (list, tuple):
|
||||
array = ET.Element("array")
|
||||
data = ET.Element("data")
|
||||
for y in x:
|
||||
data.append(_py2xml(y))
|
||||
array.append(data)
|
||||
val.append(array)
|
||||
elif type(x) is dict:
|
||||
struct = ET.Element("struct")
|
||||
for y in x.keys():
|
||||
member = ET.Element("member")
|
||||
name = ET.Element("name")
|
||||
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(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') is not None:
|
||||
return rpcbase64(value.find('Base64' % namespace).text)
|
||||
if value.find('{%s}dateTime.iso8601') is not None:
|
||||
return rpctime(value.find('{%s}dateTime.iso8601'))
|
||||
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.decodestring(self.data)
|
||||
|
||||
def __str__(self):
|
||||
return self.decode()
|
||||
|
||||
def encoded(self):
|
||||
return self.data
|
||||
|
||||
|
||||
|
||||
class rpctime(object):
|
||||
|
||||
def __init__(self,data=None):
|
||||
#assume string data is in iso format YYYYMMDDTHH:MM:SS
|
||||
if type(data) is str:
|
||||
self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
|
||||
elif type(data) is time.struct_time:
|
||||
self.timestamp = data
|
||||
elif data is None:
|
||||
self.timestamp = time.gmtime()
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
def iso8601(self):
|
||||
#return a iso8601 string
|
||||
return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
|
||||
|
||||
def __str__(self):
|
||||
return self.iso8601()
|
||||
739
sleekxmpp/plugins/xep_0009/remote.py
Normal file
739
sleekxmpp/plugins/xep_0009/remote.py
Normal file
@@ -0,0 +1,739 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
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 sleekxmpp
|
||||
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!
|
||||
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 SleekXMPP 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 self._entries.has_key(key):
|
||||
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)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error)
|
||||
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 = sleekxmpp.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)
|
||||
|
||||
221
sleekxmpp/plugins/xep_0009/rpc.py
Normal file
221
sleekxmpp/plugins/xep_0009/rpc.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins import base
|
||||
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
|
||||
from sleekxmpp.stanza.iq import Iq
|
||||
from sleekxmpp.xmlstream.handler.callback import Callback
|
||||
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
|
||||
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
|
||||
from xml.etree import cElementTree as ET
|
||||
import logging
|
||||
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class xep_0009(base.base_plugin):
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0009'
|
||||
self.description = 'Jabber-RPC'
|
||||
#self.stanza = sleekxmpp.plugins.xep_0009.stanza
|
||||
|
||||
register_stanza_plugin(Iq, RPCQuery)
|
||||
register_stanza_plugin(RPCQuery, MethodCall)
|
||||
register_stanza_plugin(RPCQuery, MethodResponse)
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
|
||||
self._handle_method_call)
|
||||
)
|
||||
self.xmpp.registerHandler(
|
||||
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
|
||||
self._handle_method_response)
|
||||
)
|
||||
self.xmpp.registerHandler(
|
||||
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 = []
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
|
||||
self.xmpp.plugin['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
|
||||
|
||||
64
sleekxmpp/plugins/xep_0009/stanza/RPC.py
Normal file
64
sleekxmpp/plugins/xep_0009/stanza/RPC.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.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)
|
||||
9
sleekxmpp/plugins/xep_0009/stanza/__init__.py
Normal file
9
sleekxmpp/plugins/xep_0009/stanza/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
|
||||
118
sleekxmpp/plugins/xep_0012.py
Normal file
118
sleekxmpp/plugins/xep_0012.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from . import base
|
||||
from .. stanza.iq import Iq
|
||||
from .. xmlstream.handler.callback import Callback
|
||||
from .. xmlstream.matcher.xpath import MatchXPath
|
||||
from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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 = ''
|
||||
|
||||
class xep_0012(base.base_plugin):
|
||||
"""
|
||||
XEP-0012 Last Activity
|
||||
"""
|
||||
def plugin_init(self):
|
||||
self.description = "Last Activity"
|
||||
self.xep = "0012"
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Last Activity',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
LastActivity.namespace)),
|
||||
self.handle_last_activity_query))
|
||||
register_stanza_plugin(Iq, LastActivity)
|
||||
|
||||
self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity)
|
||||
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
if self.xmpp.is_component:
|
||||
# We are a component, so we track the uptime
|
||||
self.xmpp.add_event_handler("session_start", self._reset_uptime)
|
||||
self._start_datetime = datetime.now()
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:last')
|
||||
|
||||
def _reset_uptime(self, event):
|
||||
self._start_datetime = datetime.now()
|
||||
|
||||
def handle_last_activity_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Last activity requested by %s" % iq['from'])
|
||||
self.xmpp.event('last_activity_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Last activity result from %s" % iq['from'])
|
||||
self.xmpp.event('last_activity', iq)
|
||||
|
||||
def handle_last_activity(self, iq):
|
||||
jid = iq['from']
|
||||
|
||||
if self.xmpp.is_component:
|
||||
# Send the uptime
|
||||
result = LastActivity()
|
||||
td = (datetime.now() - self._start_datetime)
|
||||
result['seconds'] = td.seconds + td.days * 24 * 3600
|
||||
reply = iq.reply().setPayload(result.xml).send()
|
||||
else:
|
||||
barejid = JID(jid).bare
|
||||
if barejid in self.xmpp.roster and ( self.xmpp.roster[barejid]['subscription'] in ('from', 'both') or
|
||||
barejid == self.xmpp.boundjid.bare ):
|
||||
# We don't know how to calculate it
|
||||
iq.reply().error().setPayload(iq['last_activity'].xml)
|
||||
iq['error']['code'] = '503'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'service-unavailable'
|
||||
iq.send()
|
||||
else:
|
||||
iq.reply().error().setPayload(iq['last_activity'].xml)
|
||||
iq['error']['code'] = '403'
|
||||
iq['error']['type'] = 'auth'
|
||||
iq['error']['condition'] = 'forbidden'
|
||||
iq.send()
|
||||
|
||||
def get_last_activity(self, jid):
|
||||
"""Query the LastActivity of jid and return it in seconds"""
|
||||
iq = self.xmpp.makeIqGet()
|
||||
query = LastActivity()
|
||||
iq.append(query.xml)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq.get('id')
|
||||
result = iq.send()
|
||||
if result and result is not None and result.get('type', 'error') != 'error':
|
||||
return result['last_activity']['seconds']
|
||||
else:
|
||||
return False
|
||||
@@ -1,113 +0,0 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
"""
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
class xep_0030(base.base_plugin):
|
||||
"""
|
||||
XEP-0030 Service Discovery
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0030'
|
||||
self.description = 'Service Discovery'
|
||||
self.features = {'main': ['http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#items']}
|
||||
self.identities = {'main': [{'category': 'client', 'type': 'pc', 'name': 'SleekXMPP'}]}
|
||||
self.items = {'main': []}
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>" % self.xmpp.default_ns, self.info_handler)
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='http://jabber.org/protocol/disco#items' /></iq>" % self.xmpp.default_ns, self.item_handler)
|
||||
|
||||
def add_feature(self, feature, node='main'):
|
||||
if not node in self.features:
|
||||
self.features[node] = []
|
||||
self.features[node].append(feature)
|
||||
|
||||
def add_identity(self, category=None, itype=None, name=None, node='main'):
|
||||
if not node in self.identities:
|
||||
self.identities[node] = []
|
||||
self.identities[node].append({'category': category, 'type': itype, 'name': name})
|
||||
|
||||
def add_item(self, jid=None, name=None, node='main', subnode=''):
|
||||
if not node in self.items:
|
||||
self.items[node] = []
|
||||
self.items[node].append({'jid': jid, 'name': name, 'node': subnode})
|
||||
|
||||
def info_handler(self, xml):
|
||||
logging.debug("Info request from %s" % xml.get('from', ''))
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId()))
|
||||
iq.attrib['from'] = xml.get('to')
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||
query = xml.find('{http://jabber.org/protocol/disco#info}query')
|
||||
node = query.get('node', 'main')
|
||||
for identity in self.identities.get(node, []):
|
||||
idxml = ET.Element('identity')
|
||||
for attrib in identity:
|
||||
if identity[attrib]:
|
||||
idxml.attrib[attrib] = identity[attrib]
|
||||
query.append(idxml)
|
||||
for feature in self.features.get(node, []):
|
||||
featxml = ET.Element('feature')
|
||||
featxml.attrib['var'] = feature
|
||||
query.append(featxml)
|
||||
iq.append(query)
|
||||
#print ET.tostring(iq)
|
||||
self.xmpp.send(iq)
|
||||
|
||||
def item_handler(self, xml):
|
||||
logging.debug("Item request from %s" % xml.get('from', ''))
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId()))
|
||||
iq.attrib['from'] = xml.get('to')
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||
query = self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items').find('{http://jabber.org/protocol/disco#items}query')
|
||||
node = xml.find('{http://jabber.org/protocol/disco#items}query').get('node', 'main')
|
||||
for item in self.items.get(node, []):
|
||||
itemxml = ET.Element('item')
|
||||
itemxml.attrib = item
|
||||
if itemxml.attrib['jid'] is None:
|
||||
itemxml.attrib['jid'] = xml.get('to')
|
||||
query.append(itemxml)
|
||||
self.xmpp.send(iq)
|
||||
|
||||
def getItems(self, jid, node=None):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['to'] = jid
|
||||
self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items')
|
||||
if node:
|
||||
iq.find('{http://jabber.org/protocol/disco#items}query').attrib['node'] = node
|
||||
return self.xmpp.send(iq, "<iq id='%s' />" % iq.get('id'))
|
||||
|
||||
def getInfo(self, jid, node=None):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['to'] = jid
|
||||
self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#info')
|
||||
if node:
|
||||
iq.find('{http://jabber.org/protocol/disco#info}query').attrib['node'] = node
|
||||
return self.xmpp.send(iq, self.xmpp.makeIq(iq.get('id')))
|
||||
|
||||
def parseInfo(self, xml):
|
||||
result = {'identity': {}, 'feature': []}
|
||||
for identity in xml.findall('{http://jabber.org/protocol/disco#info}query/{{http://jabber.org/protocol/disco#info}identity'):
|
||||
result['identity'][identity['name']] = identity.attrib
|
||||
for feature in xml.findall('{http://jabber.org/protocol/disco#info}query/{{http://jabber.org/protocol/disco#info}feature'):
|
||||
result['feature'].append(feature.get('var', '__unknown__'))
|
||||
return result
|
||||
12
sleekxmpp/plugins/xep_0030/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0030/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0030 import stanza
|
||||
from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
|
||||
from sleekxmpp.plugins.xep_0030.static import StaticDisco
|
||||
from sleekxmpp.plugins.xep_0030.disco import xep_0030
|
||||
623
sleekxmpp/plugins/xep_0030/disco.py
Normal file
623
sleekxmpp/plugins/xep_0030/disco.py
Normal file
@@ -0,0 +1,623 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.exceptions import XMPPError
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
|
||||
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0030(base_plugin):
|
||||
|
||||
"""
|
||||
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 SleekXMPP 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 --
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Start the XEP-0030 plugin.
|
||||
"""
|
||||
self.xep = '0030'
|
||||
self.description = 'Service Discovery'
|
||||
self.stanza = sleekxmpp.plugins.xep_0030.stanza
|
||||
|
||||
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._disco_ops = ['get_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']
|
||||
self.default_handlers = {}
|
||||
self._handlers = {}
|
||||
for op in self._disco_ops:
|
||||
self._add_disco_op(op, getattr(self.static, op))
|
||||
|
||||
def post_init(self):
|
||||
"""Handle cross-plugin dependencies."""
|
||||
base_plugin.post_init(self)
|
||||
if 'xep_0059' in self.xmpp.plugin:
|
||||
register_stanza_plugin(DiscoItems,
|
||||
self.xmpp['xep_0059'].stanza.Set)
|
||||
|
||||
def _add_disco_op(self, op, default_handler):
|
||||
self.default_handlers[op] = default_handler
|
||||
self._handlers[op] = {'global': default_handler,
|
||||
'jid': {},
|
||||
'node': {}}
|
||||
|
||||
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.
|
||||
"""
|
||||
if htype not in self._disco_ops:
|
||||
return
|
||||
if jid is None and node is None:
|
||||
self._handlers[htype]['global'] = handler
|
||||
elif node is None:
|
||||
self._handlers[htype]['jid'][jid] = handler
|
||||
elif jid is None:
|
||||
if self.xmpp.is_component:
|
||||
jid = self.xmpp.boundjid.full
|
||||
else:
|
||||
jid = self.xmpp.boundjid.bare
|
||||
self._handlers[htype]['node'][(jid, node)] = handler
|
||||
else:
|
||||
self._handlers[htype]['node'][(jid, node)] = handler
|
||||
|
||||
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.set_node_handler(htype, jid, node, None)
|
||||
|
||||
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.del_node_handler(op, jid, node)
|
||||
self.set_node_handler(op, jid, node, self.default_handlers[op])
|
||||
|
||||
def get_info(self, jid=None, node=None, local=False, **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 Sleek instance and
|
||||
no stanzas need to be sent.
|
||||
Otherwise, a disco stanza must be sent to the
|
||||
remove JID to retrieve the info.
|
||||
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.
|
||||
"""
|
||||
if local or jid is None:
|
||||
log.debug("Looking up local disco#info data " + \
|
||||
"for %s, node %s." % (jid, node))
|
||||
info = self._run_node_handler('get_info', jid, node, kwargs)
|
||||
return self._fix_default_info(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))
|
||||
|
||||
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 Sleek 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.
|
||||
"""
|
||||
if local or jid is None:
|
||||
return self._run_node_handler('get_items', jid, node, kwargs)
|
||||
|
||||
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))
|
||||
|
||||
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._run_node_handler('set_items', jid, node, 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._run_node_handler('del_items', jid, node, 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._run_node_handler('add_item', ijid, node, 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._run_node_handler('del_item', jid, node, 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._run_node_handler('add_identity', jid, node, 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._run_node_handler('add_feature', jid, node, 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._run_node_handler('del_identity', jid, node, 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._run_node_handler('del_feature', jid, node, 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._run_node_handler('set_identities', jid, node, 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._run_node_handler('del_identities', jid, node, 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._run_node_handler('set_features', jid, node, 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._run_node_handler('del_features', jid, node, kwargs)
|
||||
|
||||
def _run_node_handler(self, htype, jid, node, 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.
|
||||
"""
|
||||
if jid is None:
|
||||
if self.xmpp.is_component:
|
||||
jid = self.xmpp.boundjid.full
|
||||
else:
|
||||
jid = self.xmpp.boundjid.bare
|
||||
if node is None:
|
||||
node = ''
|
||||
|
||||
if self._handlers[htype]['node'].get((jid, node), False):
|
||||
return self._handlers[htype]['node'][(jid, node)](jid, node, data)
|
||||
elif self._handlers[htype]['jid'].get(jid, False):
|
||||
return self._handlers[htype]['jid'][jid](jid, node, data)
|
||||
elif self._handlers[htype]['global']:
|
||||
return self._handlers[htype]['global'](jid, node, data)
|
||||
else:
|
||||
return None
|
||||
|
||||
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']))
|
||||
if self.xmpp.is_component:
|
||||
jid = iq['to'].full
|
||||
else:
|
||||
jid = iq['to'].bare
|
||||
info = self._run_node_handler('get_info',
|
||||
jid,
|
||||
iq['disco_info']['node'],
|
||||
iq)
|
||||
iq.reply()
|
||||
if info:
|
||||
info = self._fix_default_info(info)
|
||||
iq.set_payload(info.xml)
|
||||
iq.send()
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Received disco info result from" + \
|
||||
"%s to %s." % (iq['from'], iq['to']))
|
||||
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']))
|
||||
if self.xmpp.is_component:
|
||||
jid = iq['to'].full
|
||||
else:
|
||||
jid = iq['to'].bare
|
||||
items = self._run_node_handler('get_items',
|
||||
jid,
|
||||
iq['disco_items']['node'])
|
||||
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, SleekXMPP 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.
|
||||
"""
|
||||
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 info
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
xep_0030.getInfo = xep_0030.get_info
|
||||
xep_0030.getItems = xep_0030.get_items
|
||||
xep_0030.make_static = xep_0030.restore_defaults
|
||||
10
sleekxmpp/plugins/xep_0030/stanza/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0030/stanza/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo
|
||||
from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems
|
||||
262
sleekxmpp/plugins/xep_0030/stanza/info.py
Normal file
262
sleekxmpp/plugins/xep_0030/stanza/info.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.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="SleekXMPP 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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
identities = set()
|
||||
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:
|
||||
identities.add((
|
||||
id_xml.attrib['category'],
|
||||
id_xml.attrib['type'],
|
||||
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
|
||||
id_xml.attrib.get('name', None)))
|
||||
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):
|
||||
"""Return the set of all supported features."""
|
||||
features = set()
|
||||
for feature_xml in self.findall('{%s}feature' % self.namespace):
|
||||
features.add(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)
|
||||
136
sleekxmpp/plugins/xep_0030/stanza/items.py
Normal file
136
sleekxmpp/plugins/xep_0030/stanza/items.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream import ElementBase, ET
|
||||
|
||||
|
||||
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="sleekdev"
|
||||
name="SleekXMPP 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_xml = ET.Element('{%s}item' % self.namespace)
|
||||
item_xml.attrib['jid'] = jid
|
||||
if name:
|
||||
item_xml.attrib['name'] = name
|
||||
if node:
|
||||
item_xml.attrib['node'] = node
|
||||
self.xml.append(item_xml)
|
||||
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_xml in self.findall('{%s}item' % self.namespace):
|
||||
item = (item_xml.attrib['jid'],
|
||||
item_xml.attrib.get('node'),
|
||||
item_xml.attrib.get('name'))
|
||||
items.add(item)
|
||||
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()
|
||||
for item_xml in self.findall('{%s}item' % self.namespace):
|
||||
self.xml.remove(item_xml)
|
||||
265
sleekxmpp/plugins/xep_0030/static.py
Normal file
265
sleekxmpp/plugins/xep_0030/static.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.exceptions import XMPPError
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
|
||||
from sleekxmpp.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 SleekXMPP object.
|
||||
"""
|
||||
|
||||
def __init__(self, xmpp):
|
||||
"""
|
||||
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 SleekXMPP object.
|
||||
"""
|
||||
self.nodes = {}
|
||||
self.xmpp = xmpp
|
||||
|
||||
def add_node(self, jid=None, node=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.
|
||||
"""
|
||||
if jid is None:
|
||||
jid = self.xmpp.boundjid.full
|
||||
if node is None:
|
||||
node = ''
|
||||
if (jid, node) not in self.nodes:
|
||||
self.nodes[(jid, node)] = {'info': DiscoInfo(),
|
||||
'items': DiscoItems()}
|
||||
self.nodes[(jid, node)]['info']['node'] = node
|
||||
self.nodes[(jid, node)]['items']['node'] = node
|
||||
|
||||
# =================================================================
|
||||
# Node Handlers
|
||||
#
|
||||
# Each handler accepts three arguments: jid, node, and data.
|
||||
# The jid and node parameters together determine the set of
|
||||
# info and items stanzas that will be retrieved or added.
|
||||
# The data parameter is a dictionary with additional paramters
|
||||
# that will be passed to other calls.
|
||||
|
||||
def get_info(self, jid, node, data):
|
||||
"""
|
||||
Return the stored info data for the requested JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) not in self.nodes:
|
||||
if not node:
|
||||
return DiscoInfo()
|
||||
else:
|
||||
raise XMPPError(condition='item-not-found')
|
||||
else:
|
||||
return self.nodes[(jid, node)]['info']
|
||||
|
||||
def del_info(self, jid, node, data):
|
||||
"""
|
||||
Reset the info stanza for a given JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) in self.nodes:
|
||||
self.nodes[(jid, node)]['info'] = DiscoInfo()
|
||||
|
||||
def get_items(self, jid, node, data):
|
||||
"""
|
||||
Return the stored items data for the requested JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) not in self.nodes:
|
||||
if not node:
|
||||
return DiscoInfo()
|
||||
else:
|
||||
raise XMPPError(condition='item-not-found')
|
||||
else:
|
||||
return self.nodes[(jid, node)]['items']
|
||||
|
||||
def set_items(self, jid, node, data):
|
||||
"""
|
||||
Replace the stored items data for a JID/node combination.
|
||||
|
||||
The data parameter may provided:
|
||||
items -- A set of items in tuple format.
|
||||
"""
|
||||
items = data.get('items', set())
|
||||
self.add_node(jid, node)
|
||||
self.nodes[(jid, node)]['items']['items'] = items
|
||||
|
||||
def del_items(self, jid, node, data):
|
||||
"""
|
||||
Reset the items stanza for a given JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) in self.nodes:
|
||||
self.nodes[(jid, node)]['items'] = DiscoItems()
|
||||
|
||||
def add_identity(self, jid, node, 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.
|
||||
"""
|
||||
self.add_node(jid, node)
|
||||
self.nodes[(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, 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)
|
||||
"""
|
||||
identities = data.get('identities', set())
|
||||
self.add_node(jid, node)
|
||||
self.nodes[(jid, node)]['info']['identities'] = identities
|
||||
|
||||
def del_identity(self, jid, node, 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.
|
||||
"""
|
||||
if (jid, node) not in self.nodes:
|
||||
return
|
||||
self.nodes[(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, data):
|
||||
"""
|
||||
Remove all identities from a JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) not in self.nodes:
|
||||
return
|
||||
del self.nodes[(jid, node)]['info']['identities']
|
||||
|
||||
def add_feature(self, jid, node, data):
|
||||
"""
|
||||
Add a feature to a JID/node combination.
|
||||
|
||||
The data parameter should include:
|
||||
feature -- The namespace of the supported feature.
|
||||
"""
|
||||
self.add_node(jid, node)
|
||||
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
|
||||
|
||||
def set_features(self, jid, node, data):
|
||||
"""
|
||||
Add or replace all features for a JID/node combination.
|
||||
|
||||
The data parameter should include:
|
||||
features -- The new set of supported features.
|
||||
"""
|
||||
features = data.get('features', set())
|
||||
self.add_node(jid, node)
|
||||
self.nodes[(jid, node)]['info']['features'] = features
|
||||
|
||||
def del_feature(self, jid, node, data):
|
||||
"""
|
||||
Remove a feature from a JID/node combination.
|
||||
|
||||
The data parameter should include:
|
||||
feature -- The namespace of the removed feature.
|
||||
"""
|
||||
if (jid, node) not in self.nodes:
|
||||
return
|
||||
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
|
||||
|
||||
def del_features(self, jid, node, data):
|
||||
"""
|
||||
Remove all features from a JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) not in self.nodes:
|
||||
return
|
||||
del self.nodes[(jid, node)]['info']['features']
|
||||
|
||||
def add_item(self, jid, node, 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.
|
||||
"""
|
||||
self.add_node(jid, node)
|
||||
self.nodes[(jid, node)]['items'].add_item(
|
||||
data.get('ijid', ''),
|
||||
node=data.get('inode', ''),
|
||||
name=data.get('name', ''))
|
||||
|
||||
def del_item(self, jid, node, 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.
|
||||
"""
|
||||
if (jid, node) in self.nodes:
|
||||
self.nodes[(jid, node)]['items'].del_item(
|
||||
data.get('ijid', ''),
|
||||
node=data.get('inode', None))
|
||||
161
sleekxmpp/plugins/xep_0033.py
Normal file
161
sleekxmpp/plugins/xep_0033.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
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.message import Message
|
||||
|
||||
|
||||
class Addresses(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/address'
|
||||
name = 'addresses'
|
||||
plugin_attrib = 'addresses'
|
||||
interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
|
||||
|
||||
def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False):
|
||||
address = Address(parent=self)
|
||||
address['type'] = atype
|
||||
address['jid'] = jid
|
||||
address['node'] = node
|
||||
address['uri'] = uri
|
||||
address['desc'] = desc
|
||||
address['delivered'] = delivered
|
||||
return address
|
||||
|
||||
def getAddresses(self, atype=None):
|
||||
addresses = []
|
||||
for addrXML in self.xml.findall('{%s}address' % Address.namespace):
|
||||
# ElementTree 1.2.6 does not support [@attr='value'] in findall
|
||||
if atype is None or addrXML.attrib.get('type') == atype:
|
||||
addresses.append(Address(xml=addrXML, parent=None))
|
||||
return addresses
|
||||
|
||||
def setAddresses(self, addresses, set_type=None):
|
||||
self.delAddresses(set_type)
|
||||
for addr in addresses:
|
||||
addr = dict(addr)
|
||||
# Remap 'type' to 'atype' to match the add method
|
||||
if set_type is not None:
|
||||
addr['type'] = set_type
|
||||
curr_type = addr.get('type', None)
|
||||
if curr_type is not None:
|
||||
del addr['type']
|
||||
addr['atype'] = curr_type
|
||||
self.addAddress(**addr)
|
||||
|
||||
def delAddresses(self, atype=None):
|
||||
if atype is None:
|
||||
return
|
||||
for addrXML in self.xml.findall('{%s}address' % Address.namespace):
|
||||
# ElementTree 1.2.6 does not support [@attr='value'] in findall
|
||||
if addrXML.attrib.get('type') == atype:
|
||||
self.xml.remove(addrXML)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
|
||||
def delBcc(self):
|
||||
self.delAddresses('bcc')
|
||||
|
||||
def delCc(self):
|
||||
self.delAddresses('cc')
|
||||
|
||||
def delNoreply(self):
|
||||
self.delAddresses('noreply')
|
||||
|
||||
def delReplyroom(self):
|
||||
self.delAddresses('replyroom')
|
||||
|
||||
def delReplyto(self):
|
||||
self.delAddresses('replyto')
|
||||
|
||||
def delTo(self):
|
||||
self.delAddresses('to')
|
||||
|
||||
# --------------------------------------------------------------
|
||||
|
||||
def getBcc(self):
|
||||
return self.getAddresses('bcc')
|
||||
|
||||
def getCc(self):
|
||||
return self.getAddresses('cc')
|
||||
|
||||
def getNoreply(self):
|
||||
return self.getAddresses('noreply')
|
||||
|
||||
def getReplyroom(self):
|
||||
return self.getAddresses('replyroom')
|
||||
|
||||
def getReplyto(self):
|
||||
return self.getAddresses('replyto')
|
||||
|
||||
def getTo(self):
|
||||
return self.getAddresses('to')
|
||||
|
||||
# --------------------------------------------------------------
|
||||
|
||||
def setBcc(self, addresses):
|
||||
self.setAddresses(addresses, 'bcc')
|
||||
|
||||
def setCc(self, addresses):
|
||||
self.setAddresses(addresses, 'cc')
|
||||
|
||||
def setNoreply(self, addresses):
|
||||
self.setAddresses(addresses, 'noreply')
|
||||
|
||||
def setReplyroom(self, addresses):
|
||||
self.setAddresses(addresses, 'replyroom')
|
||||
|
||||
def setReplyto(self, addresses):
|
||||
self.setAddresses(addresses, 'replyto')
|
||||
|
||||
def setTo(self, addresses):
|
||||
self.setAddresses(addresses, 'to')
|
||||
|
||||
|
||||
class Address(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/address'
|
||||
name = 'address'
|
||||
plugin_attrib = 'address'
|
||||
interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri'))
|
||||
address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
|
||||
|
||||
def getDelivered(self):
|
||||
return self.xml.attrib.get('delivered', False)
|
||||
|
||||
def setDelivered(self, delivered):
|
||||
if delivered:
|
||||
self.xml.attrib['delivered'] = "true"
|
||||
else:
|
||||
del self['delivered']
|
||||
|
||||
def setUri(self, uri):
|
||||
if uri:
|
||||
del self['jid']
|
||||
del self['node']
|
||||
self.xml.attrib['uri'] = uri
|
||||
elif 'uri' in self.xml.attrib:
|
||||
del self.xml.attrib['uri']
|
||||
|
||||
|
||||
class xep_0033(base.base_plugin):
|
||||
"""
|
||||
XEP-0033: Extended Stanza Addressing
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0033'
|
||||
self.description = 'Extended Stanza Addressing'
|
||||
|
||||
registerStanzaPlugin(Message, Addresses)
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace)
|
||||
@@ -1,318 +1,353 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
from .. xmlstream.stanzabase import ElementBase, JID
|
||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
|
||||
from .. stanza.presence import Presence
|
||||
from .. xmlstream.handler.callback import Callback
|
||||
from .. xmlstream.matcher.xpath import MatchXPath
|
||||
from .. xmlstream.matcher.xmlmask import MatchXMLMask
|
||||
|
||||
|
||||
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(('', ))
|
||||
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 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):
|
||||
logging.warning("Cannot set nick through mucpresence plugin.")
|
||||
return self
|
||||
|
||||
def setRoom(self, value):
|
||||
logging.warning("Cannot set room through mucpresence plugin.")
|
||||
return self
|
||||
|
||||
def delNick(self):
|
||||
logging.warning("Cannot delete nick through mucpresence plugin.")
|
||||
return self
|
||||
|
||||
def delRoom(self):
|
||||
logging.warning("Cannot delete room through mucpresence plugin.")
|
||||
return self
|
||||
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(base.base_plugin):
|
||||
"""
|
||||
Impliments XEP-0045 Multi User Chat
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.rooms = {}
|
||||
self.ourNicks = {}
|
||||
self.xep = '0045'
|
||||
self.description = 'Multi User Chat'
|
||||
# load MUC support in presence stanzas
|
||||
self.xmpp.stanzaPlugin(Presence, MUCPresence)
|
||||
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
|
||||
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
|
||||
|
||||
def handle_groupchat_presence(self, pr):
|
||||
""" Handle a presence in a muc.
|
||||
"""
|
||||
if pr['muc']['room'] not in self.rooms.keys():
|
||||
return
|
||||
entry = pr['muc'].getValues()
|
||||
if pr['type'] == 'unavailable':
|
||||
del self.rooms[entry['room']][entry['nick']]
|
||||
else:
|
||||
self.rooms[entry['room']][entry['nick']] = entry
|
||||
logging.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
||||
self.xmpp.event("groupchat_presence", pr)
|
||||
|
||||
def handle_groupchat_message(self, msg):
|
||||
""" Handle a message event in a muc.
|
||||
"""
|
||||
self.xmpp.event('groupchat_message', 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
|
||||
"""
|
||||
Implements XEP-0045 Multi User Chat
|
||||
"""
|
||||
|
||||
def getRoomForm(self, room, ifrom=None):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq['to'] = room
|
||||
if ifrom is not None:
|
||||
iq['from'] = ifrom
|
||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||
iq.append(query)
|
||||
result = iq.send()
|
||||
if result['type'] == 'error':
|
||||
return False
|
||||
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||
if xform is None: return False
|
||||
form = self.xmpp.plugin['xep_0004'].buildForm(xform)
|
||||
return form
|
||||
|
||||
def configureRoom(self, room, form=None, ifrom=None):
|
||||
if form is None:
|
||||
form = self.getRoomForm(room, ifrom=ifrom)
|
||||
#form = self.xmpp.plugin['xep_0004'].makeForm(ftype='submit')
|
||||
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
||||
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)
|
||||
#result = self.xmpp.send(iq, self.xmpp.makeIq(iq.get('id')))
|
||||
result = iq.send()
|
||||
if result['type'] == 'error':
|
||||
return False
|
||||
return True
|
||||
|
||||
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
||||
""" Join the specified room, requesting 'maxhistory' lines of history.
|
||||
"""
|
||||
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
||||
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
||||
if password:
|
||||
passelement = ET.Element('password')
|
||||
passelement.text = password
|
||||
x.append(passelement)
|
||||
history = ET.Element('history')
|
||||
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('destroy')
|
||||
if altroom:
|
||||
destroy.attrib['jid'] = altroom
|
||||
xreason = ET.Element('reason')
|
||||
xreason.text = reason
|
||||
destroy.append(xreason)
|
||||
query.append(destroy)
|
||||
iq.append(query)
|
||||
#r = self.xmpp.send(iq, self.xmpp.makeIq(iq.get('id')))
|
||||
r = iq.send()
|
||||
if r is False or r['type'] == 'error':
|
||||
return False
|
||||
return True
|
||||
def plugin_init(self):
|
||||
self.rooms = {}
|
||||
self.ourNicks = {}
|
||||
self.xep = '0045'
|
||||
self.description = 'Multi User Chat'
|
||||
# load MUC support in presence stanzas
|
||||
registerStanzaPlugin(Presence, MUCPresence)
|
||||
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
|
||||
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
|
||||
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
|
||||
self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite))
|
||||
|
||||
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
||||
""" 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('item', {'affiliation':affiliation, 'nick':nick})
|
||||
else:
|
||||
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||
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=''):
|
||||
""" Invite a jid to a room."""
|
||||
msg = self.xmpp.makeMessage(room)
|
||||
msg['from'] = self.xmpp.jid
|
||||
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('reason')
|
||||
rxml.text = reason
|
||||
invite.append(rxml)
|
||||
x.append(invite)
|
||||
msg.append(x)
|
||||
self.xmpp.send(msg)
|
||||
def handle_groupchat_invite(self, inv):
|
||||
""" Handle an invite into a muc.
|
||||
"""
|
||||
logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv))
|
||||
if inv['from'] not in self.rooms.keys():
|
||||
self.xmpp.event("groupchat_invite", inv)
|
||||
|
||||
def leaveMUC(self, room, nick):
|
||||
""" Leave the specified room.
|
||||
"""
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||
del self.rooms[room]
|
||||
|
||||
def getRoomConfig(self, room):
|
||||
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
||||
iq['to'] = room
|
||||
result = iq.send()
|
||||
if result is None or result['type'] != 'result':
|
||||
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):
|
||||
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.send()
|
||||
|
||||
def setRoomConfig(self, room, config):
|
||||
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.send()
|
||||
|
||||
def getJoinedRooms(self):
|
||||
return self.rooms.keys()
|
||||
|
||||
def getOurJidInRoom(self, roomJid):
|
||||
""" Return the jid we're using in a room.
|
||||
"""
|
||||
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
||||
|
||||
def getJidProperty(self, room, nick, jidProperty):
|
||||
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
||||
If not found, return None.
|
||||
"""
|
||||
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
|
||||
return self.rooms[room][nick][jidProperty]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getRoster(self, room):
|
||||
""" Get the list of nicks in a room.
|
||||
"""
|
||||
if room not in self.rooms.keys():
|
||||
return None
|
||||
return self.rooms[room].keys()
|
||||
def 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']
|
||||
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_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 getRoomForm(self, room, ifrom=None):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq['to'] = room
|
||||
if ifrom is not None:
|
||||
iq['from'] = ifrom
|
||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||
iq.append(query)
|
||||
result = iq.send()
|
||||
if result['type'] == 'error':
|
||||
return False
|
||||
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||
if xform is None: return False
|
||||
form = self.xmpp.plugin['old_0004'].buildForm(xform)
|
||||
return form
|
||||
|
||||
def configureRoom(self, room, form=None, ifrom=None):
|
||||
if form is None:
|
||||
form = self.getRoomForm(room, ifrom=ifrom)
|
||||
#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
|
||||
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
||||
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)
|
||||
result = iq.send()
|
||||
if result['type'] == 'error':
|
||||
return False
|
||||
return True
|
||||
|
||||
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
||||
""" Join the specified room, requesting 'maxhistory' lines of history.
|
||||
"""
|
||||
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
||||
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
||||
if password:
|
||||
passelement = ET.Element('password')
|
||||
passelement.text = password
|
||||
x.append(passelement)
|
||||
if maxhistory:
|
||||
history = ET.Element('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('destroy')
|
||||
if altroom:
|
||||
destroy.attrib['jid'] = altroom
|
||||
xreason = ET.Element('reason')
|
||||
xreason.text = reason
|
||||
destroy.append(xreason)
|
||||
query.append(destroy)
|
||||
iq.append(query)
|
||||
r = iq.send()
|
||||
if r is False or r['type'] == 'error':
|
||||
return False
|
||||
return True
|
||||
|
||||
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
||||
""" 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('item', {'affiliation':affiliation, 'nick':nick})
|
||||
else:
|
||||
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||
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('reason')
|
||||
rxml.text = reason
|
||||
invite.append(rxml)
|
||||
x.append(invite)
|
||||
msg.append(x)
|
||||
self.xmpp.send(msg)
|
||||
|
||||
def leaveMUC(self, room, nick, msg=''):
|
||||
""" Leave the specified room.
|
||||
"""
|
||||
if msg:
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
|
||||
else:
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||
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
|
||||
result = iq.send()
|
||||
if result is None or result['type'] != 'result':
|
||||
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):
|
||||
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.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()
|
||||
|
||||
10
sleekxmpp/plugins/xep_0050/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0050/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0050.stanza import Command
|
||||
from sleekxmpp.plugins.xep_0050.adhoc import xep_0050
|
||||
593
sleekxmpp/plugins/xep_0050/adhoc.py
Normal file
593
sleekxmpp/plugins/xep_0050/adhoc.py
Normal file
@@ -0,0 +1,593 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin, JID
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.plugins.xep_0050 import stanza
|
||||
from sleekxmpp.plugins.xep_0050 import Command
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0050(base_plugin):
|
||||
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
"""Start the XEP-0050 plugin."""
|
||||
self.xep = '0050'
|
||||
self.description = 'Ad-Hoc Commands'
|
||||
self.stanza = stanza
|
||||
|
||||
self.threaded = self.config.get('threaded', True)
|
||||
self.commands = {}
|
||||
self.sessions = self.config.get('session_db', {})
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback("Ad-Hoc Execute",
|
||||
StanzaPath('iq@type=set/command'),
|
||||
self._handle_command))
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback("Ad-Hoc Result",
|
||||
StanzaPath('iq@type=result/command'),
|
||||
self._handle_command_result))
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback("Ad-Hoc Error",
|
||||
StanzaPath('iq@type=error/command'),
|
||||
self._handle_command_result))
|
||||
|
||||
register_stanza_plugin(Iq, stanza.Command)
|
||||
|
||||
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 post_init(self):
|
||||
"""Handle cross-plugin interactions."""
|
||||
base_plugin.post_init(self)
|
||||
self.xmpp['xep_0030'].add_feature(Command.namespace)
|
||||
|
||||
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 intial 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
|
||||
|
||||
# Client disco uses only the bare JID
|
||||
if self.xmpp.is_component:
|
||||
jid = jid.full
|
||||
else:
|
||||
jid = jid.bare
|
||||
|
||||
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))
|
||||
|
||||
initial_session = {'id': sessionid,
|
||||
'from': iq['from'],
|
||||
'to': iq['to'],
|
||||
'node': node,
|
||||
'payload': None,
|
||||
'interfaces': '',
|
||||
'payload_classes': None,
|
||||
'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[sessionid]
|
||||
|
||||
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)
|
||||
|
||||
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 not isinstance(payload, list):
|
||||
payload = [payload]
|
||||
|
||||
session['interfaces'] = [item.plugin_attrib for item in payload]
|
||||
session['payload_classes'] = [item.__class__ for item in payload]
|
||||
|
||||
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[sessionid]
|
||||
handler = session['cancel']
|
||||
|
||||
if handler:
|
||||
handler(iq, session)
|
||||
|
||||
try:
|
||||
del self.sessions[sessionid]
|
||||
except:
|
||||
pass
|
||||
|
||||
iq.reply()
|
||||
iq['command']['node'] = node
|
||||
iq['command']['sessionid'] = sessionid
|
||||
iq['command']['status'] = 'canceled'
|
||||
iq['command']['notes'] = session['notes']
|
||||
iq.send()
|
||||
|
||||
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[sessionid]
|
||||
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)
|
||||
|
||||
iq.reply()
|
||||
iq['command']['node'] = node
|
||||
iq['command']['sessionid'] = sessionid
|
||||
iq['command']['actions'] = []
|
||||
iq['command']['status'] = 'completed'
|
||||
iq['command']['notes'] = session['notes']
|
||||
iq.send()
|
||||
|
||||
del self.sessions[sessionid]
|
||||
|
||||
|
||||
# =================================================================
|
||||
# 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 Sleek 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, **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.
|
||||
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
|
||||
sleekxmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
callback -- Optional reference to a stream handler
|
||||
function. Will be executed when a reply
|
||||
stanza is received.
|
||||
"""
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['to'] = jid
|
||||
if ifrom:
|
||||
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)
|
||||
return iq.send(**kwargs)
|
||||
|
||||
def start_command(self, jid, node, session, ifrom=None):
|
||||
"""
|
||||
Initiate executing a command provided by a remote agent.
|
||||
|
||||
The workflow provided is always non-blocking.
|
||||
|
||||
The provided session dictionary should contain:
|
||||
next -- A handler for processing the command result.
|
||||
error -- A handler for processing any error stanzas
|
||||
generated by the request.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to send the command request.
|
||||
node -- The node for the desired command.
|
||||
session -- A dictionary of relevant session data.
|
||||
ifrom -- Optionally specify the sender's JID.
|
||||
"""
|
||||
session['jid'] = jid
|
||||
session['node'] = node
|
||||
session['timestamp'] = time.time()
|
||||
session['payload'] = None
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['to'] = jid
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
session['from'] = ifrom
|
||||
iq['command']['node'] = node
|
||||
iq['command']['action'] = 'execute'
|
||||
sessionid = 'client:pending_' + iq['id']
|
||||
session['id'] = sessionid
|
||||
self.sessions[sessionid] = session
|
||||
iq.send(block=False)
|
||||
|
||||
def continue_command(self, session):
|
||||
"""
|
||||
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='next',
|
||||
payload=session.get('payload', None),
|
||||
sessionid=session['id'])
|
||||
|
||||
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'])
|
||||
|
||||
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'])
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
del self.sessions[session['id']]
|
||||
except:
|
||||
pass
|
||||
|
||||
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)
|
||||
185
sleekxmpp/plugins/xep_0050/stanza.py
Normal file
185
sleekxmpp/plugins/xep_0050/stanza.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.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 = []
|
||||
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.append(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)
|
||||
10
sleekxmpp/plugins/xep_0059/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0059/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0059.stanza import Set
|
||||
from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059
|
||||
119
sleekxmpp/plugins/xep_0059/rsm.py
Normal file
119
sleekxmpp/plugins/xep_0059/rsm.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.plugins.xep_0059 import Set
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResultIterator():
|
||||
|
||||
"""
|
||||
An iterator for Result Set Managment
|
||||
"""
|
||||
|
||||
def __init__(self, query, interface, amount=10, start=None, reverse=False):
|
||||
"""
|
||||
Arguments:
|
||||
query -- The template query
|
||||
interface -- The substanza of the query, for example disco_items
|
||||
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.reverse = reverse
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
r = self.query.send(block=True)
|
||||
|
||||
if not r or not r[self.interface]['rsm']['first'] and \
|
||||
not r[self.interface]['rsm']['last']:
|
||||
raise StopIteration
|
||||
|
||||
if self.reverse:
|
||||
self.start = r[self.interface]['rsm']['first']
|
||||
else:
|
||||
self.start = r[self.interface]['rsm']['last']
|
||||
|
||||
return r
|
||||
|
||||
|
||||
class xep_0059(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0050: Result Set Management
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Start the XEP-0059 plugin.
|
||||
"""
|
||||
self.xep = '0059'
|
||||
self.description = 'Result Set Management'
|
||||
self.stanza = sleekxmpp.plugins.xep_0059.stanza
|
||||
|
||||
def post_init(self):
|
||||
"""Handle inter-plugin dependencies."""
|
||||
base_plugin.post_init(self)
|
||||
self.xmpp['xep_0030'].add_feature(Set.namespace)
|
||||
|
||||
def iterate(self, stanza, interface):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return ResultIterator(stanza, interface)
|
||||
108
sleekxmpp/plugins/xep_0059/stanza.py
Normal file
108
sleekxmpp/plugins/xep_0059/stanza.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream import ElementBase, ET
|
||||
from sleekxmpp.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
|
||||
else:
|
||||
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 == 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
|
||||
@@ -2,7 +2,13 @@ from __future__ import with_statement
|
||||
from . import base
|
||||
import logging
|
||||
#from xml.etree import cElementTree as ET
|
||||
from .. xmlstream.stanzabase import ElementBase, ET
|
||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
|
||||
from . import stanza_pubsub
|
||||
from . xep_0004 import Form
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0060(base.base_plugin):
|
||||
"""
|
||||
@@ -12,13 +18,15 @@ class xep_0060(base.base_plugin):
|
||||
def plugin_init(self):
|
||||
self.xep = '0060'
|
||||
self.description = 'Publish-Subscribe'
|
||||
|
||||
def create_node(self, jid, node, config=None, collection=False):
|
||||
|
||||
def create_node(self, jid, node, config=None, collection=False, ntype=None):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
create = ET.Element('create')
|
||||
create.set('node', node)
|
||||
pubsub.append(create)
|
||||
configure = ET.Element('configure')
|
||||
if collection:
|
||||
ntype = 'collection'
|
||||
#if config is None:
|
||||
# submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
|
||||
#else:
|
||||
@@ -28,66 +36,67 @@ class xep_0060(base.base_plugin):
|
||||
submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
|
||||
else:
|
||||
submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
|
||||
if collection:
|
||||
if ntype:
|
||||
if 'pubsub#node_type' in submitform.field:
|
||||
submitform.field['pubsub#node_type'].setValue('collection')
|
||||
submitform.field['pubsub#node_type'].setValue(ntype)
|
||||
else:
|
||||
submitform.addField('pubsub#node_type', value='collection')
|
||||
submitform.addField('pubsub#node_type', value=ntype)
|
||||
else:
|
||||
if 'pubsub#node_type' in submitform.field:
|
||||
submitform.field['pubsub#node_type'].setValue('leaf')
|
||||
else:
|
||||
submitform.addField('pubsub#node_type', value='leaf')
|
||||
configure.append(submitform.getXML('submit'))
|
||||
submitform['type'] = 'submit'
|
||||
configure.append(submitform.xml)
|
||||
pubsub.append(configure)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is False or result is None or result['type'] == 'error': return False
|
||||
return True
|
||||
|
||||
|
||||
def subscribe(self, jid, node, bare=True, subscribee=None):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
subscribe = ET.Element('subscribe')
|
||||
subscribe.attrib['node'] = node
|
||||
if subscribee is None:
|
||||
if bare:
|
||||
subscribe.attrib['jid'] = self.xmpp.jid
|
||||
subscribe.attrib['jid'] = self.xmpp.boundjid.bare
|
||||
else:
|
||||
subscribe.attrib['jid'] = self.xmpp.fulljid
|
||||
subscribe.attrib['jid'] = self.xmpp.boundjid.full
|
||||
else:
|
||||
subscribe.attrib['jid'] = subscribee
|
||||
pubsub.append(subscribe)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is False or result is None or result['type'] == 'error': return False
|
||||
return True
|
||||
|
||||
|
||||
def unsubscribe(self, jid, node, bare=True, subscribee=None):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
unsubscribe = ET.Element('unsubscribe')
|
||||
unsubscribe.attrib['node'] = node
|
||||
if subscribee is None:
|
||||
if bare:
|
||||
unsubscribe.attrib['jid'] = self.xmpp.jid
|
||||
unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare
|
||||
else:
|
||||
unsubscribe.attrib['jid'] = self.xmpp.fulljid
|
||||
unsubscribe.attrib['jid'] = self.xmpp.boundjid.full
|
||||
else:
|
||||
unsubscribe.attrib['jid'] = subscribee
|
||||
pubsub.append(unsubscribe)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is False or result is None or result['type'] == 'error': return False
|
||||
return True
|
||||
|
||||
|
||||
def getNodeConfig(self, jid, node=None): # if no node, then grab default
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||
if node is not None:
|
||||
@@ -100,22 +109,22 @@ class xep_0060(base.base_plugin):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.append(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is None or result == False or result['type'] == 'error':
|
||||
logging.warning("got error instead of config")
|
||||
log.warning("got error instead of config")
|
||||
return False
|
||||
if node is not None:
|
||||
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x')
|
||||
else:
|
||||
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x')
|
||||
if not form or form is None:
|
||||
logging.error("No form found.")
|
||||
log.error("No form found.")
|
||||
return False
|
||||
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
||||
|
||||
return Form(xml=form)
|
||||
|
||||
def getNodeSubscriptions(self, jid, node):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||
subscriptions = ET.Element('subscriptions')
|
||||
@@ -124,11 +133,11 @@ class xep_0060(base.base_plugin):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.append(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is None or result == False or result['type'] == 'error':
|
||||
logging.warning("got error instead of config")
|
||||
log.warning("got error instead of config")
|
||||
return False
|
||||
else:
|
||||
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription')
|
||||
@@ -147,11 +156,11 @@ class xep_0060(base.base_plugin):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.append(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is None or result == False or result['type'] == 'error':
|
||||
logging.warning("got error instead of config")
|
||||
log.warning("got error instead of config")
|
||||
return False
|
||||
else:
|
||||
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation')
|
||||
@@ -170,15 +179,14 @@ class xep_0060(base.base_plugin):
|
||||
pubsub.append(delete)
|
||||
iq.append(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
if result is not None and result is not False and result.attrib.get('type', 'error') != 'error':
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
result = iq.send()
|
||||
if result is not None and result is not False and result['type'] != 'error':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
def setNodeConfig(self, jid, node, config):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||
configure = ET.Element('configure')
|
||||
@@ -188,13 +196,13 @@ class xep_0060(base.base_plugin):
|
||||
pubsub.append(configure)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
if result is None or result['type'] == 'error':
|
||||
result = iq.send()
|
||||
if result is None or result['type'] == 'error':
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setItem(self, jid, node, items=[]):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
publish = ET.Element('publish')
|
||||
@@ -209,12 +217,15 @@ class xep_0060(base.base_plugin):
|
||||
pubsub.append(publish)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is None or result is False or result['type'] == 'error': return False
|
||||
return True
|
||||
|
||||
|
||||
def addItem(self, jid, node, items=[]):
|
||||
return self.setItem(jid, node, items)
|
||||
|
||||
def deleteItem(self, jid, node, item):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
retract = ET.Element('retract')
|
||||
@@ -225,15 +236,12 @@ class xep_0060(base.base_plugin):
|
||||
pubsub.append(retract)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is None or result is False or result['type'] == 'error': return False
|
||||
return True
|
||||
|
||||
def addItem(self, jid, node, items=[]):
|
||||
return setItem(jid, node, items)
|
||||
|
||||
|
||||
def getNodes(self, jid):
|
||||
response = self.xmpp.plugin['xep_0030'].getItems(jid)
|
||||
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
|
||||
@@ -242,7 +250,7 @@ class xep_0060(base.base_plugin):
|
||||
for item in items:
|
||||
nodes[item.get('node')] = item.get('name')
|
||||
return nodes
|
||||
|
||||
|
||||
def getItems(self, jid, node):
|
||||
response = self.xmpp.plugin['xep_0030'].getItems(jid, node)
|
||||
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
|
||||
@@ -260,7 +268,7 @@ class xep_0060(base.base_plugin):
|
||||
try:
|
||||
config.field['pubsub#collection'].setValue(parent)
|
||||
except KeyError:
|
||||
logging.warning("pubsub#collection doesn't exist in config, trying to add it")
|
||||
log.warning("pubsub#collection doesn't exist in config, trying to add it")
|
||||
config.addField('pubsub#collection', value=parent)
|
||||
if not self.setNodeConfig(jid, child, config):
|
||||
return False
|
||||
@@ -279,9 +287,9 @@ class xep_0060(base.base_plugin):
|
||||
pubsub.append(affs)
|
||||
iq = self.xmpp.makeIqSet(pubsub)
|
||||
iq.attrib['to'] = ps_jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq['id']
|
||||
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||
result = iq.send()
|
||||
if result is None or result is False or result['type'] == 'error':
|
||||
return False
|
||||
return True
|
||||
@@ -294,7 +302,7 @@ class xep_0060(base.base_plugin):
|
||||
try:
|
||||
config.field['pubsub#collection'].setValue(parent)
|
||||
except KeyError:
|
||||
logging.warning("pubsub#collection doesn't exist in config, trying to add it")
|
||||
log.warning("pubsub#collection doesn't exist in config, trying to add it")
|
||||
config.addField('pubsub#collection', value=parent)
|
||||
if not self.setNodeConfig(jid, child, config):
|
||||
return False
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
from xml.etree import cElementTree as ET
|
||||
@@ -24,6 +12,9 @@ import hashlib
|
||||
from . import base
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0078(base.base_plugin):
|
||||
"""
|
||||
XEP-0078 NON-SASL Authentication
|
||||
@@ -35,22 +26,22 @@ class xep_0078(base.base_plugin):
|
||||
#disabling until I fix conflict with PLAIN
|
||||
#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
|
||||
self.streamid = ''
|
||||
|
||||
|
||||
def check_stream(self, xml):
|
||||
self.streamid = xml.attrib['id']
|
||||
if xml.get('version', '0') != '1.0':
|
||||
self.auth()
|
||||
|
||||
|
||||
def auth(self, xml=None):
|
||||
logging.debug("Starting jabber:iq:auth Authentication")
|
||||
log.debug("Starting jabber:iq:auth Authentication")
|
||||
auth_request = self.xmpp.makeIqGet()
|
||||
auth_request_query = ET.Element('{jabber:iq:auth}query')
|
||||
auth_request.attrib['to'] = self.xmpp.server
|
||||
auth_request.attrib['to'] = self.xmpp.boundjid.host
|
||||
username = ET.Element('username')
|
||||
username.text = self.xmpp.username
|
||||
auth_request_query.append(username)
|
||||
auth_request.append(auth_request_query)
|
||||
result = self.xmpp.send(auth_request, self.xmpp.makeIqResult(self.xmpp.id))
|
||||
result = auth_request.send()
|
||||
rquery = result.find('{jabber:iq:auth}query')
|
||||
attempt = self.xmpp.makeIqSet()
|
||||
query = ET.Element('{jabber:iq:auth}query')
|
||||
@@ -59,23 +50,23 @@ class xep_0078(base.base_plugin):
|
||||
query.append(username)
|
||||
query.append(resource)
|
||||
if rquery.find('{jabber:iq:auth}digest') is None:
|
||||
logging.warning("Authenticating via jabber:iq:auth Plain.")
|
||||
log.warning("Authenticating via jabber:iq:auth Plain.")
|
||||
password = ET.Element('password')
|
||||
password.text = self.xmpp.password
|
||||
query.append(password)
|
||||
else:
|
||||
logging.debug("Authenticating via jabber:iq:auth Digest")
|
||||
log.debug("Authenticating via jabber:iq:auth Digest")
|
||||
digest = ET.Element('digest')
|
||||
digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
|
||||
query.append(digest)
|
||||
attempt.append(query)
|
||||
result = self.xmpp.send(attempt, self.xmpp.makeIq(self.xmpp.id))
|
||||
result = attempt.send()
|
||||
if result.attrib['type'] == 'result':
|
||||
with self.xmpp.lock:
|
||||
self.xmpp.authenticated = True
|
||||
self.xmpp.sessionstarted = True
|
||||
self.xmpp.event("session_start")
|
||||
else:
|
||||
logging.info("Authentication failed")
|
||||
log.info("Authentication failed")
|
||||
self.xmpp.disconnect()
|
||||
self.xmpp.event("failed_auth")
|
||||
|
||||
10
sleekxmpp/plugins/xep_0085/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0085/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permissio
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0085.stanza import ChatState
|
||||
from sleekxmpp.plugins.xep_0085.chat_states import xep_0085
|
||||
49
sleekxmpp/plugins/xep_0085/chat_states.py
Normal file
49
sleekxmpp/plugins/xep_0085/chat_states.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permissio
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.stanza import Message
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.plugins.xep_0085 import stanza, ChatState
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0085(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0085 Chat State Notifications
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0085'
|
||||
self.description = 'Chat State Notifications'
|
||||
self.stanza = stanza
|
||||
|
||||
for state in ChatState.states:
|
||||
self.xmpp.register_handler(
|
||||
Callback('Chat State: %s' % state,
|
||||
StanzaPath('message@chat_state=%s' % state),
|
||||
self._handle_chat_state))
|
||||
|
||||
register_stanza_plugin(Message, ChatState)
|
||||
|
||||
def post_init(self):
|
||||
base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
|
||||
|
||||
def _handle_chat_state(self, msg):
|
||||
state = msg['chat_state']
|
||||
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
|
||||
self.xmpp.event('chatstate_%s' % state, msg)
|
||||
73
sleekxmpp/plugins/xep_0085/stanza.py
Normal file
73
sleekxmpp/plugins/xep_0085/stanza.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permissio
|
||||
"""
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.xmlstream import ElementBase, ET
|
||||
|
||||
|
||||
class ChatState(ElementBase):
|
||||
|
||||
"""
|
||||
Example chat state stanzas:
|
||||
<message>
|
||||
<active xmlns="http://jabber.org/protocol/chatstates" />
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<paused xmlns="http://jabber.org/protocol/chatstates" />
|
||||
</message>
|
||||
|
||||
Stanza Interfaces:
|
||||
chat_state
|
||||
|
||||
Attributes:
|
||||
states
|
||||
|
||||
Methods:
|
||||
get_chat_state
|
||||
set_chat_state
|
||||
del_chat_state
|
||||
"""
|
||||
|
||||
name = ''
|
||||
namespace = 'http://jabber.org/protocol/chatstates'
|
||||
plugin_attrib = 'chat_state'
|
||||
interfaces = set(('chat_state',))
|
||||
is_extension = True
|
||||
|
||||
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
|
||||
|
||||
def setup(self, xml=None):
|
||||
self.xml = ET.Element('')
|
||||
return True
|
||||
|
||||
def get_chat_state(self):
|
||||
parent = self.parent()
|
||||
for state in self.states:
|
||||
state_xml = parent.find('{%s}%s' % (self.namespace, state))
|
||||
if state_xml is not None:
|
||||
self.xml = state_xml
|
||||
return state
|
||||
return ''
|
||||
|
||||
def set_chat_state(self, state):
|
||||
self.del_chat_state()
|
||||
parent = self.parent()
|
||||
if state in self.states:
|
||||
self.xml = ET.Element('{%s}%s' % (self.namespace, state))
|
||||
parent.append(self.xml)
|
||||
elif state not in [None, '']:
|
||||
raise ValueError('Invalid chat state')
|
||||
|
||||
def del_chat_state(self):
|
||||
parent = self.parent()
|
||||
for state in self.states:
|
||||
state_xml = parent.find('{%s}%s' % (self.namespace, state))
|
||||
if state_xml is not None:
|
||||
self.xml = ET.Element('')
|
||||
parent.xml.remove(state_xml)
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
import copy
|
||||
|
||||
class xep_0086(base.base_plugin):
|
||||
"""
|
||||
XEP-0086 Error Condition Mappings
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0086'
|
||||
self.description = 'Error Condition Mappings'
|
||||
self.error_map = {
|
||||
'bad-request':('modify','400'),
|
||||
'conflict':('cancel','409'),
|
||||
'feature-not-implemented':('cancel','501'),
|
||||
'forbidden':('auth','403'),
|
||||
'gone':('modify','302'),
|
||||
'internal-server-error':('wait','500'),
|
||||
'item-not-found':('cancel','404'),
|
||||
'jid-malformed':('modify','400'),
|
||||
'not-acceptable':('modify','406'),
|
||||
'not-allowed':('cancel','405'),
|
||||
'not-authorized':('auth','401'),
|
||||
'payment-required':('auth','402'),
|
||||
'recipient-unavailable':('wait','404'),
|
||||
'redirect':('modify','302'),
|
||||
'registration-required':('auth','407'),
|
||||
'remote-server-not-found':('cancel','404'),
|
||||
'remote-server-timeout':('wait','504'),
|
||||
'resource-constraint':('wait','500'),
|
||||
'service-unavailable':('cancel','503'),
|
||||
'subscription-required':('auth','407'),
|
||||
'undefined-condition':(None,'500'),
|
||||
'unexpected-request':('wait','400')
|
||||
}
|
||||
|
||||
|
||||
def makeError(self, condition, cdata=None, errorType=None, text=None, customElem=None):
|
||||
conditionElem = self.xmpp.makeStanzaErrorCondition(condition, cdata)
|
||||
if errorType is None:
|
||||
error = self.xmpp.makeStanzaError(conditionElem, self.error_map[condition][0], self.error_map[condition][1], text, customElem)
|
||||
else:
|
||||
error = self.xmpp.makeStanzaError(conditionElem, errorType, self.error_map[condition][1], text, customElem)
|
||||
error.append(conditionElem)
|
||||
return error
|
||||
10
sleekxmpp/plugins/xep_0086/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0086/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0086.stanza import LegacyError
|
||||
from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086
|
||||
42
sleekxmpp/plugins/xep_0086/legacy_error.py
Normal file
42
sleekxmpp/plugins/xep_0086/legacy_error.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.stanza import Error
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.plugins.xep_0086 import stanza, LegacyError
|
||||
|
||||
|
||||
class xep_0086(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0086: Error Condition Mappings
|
||||
|
||||
Older XMPP implementations used code based error messages, similar
|
||||
to HTTP response codes. Since then, error condition elements have
|
||||
been introduced. XEP-0086 provides a mapping between the new
|
||||
condition elements and a combination of error types and the older
|
||||
response codes.
|
||||
|
||||
Also see <http://xmpp.org/extensions/xep-0086.html>.
|
||||
|
||||
Configuration Values:
|
||||
override -- Indicates if applying legacy error codes should
|
||||
be done automatically. Defaults to True.
|
||||
If False, then inserting legacy error codes can
|
||||
be done using:
|
||||
iq['error']['legacy']['condition'] = ...
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0086'
|
||||
self.description = 'Error Condition Mappings'
|
||||
self.stanza = stanza
|
||||
|
||||
register_stanza_plugin(Error, LegacyError,
|
||||
overrides=self.config.get('override', True))
|
||||
91
sleekxmpp/plugins/xep_0086/stanza.py
Normal file
91
sleekxmpp/plugins/xep_0086/stanza.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.stanza import Error
|
||||
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||
|
||||
|
||||
class LegacyError(ElementBase):
|
||||
|
||||
"""
|
||||
Older XMPP implementations used code based error messages, similar
|
||||
to HTTP response codes. Since then, error condition elements have
|
||||
been introduced. XEP-0086 provides a mapping between the new
|
||||
condition elements and a combination of error types and the older
|
||||
response codes.
|
||||
|
||||
Also see <http://xmpp.org/extensions/xep-0086.html>.
|
||||
|
||||
Example legacy error stanzas:
|
||||
<error xmlns="jabber:client" code="501" type="cancel">
|
||||
<feature-not-implemented
|
||||
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
</error>
|
||||
|
||||
<error code="402" type="auth">
|
||||
<payment-required
|
||||
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
</error>
|
||||
|
||||
Attributes:
|
||||
error_map -- A map of error conditions to error types and
|
||||
code values.
|
||||
Methods:
|
||||
setup -- Overrides ElementBase.setup
|
||||
set_condition -- Remap the type and code interfaces when a
|
||||
condition is set.
|
||||
"""
|
||||
|
||||
name = 'legacy'
|
||||
namespace = Error.namespace
|
||||
plugin_attrib = name
|
||||
interfaces = set(('condition',))
|
||||
overrides = ['set_condition']
|
||||
|
||||
error_map = {'bad-request': ('modify','400'),
|
||||
'conflict': ('cancel','409'),
|
||||
'feature-not-implemented': ('cancel','501'),
|
||||
'forbidden': ('auth','403'),
|
||||
'gone': ('modify','302'),
|
||||
'internal-server-error': ('wait','500'),
|
||||
'item-not-found': ('cancel','404'),
|
||||
'jid-malformed': ('modify','400'),
|
||||
'not-acceptable': ('modify','406'),
|
||||
'not-allowed': ('cancel','405'),
|
||||
'not-authorized': ('auth','401'),
|
||||
'payment-required': ('auth','402'),
|
||||
'recipient-unavailable': ('wait','404'),
|
||||
'redirect': ('modify','302'),
|
||||
'registration-required': ('auth','407'),
|
||||
'remote-server-not-found': ('cancel','404'),
|
||||
'remote-server-timeout': ('wait','504'),
|
||||
'resource-constraint': ('wait','500'),
|
||||
'service-unavailable': ('cancel','503'),
|
||||
'subscription-required': ('auth','407'),
|
||||
'undefined-condition': (None,'500'),
|
||||
'unexpected-request': ('wait','400')}
|
||||
|
||||
def setup(self, xml):
|
||||
"""Don't create XML for the plugin."""
|
||||
self.xml = ET.Element('')
|
||||
|
||||
def set_condition(self, value):
|
||||
"""
|
||||
Set the error type and code based on the given error
|
||||
condition value.
|
||||
|
||||
Arguments:
|
||||
value -- The new error condition.
|
||||
"""
|
||||
self.parent().set_condition(value)
|
||||
|
||||
error_data = self.error_map.get(value, None)
|
||||
if error_data is not None:
|
||||
if error_data[0] is not None:
|
||||
self.parent()['type'] = error_data[0]
|
||||
self.parent()['code'] = error_data[1]
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
"""
|
||||
from xml.etree import cElementTree as ET
|
||||
from . import base
|
||||
from .. xmlstream.handler.xmlwaiter import XMLWaiter
|
||||
|
||||
class xep_0092(base.base_plugin):
|
||||
"""
|
||||
XEP-0092 Software Version
|
||||
"""
|
||||
def plugin_init(self):
|
||||
self.description = "Software Version"
|
||||
self.xep = "0092"
|
||||
self.name = self.config.get('name', 'SleekXMPP')
|
||||
self.version = self.config.get('version', '0.1-dev')
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version)
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('jabber:iq:version')
|
||||
|
||||
def report_version(self, xml):
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||
query = ET.Element('{jabber:iq:version}query')
|
||||
name = ET.Element('name')
|
||||
name.text = self.name
|
||||
version = ET.Element('version')
|
||||
version.text = self.version
|
||||
query.append(name)
|
||||
query.append(version)
|
||||
iq.append(query)
|
||||
self.xmpp.send(iq)
|
||||
|
||||
def getVersion(self, jid):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
query = ET.Element('{jabber:iq:version}query')
|
||||
iq.append(query)
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
id = iq.get('id')
|
||||
result = self.xmpp.send(iq, "<iq xmlns='%s' id='%s'/>" % (self.xmpp.default_ns, id))
|
||||
if result and result is not None and result.get('type', 'error') != 'error':
|
||||
qry = result.find('{jabber:iq:version}query')
|
||||
version = {}
|
||||
for child in qry.getchildren():
|
||||
version[child.tag.split('}')[-1]] = child.text
|
||||
return version
|
||||
else:
|
||||
return False
|
||||
|
||||
11
sleekxmpp/plugins/xep_0092/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0092/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0092 import stanza
|
||||
from sleekxmpp.plugins.xep_0092.stanza import Version
|
||||
from sleekxmpp.plugins.xep_0092.version import xep_0092
|
||||
42
sleekxmpp/plugins/xep_0092/stanza.py
Normal file
42
sleekxmpp/plugins/xep_0092/stanza.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream import ElementBase, ET
|
||||
|
||||
|
||||
class Version(ElementBase):
|
||||
|
||||
"""
|
||||
XMPP allows for an agent to advertise the name and version of the
|
||||
underlying software libraries, as well as the operating system
|
||||
that the agent is running on.
|
||||
|
||||
Example version stanzas:
|
||||
<iq type="get">
|
||||
<query xmlns="jabber:iq:version" />
|
||||
</iq>
|
||||
|
||||
<iq type="result">
|
||||
<query xmlns="jabber:iq:version">
|
||||
<name>SleekXMPP</name>
|
||||
<version>1.0</version>
|
||||
<os>Linux</os>
|
||||
</query>
|
||||
</iq>
|
||||
|
||||
Stanza Interface:
|
||||
name -- The human readable name of the software.
|
||||
version -- The specific version of the software.
|
||||
os -- The name of the operating system running the program.
|
||||
"""
|
||||
|
||||
name = 'query'
|
||||
namespace = 'jabber:iq:version'
|
||||
plugin_attrib = 'software_version'
|
||||
interfaces = set(('name', 'version', 'os'))
|
||||
sub_interfaces = interfaces
|
||||
88
sleekxmpp/plugins/xep_0092/version.py
Normal file
88
sleekxmpp/plugins/xep_0092/version.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.plugins.xep_0092 import Version
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0092(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0092: Software Version
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Start the XEP-0092 plugin.
|
||||
"""
|
||||
self.xep = "0092"
|
||||
self.description = "Software Version"
|
||||
self.stanza = sleekxmpp.plugins.xep_0092.stanza
|
||||
|
||||
self.name = self.config.get('name', 'SleekXMPP')
|
||||
self.version = self.config.get('version', '0.1-dev')
|
||||
self.os = self.config.get('os', '')
|
||||
|
||||
self.getVersion = self.get_version
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Software Version',
|
||||
StanzaPath('iq@type=get/software_version'),
|
||||
self._handle_version))
|
||||
|
||||
register_stanza_plugin(Iq, Version)
|
||||
|
||||
def post_init(self):
|
||||
"""
|
||||
Handle cross-plugin dependencies.
|
||||
"""
|
||||
base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
|
||||
|
||||
def _handle_version(self, iq):
|
||||
"""
|
||||
Respond to a software version query.
|
||||
|
||||
Arguments:
|
||||
iq -- The Iq stanza containing the software version query.
|
||||
"""
|
||||
iq.reply()
|
||||
iq['software_version']['name'] = self.name
|
||||
iq['software_version']['version'] = self.version
|
||||
iq['software_version']['os'] = self.os
|
||||
iq.send()
|
||||
|
||||
def get_version(self, jid, ifrom=None):
|
||||
"""
|
||||
Retrieve the software version of a remote agent.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the entity to query.
|
||||
"""
|
||||
iq = self.xmpp.Iq()
|
||||
iq['to'] = jid
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
iq['type'] = 'get'
|
||||
iq['query'] = Version.namespace
|
||||
|
||||
result = iq.send()
|
||||
|
||||
if result and result['type'] != 'error':
|
||||
return result['software_version'].values
|
||||
return False
|
||||
10
sleekxmpp/plugins/xep_0128/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0128/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco
|
||||
from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128
|
||||
101
sleekxmpp/plugins/xep_0128/extended_disco.py
Normal file
101
sleekxmpp/plugins/xep_0128/extended_disco.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.plugins.xep_0004 import Form
|
||||
from sleekxmpp.plugins.xep_0030 import DiscoInfo
|
||||
from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco
|
||||
|
||||
|
||||
class xep_0128(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0128: Service Discovery Extensions
|
||||
|
||||
Allow the use of data forms to add additional identity
|
||||
information to disco#info results.
|
||||
|
||||
Also see <http://www.xmpp.org/extensions/xep-0128.html>.
|
||||
|
||||
Attributes:
|
||||
disco -- A reference to the XEP-0030 plugin.
|
||||
static -- Object containing the default set of static
|
||||
node handlers.
|
||||
xmpp -- The main SleekXMPP object.
|
||||
|
||||
Methods:
|
||||
set_extended_info -- Set extensions to a disco#info result.
|
||||
add_extended_info -- Add an extension to a disco#info result.
|
||||
del_extended_info -- Remove all extensions from a disco#info result.
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
"""Start the XEP-0128 plugin."""
|
||||
self.xep = '0128'
|
||||
self.description = 'Service Discovery Extensions'
|
||||
|
||||
self._disco_ops = ['set_extended_info',
|
||||
'add_extended_info',
|
||||
'del_extended_info']
|
||||
|
||||
register_stanza_plugin(DiscoInfo, Form, iterable=True)
|
||||
|
||||
def post_init(self):
|
||||
"""Handle cross-plugin dependencies."""
|
||||
base_plugin.post_init(self)
|
||||
self.disco = self.xmpp['xep_0030']
|
||||
self.static = StaticExtendedDisco(self.disco.static)
|
||||
|
||||
self.disco.set_extended_info = self.set_extended_info
|
||||
self.disco.add_extended_info = self.add_extended_info
|
||||
self.disco.del_extended_info = self.del_extended_info
|
||||
|
||||
for op in self._disco_ops:
|
||||
self.disco._add_disco_op(op, getattr(self.static, op))
|
||||
|
||||
def set_extended_info(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Set additional, extended identity information to a node.
|
||||
|
||||
Replaces any existing extended information.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
data -- Either a form, or a list of forms to use
|
||||
as extended information, replacing any
|
||||
existing extensions.
|
||||
"""
|
||||
self.disco._run_node_handler('set_extended_info', jid, node, kwargs)
|
||||
|
||||
def add_extended_info(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Add additional, extended identity information to a node.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
data -- Either a form, or a list of forms to add
|
||||
as extended information.
|
||||
"""
|
||||
self.disco._run_node_handler('add_extended_info', jid, node, kwargs)
|
||||
|
||||
def del_extended_info(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove all extended identity information to a node.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
"""
|
||||
self.disco._run_node_handler('del_extended_info', jid, node, kwargs)
|
||||
72
sleekxmpp/plugins/xep_0128/static.py
Normal file
72
sleekxmpp/plugins/xep_0128/static.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.plugins.xep_0030 import StaticDisco
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StaticExtendedDisco(object):
|
||||
|
||||
"""
|
||||
Extend the default StaticDisco implementation to provide
|
||||
support for extended identity information.
|
||||
"""
|
||||
|
||||
def __init__(self, static):
|
||||
"""
|
||||
Augment the default XEP-0030 static handler object.
|
||||
|
||||
Arguments:
|
||||
static -- The default static XEP-0030 handler object.
|
||||
"""
|
||||
self.static = static
|
||||
|
||||
def set_extended_info(self, jid, node, data):
|
||||
"""
|
||||
Replace the extended identity data for a JID/node combination.
|
||||
|
||||
The data parameter may provide:
|
||||
data -- Either a single data form, or a list of data forms.
|
||||
"""
|
||||
self.del_extended_info(jid, node, data)
|
||||
self.add_extended_info(jid, node, data)
|
||||
|
||||
def add_extended_info(self, jid, node, data):
|
||||
"""
|
||||
Add additional extended identity data for a JID/node combination.
|
||||
|
||||
The data parameter may provide:
|
||||
data -- Either a single data form, or a list of data forms.
|
||||
"""
|
||||
self.static.add_node(jid, node)
|
||||
|
||||
forms = data.get('data', [])
|
||||
if not isinstance(forms, list):
|
||||
forms = [forms]
|
||||
|
||||
for form in forms:
|
||||
self.static.nodes[(jid, node)]['info'].append(form)
|
||||
|
||||
def del_extended_info(self, jid, node, data):
|
||||
"""
|
||||
Replace the extended identity data for a JID/node combination.
|
||||
|
||||
The data parameter is not used.
|
||||
"""
|
||||
if (jid, node) not in self.static.nodes:
|
||||
return
|
||||
|
||||
info = self.static.nodes[(jid, node)]['info']
|
||||
|
||||
for form in info['substanza']:
|
||||
info.xml.remove(form.xml)
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
XEP-0199 (Ping) support
|
||||
Copyright (C) 2007 Kevin Smith
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
"""
|
||||
from xml.etree import cElementTree as ET
|
||||
from . import base
|
||||
import time
|
||||
import logging
|
||||
|
||||
class xep_0199(base.base_plugin):
|
||||
"""XEP-0199 XMPP Ping"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.description = "XMPP Ping"
|
||||
self.xep = "0199"
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping)
|
||||
self.running = False
|
||||
#if self.config.get('keepalive', True):
|
||||
#self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('http://www.xmpp.org/extensions/xep-0199.html#ns')
|
||||
|
||||
def handler_pingserver(self, xml):
|
||||
if not self.running:
|
||||
time.sleep(self.config.get('frequency', 300))
|
||||
while self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is not False:
|
||||
time.sleep(self.config.get('frequency', 300))
|
||||
logging.debug("Did not recieve ping back in time. Requesting Reconnect.")
|
||||
self.xmpp.disconnect(reconnect=True)
|
||||
|
||||
def handler_ping(self, xml):
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||
self.xmpp.send(iq)
|
||||
|
||||
def sendPing(self, jid, timeout = 30):
|
||||
""" sendPing(jid, timeout)
|
||||
Sends a ping to the specified jid, returning the time (in seconds)
|
||||
to receive a reply, or None if no reply is received in timeout seconds.
|
||||
"""
|
||||
id = self.xmpp.getNewId()
|
||||
iq = self.xmpp.makeIq(id)
|
||||
iq.attrib['type'] = 'get'
|
||||
iq.attrib['to'] = jid
|
||||
ping = ET.Element('{http://www.xmpp.org/extensions/xep-0199.html#ns}ping')
|
||||
iq.append(ping)
|
||||
startTime = time.clock()
|
||||
pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout)
|
||||
endTime = time.clock()
|
||||
if pingresult == False:
|
||||
#self.xmpp.disconnect(reconnect=True)
|
||||
return False
|
||||
return endTime - startTime
|
||||
10
sleekxmpp/plugins/xep_0199/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0199/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0199.stanza import Ping
|
||||
from sleekxmpp.plugins.xep_0199.ping import xep_0199
|
||||
163
sleekxmpp/plugins/xep_0199/ping.py
Normal file
163
sleekxmpp/plugins/xep_0199/ping.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.plugins.xep_0199 import stanza, Ping
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0199(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0199: XMPP Ping
|
||||
|
||||
Given that XMPP is based on TCP connections, it is possible for the
|
||||
underlying connection to be terminated without the application's
|
||||
awareness. Ping stanzas provide an alternative to whitespace based
|
||||
keepalive methods for detecting lost connections.
|
||||
|
||||
Also see <http://www.xmpp.org/extensions/xep-0199.html>.
|
||||
|
||||
Attributes:
|
||||
keepalive -- If True, periodically send ping requests
|
||||
to the server. If a ping is not answered,
|
||||
the connection will be reset.
|
||||
frequency -- Time in seconds between keepalive pings.
|
||||
Defaults to 300 seconds.
|
||||
timeout -- Time in seconds to wait for a ping response.
|
||||
Defaults to 30 seconds.
|
||||
Methods:
|
||||
send_ping -- Send a ping to a given JID, returning the
|
||||
round trip time.
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Start the XEP-0199 plugin.
|
||||
"""
|
||||
self.description = 'XMPP Ping'
|
||||
self.xep = '0199'
|
||||
self.stanza = stanza
|
||||
|
||||
self.keepalive = self.config.get('keepalive', False)
|
||||
self.frequency = float(self.config.get('frequency', 300))
|
||||
self.timeout = self.config.get('timeout', 30)
|
||||
|
||||
register_stanza_plugin(Iq, Ping)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Ping',
|
||||
StanzaPath('iq@type=get/ping'),
|
||||
self._handle_ping))
|
||||
|
||||
if self.keepalive:
|
||||
self.xmpp.add_event_handler('session_start',
|
||||
self._handle_keepalive,
|
||||
threaded=True)
|
||||
|
||||
def post_init(self):
|
||||
"""Handle cross-plugin dependencies."""
|
||||
base_plugin.post_init(self)
|
||||
self.xmpp['xep_0030'].add_feature(Ping.namespace)
|
||||
|
||||
def _handle_keepalive(self, event):
|
||||
"""
|
||||
Begin periodic pinging of the server. If a ping is not
|
||||
answered, the connection will be restarted.
|
||||
|
||||
The pinging interval can be adjused using self.frequency
|
||||
before beginning processing.
|
||||
|
||||
Arguments:
|
||||
event -- The session_start event.
|
||||
"""
|
||||
def scheduled_ping():
|
||||
"""Send ping request to the server."""
|
||||
log.debug("Pinging...")
|
||||
resp = self.send_ping(self.xmpp.boundjid.host, self.timeout)
|
||||
if resp is None or resp is False:
|
||||
log.debug("Did not recieve ping back in time." + \
|
||||
"Requesting Reconnect.")
|
||||
self.xmpp.reconnect()
|
||||
|
||||
self.xmpp.schedule('Ping Keep Alive',
|
||||
self.frequency,
|
||||
scheduled_ping,
|
||||
repeat=True)
|
||||
|
||||
def _handle_ping(self, iq):
|
||||
"""
|
||||
Automatically reply to ping requests.
|
||||
|
||||
Arguments:
|
||||
iq -- The ping request.
|
||||
"""
|
||||
log.debug("Pinged by %s" % iq['from'])
|
||||
iq.reply().enable('ping').send()
|
||||
|
||||
def send_ping(self, jid, timeout=None, errorfalse=False,
|
||||
ifrom=None, block=True, callback=None):
|
||||
"""
|
||||
Send a ping request and calculate the response time.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID that will receive the ping.
|
||||
timeout -- Time in seconds to wait for a response.
|
||||
Defaults to self.timeout.
|
||||
errorfalse -- Indicates if False should be returned
|
||||
if an error stanza is received. Defaults
|
||||
to False.
|
||||
ifrom -- Specifiy the sender JID.
|
||||
block -- Indicate if execution should block until
|
||||
a pong response is received. Defaults
|
||||
to True.
|
||||
callback -- Optional handler to execute when a pong
|
||||
is received. Useful in conjunction with
|
||||
the option block=False.
|
||||
"""
|
||||
log.debug("Pinging %s" % jid)
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
iq.enable('ping')
|
||||
|
||||
start_time = time.clock()
|
||||
resp = iq.send(block=block,
|
||||
timeout=timeout,
|
||||
callback=callback)
|
||||
end_time = time.clock()
|
||||
|
||||
delay = end_time - start_time
|
||||
|
||||
if not block:
|
||||
return None
|
||||
|
||||
if not resp or resp['type'] == 'error':
|
||||
return False
|
||||
|
||||
log.debug("Pong: %s %f" % (jid, delay))
|
||||
return delay
|
||||
|
||||
|
||||
# Backwards compatibility for names
|
||||
xep_0199.sendPing = xep_0199.send_ping
|
||||
36
sleekxmpp/plugins/xep_0199/stanza.py
Normal file
36
sleekxmpp/plugins/xep_0199/stanza.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.xmlstream import ElementBase
|
||||
|
||||
|
||||
class Ping(ElementBase):
|
||||
|
||||
"""
|
||||
Given that XMPP is based on TCP connections, it is possible for the
|
||||
underlying connection to be terminated without the application's
|
||||
awareness. Ping stanzas provide an alternative to whitespace based
|
||||
keepalive methods for detecting lost connections.
|
||||
|
||||
Example ping stanza:
|
||||
<iq type="get">
|
||||
<ping xmlns="urn:xmpp:ping" />
|
||||
</iq>
|
||||
|
||||
Stanza Interface:
|
||||
None
|
||||
|
||||
Methods:
|
||||
None
|
||||
"""
|
||||
|
||||
name = 'ping'
|
||||
namespace = 'urn:xmpp:ping'
|
||||
plugin_attrib = 'ping'
|
||||
interfaces = set()
|
||||
117
sleekxmpp/plugins/xep_0202.py
Normal file
117
sleekxmpp/plugins/xep_0202.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from datetime import datetime, tzinfo
|
||||
import logging
|
||||
import time
|
||||
|
||||
from . import base
|
||||
from .. stanza.iq import Iq
|
||||
from .. xmlstream.handler.callback import Callback
|
||||
from .. xmlstream.matcher.xpath import MatchXPath
|
||||
from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EntityTime(ElementBase):
|
||||
name = 'time'
|
||||
namespace = 'urn:xmpp:time'
|
||||
plugin_attrib = 'entity_time'
|
||||
interfaces = set(('tzo', 'utc'))
|
||||
sub_interfaces = set(('tzo', 'utc'))
|
||||
|
||||
#def get_tzo(self):
|
||||
# TODO: Right now it returns a string but maybe it should
|
||||
# return a datetime.tzinfo object or maybe a datetime.timedelta?
|
||||
#pass
|
||||
|
||||
def set_tzo(self, tzo):
|
||||
if isinstance(tzo, tzinfo):
|
||||
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
|
||||
seconds = td.seconds + td.days * 24 * 3600
|
||||
sign = ('+' if seconds >= 0 else '-')
|
||||
minutes = abs(seconds // 60)
|
||||
tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60)
|
||||
elif not isinstance(tzo, str):
|
||||
raise TypeError('The time should be a string or a datetime.tzinfo object.')
|
||||
self._set_sub_text('tzo', tzo)
|
||||
|
||||
def get_utc(self):
|
||||
# Returns a datetime object instead the string. Is this a good idea?
|
||||
value = self._get_sub_text('utc')
|
||||
if '.' in value:
|
||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
else:
|
||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
def set_utc(self, tim=None):
|
||||
if isinstance(tim, datetime):
|
||||
if tim.utcoffset():
|
||||
tim = tim - tim.utcoffset()
|
||||
tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
elif isinstance(tim, time.struct_time):
|
||||
tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim)
|
||||
elif not isinstance(tim, str):
|
||||
raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.')
|
||||
|
||||
self._set_sub_text('utc', tim)
|
||||
|
||||
|
||||
class xep_0202(base.base_plugin):
|
||||
"""
|
||||
XEP-0202 Entity Time
|
||||
"""
|
||||
def plugin_init(self):
|
||||
self.description = "Entity Time"
|
||||
self.xep = "0202"
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Time Request',
|
||||
MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns,
|
||||
EntityTime.namespace)),
|
||||
self.handle_entity_time_query))
|
||||
register_stanza_plugin(Iq, EntityTime)
|
||||
|
||||
self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time)
|
||||
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
|
||||
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time')
|
||||
|
||||
def handle_entity_time_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Entity time requested by %s" % iq['from'])
|
||||
self.xmpp.event('entity_time_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Entity time result from %s" % iq['from'])
|
||||
self.xmpp.event('entity_time', iq)
|
||||
|
||||
def handle_entity_time(self, iq):
|
||||
iq = iq.reply()
|
||||
iq.enable('entity_time')
|
||||
tzo = time.strftime('%z') # %z is not on all ANSI C libraries
|
||||
tzo = tzo[:3] + ':' + tzo[3:]
|
||||
iq['entity_time']['tzo'] = tzo
|
||||
iq['entity_time']['utc'] = datetime.utcnow()
|
||||
iq.send()
|
||||
|
||||
def get_entity_time(self, jid):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.enable('entity_time')
|
||||
iq.attrib['to'] = jid
|
||||
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||
id = iq.get('id')
|
||||
result = iq.send()
|
||||
if result and result is not None and result.get('type', 'error') != 'error':
|
||||
return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']}
|
||||
else:
|
||||
return False
|
||||
10
sleekxmpp/plugins/xep_0249/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0249/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dalek
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins.xep_0249.stanza import Invite
|
||||
from sleekxmpp.plugins.xep_0249.invite import xep_0249
|
||||
79
sleekxmpp/plugins/xep_0249/invite.py
Normal file
79
sleekxmpp/plugins/xep_0249/invite.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dalek
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import Message
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.plugins.xep_0249 import Invite
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0249(base_plugin):
|
||||
|
||||
"""
|
||||
XEP-0249: Direct MUC Invitations
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = "0249"
|
||||
self.description = "Direct MUC Invitations"
|
||||
self.stanza = sleekxmpp.plugins.xep_0249.stanza
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Direct MUC Invitations',
|
||||
StanzaPath('message/groupchat_invite'),
|
||||
self._handle_invite))
|
||||
|
||||
register_stanza_plugin(Message, Invite)
|
||||
|
||||
def post_init(self):
|
||||
base_plugin.post_init(self)
|
||||
self.xmpp['xep_0030'].add_feature(Invite.namespace)
|
||||
|
||||
def _handle_invite(self, msg):
|
||||
"""
|
||||
Raise an event for all invitations received.
|
||||
"""
|
||||
log.debug("Received direct muc invitation from %s to room %s",
|
||||
msg['from'], msg['groupchat_invite']['jid'])
|
||||
|
||||
self.xmpp.event('groupchat_direct_invite', msg)
|
||||
|
||||
def send_invitation(self, jid, roomjid, password=None,
|
||||
reason=None, ifrom=None):
|
||||
"""
|
||||
Send a direct MUC invitation to an XMPP entity.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the entity that will receive
|
||||
the invitation
|
||||
roomjid -- the address of the groupchat room to be joined
|
||||
password -- a password needed for entry into a
|
||||
password-protected room (OPTIONAL).
|
||||
reason -- a human-readable purpose for the invitation
|
||||
(OPTIONAL).
|
||||
"""
|
||||
|
||||
msg = self.xmpp.Message()
|
||||
msg['to'] = jid
|
||||
if ifrom is not None:
|
||||
msg['from'] = ifrom
|
||||
msg['groupchat_invite']['jid'] = roomjid
|
||||
if password is not None:
|
||||
msg['groupchat_invite']['password'] = password
|
||||
if reason is not None:
|
||||
msg['groupchat_invite']['reason'] = reason
|
||||
|
||||
return msg.send()
|
||||
39
sleekxmpp/plugins/xep_0249/stanza.py
Normal file
39
sleekxmpp/plugins/xep_0249/stanza.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz, Dalek
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream import ElementBase
|
||||
|
||||
|
||||
class Invite(ElementBase):
|
||||
|
||||
"""
|
||||
XMPP allows for an agent in an MUC room to directly invite another
|
||||
user to join the chat room (as opposed to a mediated invitation
|
||||
done through the server).
|
||||
|
||||
Example invite stanza:
|
||||
<message from='crone1@shakespeare.lit/desktop'
|
||||
to='hecate@shakespeare.lit'>
|
||||
<x xmlns='jabber:x:conference'
|
||||
jid='darkcave@macbeth.shakespeare.lit'
|
||||
password='cauldronburn'
|
||||
reason='Hey Hecate, this is the place for all good witches!'/>
|
||||
</message>
|
||||
|
||||
Stanza Interface:
|
||||
jid -- The JID of the groupchat room
|
||||
password -- The password used to gain entry in the room
|
||||
(optional)
|
||||
reason -- The reason for the invitation (optional)
|
||||
|
||||
"""
|
||||
|
||||
name = "x"
|
||||
namespace = "jabber:x:conference"
|
||||
plugin_attrib = "groupchat_invite"
|
||||
interfaces = ("jid", "password", "reason")
|
||||
@@ -3,6 +3,12 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
__all__ = ['presence']
|
||||
|
||||
|
||||
from sleekxmpp.stanza.error import Error
|
||||
from sleekxmpp.stanza.stream_error import StreamError
|
||||
from sleekxmpp.stanza.iq import Iq
|
||||
from sleekxmpp.stanza.message import Message
|
||||
from sleekxmpp.stanza.presence import Presence
|
||||
|
||||
26
sleekxmpp/stanza/atom.py
Normal file
26
sleekxmpp/stanza/atom.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream import ElementBase
|
||||
|
||||
|
||||
class AtomEntry(ElementBase):
|
||||
|
||||
"""
|
||||
A simple Atom feed entry.
|
||||
|
||||
Stanza Interface:
|
||||
title -- The title of the Atom feed entry.
|
||||
summary -- The summary of the Atom feed entry.
|
||||
"""
|
||||
|
||||
namespace = 'http://www.w3.org/2005/Atom'
|
||||
name = 'entry'
|
||||
plugin_attrib = 'entry'
|
||||
interfaces = set(('title', 'summary'))
|
||||
sub_interfaces = set(('title', 'summary'))
|
||||
@@ -3,60 +3,140 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import ElementBase, ET
|
||||
|
||||
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||
|
||||
|
||||
class Error(ElementBase):
|
||||
namespace = 'jabber:client'
|
||||
name = 'error'
|
||||
plugin_attrib = 'error'
|
||||
conditions = set(('bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone', 'item-not-found', 'jid-malformed', 'not-acceptable', 'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable', 'redirect', 'registration-required', 'remote-server-not-found', 'remote-server-timeout', 'service-unavailable', 'subscription-required', 'undefined-condition', 'unexpected-request'))
|
||||
interfaces = set(('condition', 'text', 'type'))
|
||||
types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
|
||||
sub_interfaces = set(('text',))
|
||||
condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
|
||||
|
||||
def setup(self, xml=None):
|
||||
if ElementBase.setup(self, xml): #if we had to generate xml
|
||||
self['type'] = 'cancel'
|
||||
self['condition'] = 'feature-not-implemented'
|
||||
if self.parent is not None:
|
||||
self.parent['type'] = 'error'
|
||||
|
||||
def getCondition(self):
|
||||
for child in self.xml.getchildren():
|
||||
if "{%s}" % self.condition_ns in child.tag:
|
||||
return child.tag.split('}', 1)[-1]
|
||||
return ''
|
||||
|
||||
def setCondition(self, value):
|
||||
if value in self.conditions:
|
||||
for child in self.xml.getchildren():
|
||||
if "{%s}" % self.condition_ns in child.tag:
|
||||
self.xml.remove(child)
|
||||
condition = ET.Element("{%s}%s" % (self.condition_ns, value))
|
||||
self.xml.append(condition)
|
||||
return self
|
||||
|
||||
def delCondition(self):
|
||||
return self
|
||||
|
||||
def getText(self):
|
||||
text = ''
|
||||
textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text")
|
||||
if textxml is not None:
|
||||
text = textxml.text
|
||||
return text
|
||||
|
||||
def setText(self, value):
|
||||
self.delText()
|
||||
textxml = ET.Element('{urn:ietf:params:xml:ns:xmpp-stanzas}text')
|
||||
textxml.text = value
|
||||
self.xml.append(textxml)
|
||||
return self
|
||||
|
||||
def delText(self):
|
||||
textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text")
|
||||
if textxml is not None:
|
||||
self.xml.remove(textxml)
|
||||
|
||||
"""
|
||||
XMPP stanzas of type 'error' should include an <error> stanza that
|
||||
describes the nature of the error and how it should be handled.
|
||||
|
||||
Use the 'XEP-0086: Error Condition Mappings' plugin to include error
|
||||
codes used in older XMPP versions.
|
||||
|
||||
Example error stanza:
|
||||
<error type="cancel" code="404">
|
||||
<item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
|
||||
The item was not found.
|
||||
</text>
|
||||
</error>
|
||||
|
||||
Stanza Interface:
|
||||
code -- The error code used in older XMPP versions.
|
||||
condition -- The name of the condition element.
|
||||
text -- Human readable description of the error.
|
||||
type -- Error type indicating how the error should be handled.
|
||||
|
||||
Attributes:
|
||||
conditions -- The set of allowable error condition elements.
|
||||
condition_ns -- The namespace for the condition element.
|
||||
types -- A set of values indicating how the error
|
||||
should be treated.
|
||||
|
||||
Methods:
|
||||
setup -- Overrides ElementBase.setup.
|
||||
get_condition -- Retrieve the name of the condition element.
|
||||
set_condition -- Add a condition element.
|
||||
del_condition -- Remove the condition element.
|
||||
get_text -- Retrieve the contents of the <text> element.
|
||||
set_text -- Set the contents of the <text> element.
|
||||
del_text -- Remove the <text> element.
|
||||
"""
|
||||
|
||||
namespace = 'jabber:client'
|
||||
name = 'error'
|
||||
plugin_attrib = 'error'
|
||||
interfaces = set(('code', 'condition', 'text', 'type'))
|
||||
sub_interfaces = set(('text',))
|
||||
conditions = set(('bad-request', 'conflict', 'feature-not-implemented',
|
||||
'forbidden', 'gone', 'internal-server-error',
|
||||
'item-not-found', 'jid-malformed', 'not-acceptable',
|
||||
'not-allowed', 'not-authorized', 'payment-required',
|
||||
'recipient-unavailable', 'redirect',
|
||||
'registration-required', 'remote-server-not-found',
|
||||
'remote-server-timeout', 'resource-constraint',
|
||||
'service-unavailable', 'subscription-required',
|
||||
'undefined-condition', 'unexpected-request'))
|
||||
condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
|
||||
types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
|
||||
|
||||
def setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides ElementBase.setup.
|
||||
|
||||
Sets a default error type and condition, and changes the
|
||||
parent stanza's type to 'error'.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
if ElementBase.setup(self, xml):
|
||||
#If we had to generate XML then set default values.
|
||||
self['type'] = 'cancel'
|
||||
self['condition'] = 'feature-not-implemented'
|
||||
if self.parent is not None:
|
||||
self.parent()['type'] = 'error'
|
||||
|
||||
def get_condition(self):
|
||||
"""Return the condition element's name."""
|
||||
for child in self.xml.getchildren():
|
||||
if "{%s}" % self.condition_ns in child.tag:
|
||||
return child.tag.split('}', 1)[-1]
|
||||
return ''
|
||||
|
||||
def set_condition(self, value):
|
||||
"""
|
||||
Set the tag name of the condition element.
|
||||
|
||||
Arguments:
|
||||
value -- The tag name of the condition element.
|
||||
"""
|
||||
if value in self.conditions:
|
||||
del self['condition']
|
||||
self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value)))
|
||||
return self
|
||||
|
||||
def del_condition(self):
|
||||
"""Remove the condition element."""
|
||||
for child in self.xml.getchildren():
|
||||
if "{%s}" % self.condition_ns in child.tag:
|
||||
tag = child.tag.split('}', 1)[-1]
|
||||
if tag in self.conditions:
|
||||
self.xml.remove(child)
|
||||
return self
|
||||
|
||||
def get_text(self):
|
||||
"""Retrieve the contents of the <text> element."""
|
||||
return self._get_sub_text('{%s}text' % self.condition_ns)
|
||||
|
||||
def set_text(self, value):
|
||||
"""
|
||||
Set the contents of the <text> element.
|
||||
|
||||
Arguments:
|
||||
value -- The new contents for the <text> element.
|
||||
"""
|
||||
self._set_sub_text('{%s}text' % self.condition_ns, text=value)
|
||||
return self
|
||||
|
||||
def del_text(self):
|
||||
"""Remove the <text> element."""
|
||||
self._del_sub('{%s}text' % self.condition_ns)
|
||||
return self
|
||||
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Error.getCondition = Error.get_condition
|
||||
Error.setCondition = Error.set_condition
|
||||
Error.delCondition = Error.del_condition
|
||||
Error.getText = Error.get_text
|
||||
Error.setText = Error.set_text
|
||||
Error.delText = Error.del_text
|
||||
|
||||
@@ -3,32 +3,84 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import ElementBase, ET
|
||||
|
||||
from sleekxmpp.stanza import Message
|
||||
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||
|
||||
|
||||
class HTMLIM(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/xhtml-im'
|
||||
name = 'html'
|
||||
plugin_attrib = 'html'
|
||||
interfaces = set(('html'))
|
||||
plugin_attrib_map = set()
|
||||
plugin_xml_map = set()
|
||||
|
||||
def setHtml(self, html):
|
||||
if issinstance(html, str):
|
||||
html = ET.XML(html)
|
||||
if html.find('{http://www.w3.org/1999/xhtml}body') is None:
|
||||
body = ET.Element('{http://www.w3.org/1999/xhtml}body')
|
||||
body.append(html)
|
||||
else:
|
||||
body = html
|
||||
self.xml.append(html)
|
||||
|
||||
def getHtml(self):
|
||||
html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
|
||||
if html is None: return ''
|
||||
return __str__(html)
|
||||
|
||||
def delHtml(self):
|
||||
return self.__del__()
|
||||
"""
|
||||
XEP-0071: XHTML-IM defines a method for embedding XHTML content
|
||||
within a <message> stanza so that lightweight markup can be used
|
||||
to format the message contents and to create links.
|
||||
|
||||
Only a subset of XHTML is recommended for use with XHTML-IM.
|
||||
See the full spec at 'http://xmpp.org/extensions/xep-0071.html'
|
||||
for more information.
|
||||
|
||||
Example stanza:
|
||||
<message to="user@example.com">
|
||||
<body>Non-html message content.</body>
|
||||
<html xmlns="http://jabber.org/protocol/xhtml-im">
|
||||
<body xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p><b>HTML!</b></p>
|
||||
</body>
|
||||
</html>
|
||||
</message>
|
||||
|
||||
Stanza Interface:
|
||||
body -- The contents of the HTML body tag.
|
||||
|
||||
Methods:
|
||||
setup -- Overrides ElementBase.setup.
|
||||
get_body -- Return the HTML body contents.
|
||||
set_body -- Set the HTML body contents.
|
||||
del_body -- Remove the HTML body contents.
|
||||
"""
|
||||
|
||||
namespace = 'http://jabber.org/protocol/xhtml-im'
|
||||
name = 'html'
|
||||
interfaces = set(('body',))
|
||||
plugin_attrib = name
|
||||
|
||||
def set_body(self, html):
|
||||
"""
|
||||
Set the contents of the HTML body.
|
||||
|
||||
Arguments:
|
||||
html -- Either a string or XML object. If the top level
|
||||
element is not <body> with a namespace of
|
||||
'http://www.w3.org/1999/xhtml', it will be wrapped.
|
||||
"""
|
||||
if isinstance(html, str):
|
||||
html = ET.XML(html)
|
||||
if html.tag != '{http://www.w3.org/1999/xhtml}body':
|
||||
body = ET.Element('{http://www.w3.org/1999/xhtml}body')
|
||||
body.append(html)
|
||||
self.xml.append(body)
|
||||
else:
|
||||
self.xml.append(html)
|
||||
|
||||
def get_body(self):
|
||||
"""Return the contents of the HTML body."""
|
||||
html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
|
||||
if html is None:
|
||||
return ''
|
||||
return html
|
||||
|
||||
def del_body(self):
|
||||
"""Remove the HTML body contents."""
|
||||
if self.parent is not None:
|
||||
self.parent().xml.remove(self.xml)
|
||||
|
||||
|
||||
register_stanza_plugin(Message, HTMLIM)
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
HTMLIM.setBody = HTMLIM.set_body
|
||||
HTMLIM.getBody = HTMLIM.get_body
|
||||
HTMLIM.delBody = HTMLIM.del_body
|
||||
|
||||
@@ -3,73 +3,233 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import StanzaBase
|
||||
from xml.etree import cElementTree as ET
|
||||
from . error import Error
|
||||
from .. xmlstream.handler.waiter import Waiter
|
||||
from .. xmlstream.matcher.id import MatcherId
|
||||
from . rootstanza import RootStanza
|
||||
|
||||
from sleekxmpp.stanza import Error
|
||||
from sleekxmpp.stanza.rootstanza import RootStanza
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||
from sleekxmpp.xmlstream.handler import Waiter, Callback
|
||||
from sleekxmpp.xmlstream.matcher import MatcherId
|
||||
|
||||
|
||||
class Iq(RootStanza):
|
||||
interfaces = set(('type', 'to', 'from', 'id','query'))
|
||||
types = set(('get', 'result', 'set', 'error'))
|
||||
name = 'iq'
|
||||
namespace = 'jabber:client'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
StanzaBase.__init__(self, *args, **kwargs)
|
||||
if self['id'] == '':
|
||||
if self.stream is not None:
|
||||
self['id'] = self.stream.getNewId()
|
||||
else:
|
||||
self['id'] = '0'
|
||||
|
||||
def unhandled(self):
|
||||
if self['type'] in ('get', 'set'):
|
||||
self.reply()
|
||||
self['error']['condition'] = 'feature-not-implemented'
|
||||
self['error']['text'] = 'No handlers registered for this request.'
|
||||
self.send()
|
||||
|
||||
def setPayload(self, value):
|
||||
self.clear()
|
||||
StanzaBase.setPayload(self, value)
|
||||
|
||||
def setQuery(self, value):
|
||||
query = self.xml.find("{%s}query" % value)
|
||||
if query is None and value:
|
||||
self.clear()
|
||||
query = ET.Element("{%s}query" % value)
|
||||
self.xml.append(query)
|
||||
return self
|
||||
|
||||
def getQuery(self):
|
||||
for child in self.xml.getchildren():
|
||||
if child.tag.endswith('query'):
|
||||
ns =child.tag.split('}')[0]
|
||||
if '{' in ns:
|
||||
ns = ns[1:]
|
||||
return ns
|
||||
return ''
|
||||
|
||||
def reply(self):
|
||||
self['type'] = 'result'
|
||||
StanzaBase.reply(self)
|
||||
return self
|
||||
|
||||
def delQuery(self):
|
||||
for child in self.getchildren():
|
||||
if child.tag.endswith('query'):
|
||||
self.xml.remove(child)
|
||||
return self
|
||||
|
||||
def send(self, block=True, timeout=10):
|
||||
if block and self['type'] in ('get', 'set'):
|
||||
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
|
||||
self.stream.registerHandler(waitfor)
|
||||
StanzaBase.send(self)
|
||||
return waitfor.wait(timeout)
|
||||
else:
|
||||
return StanzaBase.send(self)
|
||||
"""
|
||||
XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of
|
||||
requesting and modifying information, similar to HTTP's GET and
|
||||
POST methods.
|
||||
|
||||
Each <iq> stanza must have an 'id' value which associates the
|
||||
stanza with the response stanza. XMPP entities must always
|
||||
be given a response <iq> stanza with a type of 'result' after
|
||||
sending a stanza of type 'get' or 'set'.
|
||||
|
||||
Most uses cases for <iq> stanzas will involve adding a <query>
|
||||
element whose namespace indicates the type of information
|
||||
desired. However, some custom XMPP applications use <iq> stanzas
|
||||
as a carrier stanza for an application-specific protocol instead.
|
||||
|
||||
Example <iq> Stanzas:
|
||||
<iq to="user@example.com" type="get" id="314">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" />
|
||||
</iq>
|
||||
|
||||
<iq to="user@localhost" type="result" id="17">
|
||||
<query xmlns='jabber:iq:roster'>
|
||||
<item jid='otheruser@example.net'
|
||||
name='John Doe'
|
||||
subscription='both'>
|
||||
<group>Friends</group>
|
||||
</item>
|
||||
</query>
|
||||
</iq>
|
||||
|
||||
Stanza Interface:
|
||||
query -- The namespace of the <query> element if one exists.
|
||||
|
||||
Attributes:
|
||||
types -- May be one of: get, set, result, or error.
|
||||
|
||||
Methods:
|
||||
__init__ -- Overrides StanzaBase.__init__.
|
||||
unhandled -- Send error if there are no handlers.
|
||||
set_payload -- Overrides StanzaBase.set_payload.
|
||||
set_query -- Add or modify a <query> element.
|
||||
get_query -- Return the namespace of the <query> element.
|
||||
del_query -- Remove the <query> element.
|
||||
reply -- Overrides StanzaBase.reply
|
||||
send -- Overrides StanzaBase.send
|
||||
"""
|
||||
|
||||
namespace = 'jabber:client'
|
||||
name = 'iq'
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'query'))
|
||||
types = set(('get', 'result', 'set', 'error'))
|
||||
plugin_attrib = name
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize a new <iq> stanza with an 'id' value.
|
||||
|
||||
Overrides StanzaBase.__init__.
|
||||
"""
|
||||
StanzaBase.__init__(self, *args, **kwargs)
|
||||
if self['id'] == '':
|
||||
if self.stream is not None:
|
||||
self['id'] = self.stream.new_id()
|
||||
else:
|
||||
self['id'] = '0'
|
||||
|
||||
def unhandled(self):
|
||||
"""
|
||||
Send a feature-not-implemented error if the stanza is not handled.
|
||||
|
||||
Overrides StanzaBase.unhandled.
|
||||
"""
|
||||
if self['type'] in ('get', 'set'):
|
||||
self.reply()
|
||||
self['error']['condition'] = 'feature-not-implemented'
|
||||
self['error']['text'] = 'No handlers registered for this request.'
|
||||
self.send()
|
||||
|
||||
def set_payload(self, value):
|
||||
"""
|
||||
Set the XML contents of the <iq> stanza.
|
||||
|
||||
Arguments:
|
||||
value -- An XML object to use as the <iq> stanza's contents
|
||||
"""
|
||||
self.clear()
|
||||
StanzaBase.set_payload(self, value)
|
||||
return self
|
||||
|
||||
def set_query(self, value):
|
||||
"""
|
||||
Add or modify a <query> element.
|
||||
|
||||
Query elements are differentiated by their namespace.
|
||||
|
||||
Arguments:
|
||||
value -- The namespace of the <query> element.
|
||||
"""
|
||||
query = self.xml.find("{%s}query" % value)
|
||||
if query is None and value:
|
||||
self.clear()
|
||||
query = ET.Element("{%s}query" % value)
|
||||
self.xml.append(query)
|
||||
return self
|
||||
|
||||
def get_query(self):
|
||||
"""Return the namespace of the <query> element."""
|
||||
for child in self.xml.getchildren():
|
||||
if child.tag.endswith('query'):
|
||||
ns = child.tag.split('}')[0]
|
||||
if '{' in ns:
|
||||
ns = ns[1:]
|
||||
return ns
|
||||
return ''
|
||||
|
||||
def del_query(self):
|
||||
"""Remove the <query> element."""
|
||||
for child in self.xml.getchildren():
|
||||
if child.tag.endswith('query'):
|
||||
self.xml.remove(child)
|
||||
return self
|
||||
|
||||
def reply(self, clear=True):
|
||||
"""
|
||||
Send a reply <iq> stanza.
|
||||
|
||||
Overrides StanzaBase.reply
|
||||
|
||||
Sets the 'type' to 'result' in addition to the default
|
||||
StanzaBase.reply behavior.
|
||||
|
||||
Arguments:
|
||||
clear -- Indicates if existing content should be
|
||||
removed before replying. Defaults to True.
|
||||
"""
|
||||
self['type'] = 'result'
|
||||
StanzaBase.reply(self, clear)
|
||||
return self
|
||||
|
||||
def send(self, block=True, timeout=None, callback=None, now=False):
|
||||
"""
|
||||
Send an <iq> stanza over the XML stream.
|
||||
|
||||
The send call can optionally block until a response is received or
|
||||
a timeout occurs. Be aware that using blocking in non-threaded event
|
||||
handlers can drastically impact performance. Otherwise, a callback
|
||||
handler can be provided that will be executed when the Iq stanza's
|
||||
result reply is received. Be aware though that that the callback
|
||||
handler will not be executed in its own thread.
|
||||
|
||||
Using both block and callback is not recommended, and only the
|
||||
callback argument will be used in that case.
|
||||
|
||||
Overrides StanzaBase.send
|
||||
|
||||
Arguments:
|
||||
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 sleekxmpp.xmlstream.RESPONSE_TIMEOUT
|
||||
callback -- Optional reference to a stream handler function. Will
|
||||
be executed when a reply stanza is received.
|
||||
now -- Indicates if the send queue should be skipped and send
|
||||
the stanza immediately. Used during stream
|
||||
initialization. Defaults to False.
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = self.stream.response_timeout
|
||||
if callback is not None and self['type'] in ('get', 'set'):
|
||||
handler_name = 'IqCallback_%s' % self['id']
|
||||
handler = Callback(handler_name,
|
||||
MatcherId(self['id']),
|
||||
callback,
|
||||
once=True)
|
||||
self.stream.register_handler(handler)
|
||||
StanzaBase.send(self, now=now)
|
||||
return handler_name
|
||||
elif block and self['type'] in ('get', 'set'):
|
||||
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
|
||||
self.stream.register_handler(waitfor)
|
||||
StanzaBase.send(self, now=now)
|
||||
return waitfor.wait(timeout)
|
||||
else:
|
||||
return StanzaBase.send(self, now=now)
|
||||
|
||||
def _set_stanza_values(self, values):
|
||||
"""
|
||||
Set multiple stanza interface values using a dictionary.
|
||||
|
||||
Stanza plugin values may be set usind nested dictionaries.
|
||||
|
||||
If the interface 'query' is given, then it will be set
|
||||
last to avoid duplication of the <query /> element.
|
||||
|
||||
Overrides ElementBase._set_stanza_values.
|
||||
|
||||
Arguments:
|
||||
values -- A dictionary mapping stanza interface with values.
|
||||
Plugin interfaces may accept a nested dictionary that
|
||||
will be used recursively.
|
||||
"""
|
||||
query = values.get('query', '')
|
||||
if query:
|
||||
del values['query']
|
||||
StanzaBase._set_stanza_values(self, values)
|
||||
self['query'] = query
|
||||
else:
|
||||
StanzaBase._set_stanza_values(self, values)
|
||||
return self
|
||||
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Iq.setPayload = Iq.set_payload
|
||||
Iq.getQuery = Iq.get_query
|
||||
Iq.setQuery = Iq.set_query
|
||||
Iq.delQuery = Iq.del_query
|
||||
|
||||
@@ -3,60 +3,155 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import StanzaBase
|
||||
from xml.etree import cElementTree as ET
|
||||
from . error import Error
|
||||
from . rootstanza import RootStanza
|
||||
|
||||
from sleekxmpp.stanza import Error
|
||||
from sleekxmpp.stanza.rootstanza import RootStanza
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||
|
||||
|
||||
class Message(RootStanza):
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', 'mucroom', 'mucnick'))
|
||||
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
|
||||
sub_interfaces = set(('body', 'subject'))
|
||||
name = 'message'
|
||||
namespace = 'jabber:client'
|
||||
|
||||
def getType(self):
|
||||
return self.xml.attrib.get('type', 'normal')
|
||||
|
||||
def chat(self):
|
||||
self['type'] = 'chat'
|
||||
return self
|
||||
|
||||
def normal(self):
|
||||
self['type'] = 'normal'
|
||||
return self
|
||||
|
||||
def reply(self, body=None):
|
||||
StanzaBase.reply(self)
|
||||
if self['type'] == 'groupchat':
|
||||
self['to'] = self['to'].bare
|
||||
del self['id']
|
||||
if body is not None:
|
||||
self['body'] = body
|
||||
return self
|
||||
|
||||
def getMucroom(self):
|
||||
if self['type'] == 'groupchat':
|
||||
return self['from'].bare
|
||||
else:
|
||||
return ''
|
||||
|
||||
def setMucroom(self, value):
|
||||
pass
|
||||
|
||||
def delMucroom(self):
|
||||
pass
|
||||
|
||||
def getMucnick(self):
|
||||
if self['type'] == 'groupchat':
|
||||
return self['from'].resource
|
||||
else:
|
||||
return ''
|
||||
|
||||
def setMucnick(self, value):
|
||||
pass
|
||||
|
||||
def delMucnick(self):
|
||||
pass
|
||||
"""
|
||||
XMPP's <message> stanzas are a "push" mechanism to send information
|
||||
to other XMPP entities without requiring a response.
|
||||
|
||||
Chat clients will typically use <message> stanzas that have a type
|
||||
of either "chat" or "groupchat".
|
||||
|
||||
When handling a message event, be sure to check if the message is
|
||||
an error response.
|
||||
|
||||
Example <message> stanzas:
|
||||
<message to="user1@example.com" from="user2@example.com">
|
||||
<body>Hi!</body>
|
||||
</message>
|
||||
|
||||
<message type="groupchat" to="room@conference.example.com">
|
||||
<body>Hi everyone!</body>
|
||||
</message>
|
||||
|
||||
Stanza Interface:
|
||||
body -- The main contents of the message.
|
||||
subject -- An optional description of the message's contents.
|
||||
mucroom -- (Read-only) The name of the MUC room that sent the message.
|
||||
mucnick -- (Read-only) The MUC nickname of message's sender.
|
||||
|
||||
Attributes:
|
||||
types -- May be one of: normal, chat, headline, groupchat, or error.
|
||||
|
||||
Methods:
|
||||
setup -- Overrides StanzaBase.setup.
|
||||
chat -- Set the message type to 'chat'.
|
||||
normal -- Set the message type to 'normal'.
|
||||
reply -- Overrides StanzaBase.reply
|
||||
get_type -- Overrides StanzaBase interface
|
||||
get_mucroom -- Return the name of the MUC room of the message.
|
||||
set_mucroom -- Dummy method to prevent assignment.
|
||||
del_mucroom -- Dummy method to prevent deletion.
|
||||
get_mucnick -- Return the MUC nickname of the message's sender.
|
||||
set_mucnick -- Dummy method to prevent assignment.
|
||||
del_mucnick -- Dummy method to prevent deletion.
|
||||
"""
|
||||
|
||||
namespace = 'jabber:client'
|
||||
name = 'message'
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject',
|
||||
'mucroom', 'mucnick'))
|
||||
sub_interfaces = set(('body', 'subject'))
|
||||
plugin_attrib = name
|
||||
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
|
||||
|
||||
def get_type(self):
|
||||
"""
|
||||
Return the message type.
|
||||
|
||||
Overrides default stanza interface behavior.
|
||||
|
||||
Returns 'normal' if no type attribute is present.
|
||||
"""
|
||||
return self._get_attr('type', 'normal')
|
||||
|
||||
def chat(self):
|
||||
"""Set the message type to 'chat'."""
|
||||
self['type'] = 'chat'
|
||||
return self
|
||||
|
||||
def normal(self):
|
||||
"""Set the message type to 'chat'."""
|
||||
self['type'] = 'normal'
|
||||
return self
|
||||
|
||||
def reply(self, body=None, clear=True):
|
||||
"""
|
||||
Create a message reply.
|
||||
|
||||
Overrides StanzaBase.reply.
|
||||
|
||||
Sets proper 'to' attribute if the message is from a MUC, and
|
||||
adds a message body if one is given.
|
||||
|
||||
Arguments:
|
||||
body -- Optional text content for the message.
|
||||
clear -- Indicates if existing content should be removed
|
||||
before replying. Defaults to True.
|
||||
"""
|
||||
StanzaBase.reply(self, clear)
|
||||
if self['type'] == 'groupchat':
|
||||
self['to'] = self['to'].bare
|
||||
|
||||
del self['id']
|
||||
|
||||
if body is not None:
|
||||
self['body'] = body
|
||||
return self
|
||||
|
||||
def get_mucroom(self):
|
||||
"""
|
||||
Return the name of the MUC room where the message originated.
|
||||
|
||||
Read-only stanza interface.
|
||||
"""
|
||||
if self['type'] == 'groupchat':
|
||||
return self['from'].bare
|
||||
else:
|
||||
return ''
|
||||
|
||||
def get_mucnick(self):
|
||||
"""
|
||||
Return the nickname of the MUC user that sent the message.
|
||||
|
||||
Read-only stanza interface.
|
||||
"""
|
||||
if self['type'] == 'groupchat':
|
||||
return self['from'].resource
|
||||
else:
|
||||
return ''
|
||||
|
||||
def set_mucroom(self, value):
|
||||
"""Dummy method to prevent modification."""
|
||||
pass
|
||||
|
||||
def del_mucroom(self):
|
||||
"""Dummy method to prevent deletion."""
|
||||
pass
|
||||
|
||||
def set_mucnick(self, value):
|
||||
"""Dummy method to prevent modification."""
|
||||
pass
|
||||
|
||||
def del_mucnick(self):
|
||||
"""Dummy method to prevent deletion."""
|
||||
pass
|
||||
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Message.getType = Message.get_type
|
||||
Message.getMucroom = Message.get_mucroom
|
||||
Message.setMucroom = Message.set_mucroom
|
||||
Message.delMucroom = Message.del_mucroom
|
||||
Message.getMucnick = Message.get_mucnick
|
||||
Message.setMucnick = Message.set_mucnick
|
||||
Message.delMucnick = Message.del_mucnick
|
||||
|
||||
@@ -3,23 +3,76 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import ElementBase, ET
|
||||
|
||||
from sleekxmpp.stanza import Message, Presence
|
||||
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||
|
||||
|
||||
class Nick(ElementBase):
|
||||
namespace = 'http://jabber.org/nick/nick'
|
||||
name = 'nick'
|
||||
plugin_attrib = 'nick'
|
||||
interfaces = set(('nick'))
|
||||
plugin_attrib_map = set()
|
||||
plugin_xml_map = set()
|
||||
|
||||
def setNick(self, nick):
|
||||
self.xml.text = nick
|
||||
|
||||
def getNick(self):
|
||||
return self.xml.text
|
||||
|
||||
def delNick(self):
|
||||
return self.__del__()
|
||||
"""
|
||||
XEP-0172: User Nickname allows the addition of a <nick> element
|
||||
in several stanza types, including <message> and <presence> stanzas.
|
||||
|
||||
The nickname contained in a <nick> should be the global, friendly or
|
||||
informal name chosen by the owner of a bare JID. The <nick> element
|
||||
may be included when establishing communications with new entities,
|
||||
such as normal XMPP users or MUC services.
|
||||
|
||||
The nickname contained in a <nick> element will not necessarily be
|
||||
the same as the nickname used in a MUC.
|
||||
|
||||
Example stanzas:
|
||||
<message to="user@example.com">
|
||||
<nick xmlns="http://jabber.org/nick/nick">The User</nick>
|
||||
<body>...</body>
|
||||
</message>
|
||||
|
||||
<presence to="otheruser@example.com" type="subscribe">
|
||||
<nick xmlns="http://jabber.org/nick/nick">The User</nick>
|
||||
</presence>
|
||||
|
||||
Stanza Interface:
|
||||
nick -- A global, friendly or informal name chosen by a user.
|
||||
|
||||
Methods:
|
||||
setup -- Overrides ElementBase.setup.
|
||||
get_nick -- Return the nickname in the <nick> element.
|
||||
set_nick -- Add a <nick> element with the given nickname.
|
||||
del_nick -- Remove the <nick> element.
|
||||
"""
|
||||
|
||||
namespace = 'http://jabber.org/protocol/nick'
|
||||
name = 'nick'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('nick',))
|
||||
|
||||
def set_nick(self, nick):
|
||||
"""
|
||||
Add a <nick> element with the given nickname.
|
||||
|
||||
Arguments:
|
||||
nick -- A human readable, informal name.
|
||||
"""
|
||||
self.xml.text = nick
|
||||
|
||||
def get_nick(self):
|
||||
"""Return the nickname in the <nick> element."""
|
||||
return self.xml.text
|
||||
|
||||
def del_nick(self):
|
||||
"""Remove the <nick> element."""
|
||||
if self.parent is not None:
|
||||
self.parent().xml.remove(self.xml)
|
||||
|
||||
|
||||
register_stanza_plugin(Message, Nick)
|
||||
register_stanza_plugin(Presence, Nick)
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Nick.setNick = Nick.set_nick
|
||||
Nick.getNick = Nick.get_nick
|
||||
Nick.delNick = Nick.del_nick
|
||||
|
||||
@@ -3,60 +3,178 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import StanzaBase
|
||||
from xml.etree import cElementTree as ET
|
||||
from . error import Error
|
||||
from . rootstanza import RootStanza
|
||||
|
||||
from sleekxmpp.stanza import Error
|
||||
from sleekxmpp.stanza.rootstanza import RootStanza
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||
|
||||
|
||||
class Presence(RootStanza):
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'status', 'priority'))
|
||||
types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'))
|
||||
showtypes = set(('dnd', 'chat', 'xa', 'away'))
|
||||
sub_interfaces = set(('status', 'priority'))
|
||||
name = 'presence'
|
||||
namespace = 'jabber:client'
|
||||
|
||||
def getShowElement(self):
|
||||
return self.xml.find("{%s}show" % self.namespace)
|
||||
"""
|
||||
XMPP's <presence> stanza allows entities to know the status of other
|
||||
clients and components. Since it is currently the only multi-cast
|
||||
stanza in XMPP, many extensions add more information to <presence>
|
||||
stanzas to broadcast to every entry in the roster, such as
|
||||
capabilities, music choices, or locations (XEP-0115: Entity Capabilities
|
||||
and XEP-0163: Personal Eventing Protocol).
|
||||
|
||||
def setType(self, value):
|
||||
show = self.getShowElement()
|
||||
if value in self.types:
|
||||
if show is not None:
|
||||
self.xml.remove(show)
|
||||
if value == 'available':
|
||||
value = ''
|
||||
self._setAttr('type', value)
|
||||
elif value in self.showtypes:
|
||||
if show is None:
|
||||
show = ET.Element("{%s}show" % self.namespace)
|
||||
self.xml.append(show)
|
||||
show.text = value
|
||||
return self
|
||||
Since <presence> stanzas are broadcast when an XMPP entity changes
|
||||
its status, the bulk of the traffic in an XMPP network will be from
|
||||
<presence> stanzas. Therefore, do not include more information than
|
||||
necessary in a status message or within a <presence> stanza in order
|
||||
to help keep the network running smoothly.
|
||||
|
||||
def setPriority(self, value):
|
||||
self._setSubText('priority', str(value))
|
||||
|
||||
def getPriority(self):
|
||||
p = self._getSubText('priority')
|
||||
if not p: p = 0
|
||||
return int(p)
|
||||
|
||||
def getType(self):
|
||||
out = self._getAttr('type')
|
||||
if not out:
|
||||
show = self.getShowElement()
|
||||
if show is not None:
|
||||
out = show.text
|
||||
if not out or out is None:
|
||||
out = 'available'
|
||||
return out
|
||||
|
||||
def reply(self):
|
||||
if self['type'] == 'unsubscribe':
|
||||
self['type'] = 'unsubscribed'
|
||||
elif self['type'] == 'subscribe':
|
||||
self['type'] = 'subscribed'
|
||||
return StanzaBase.reply(self)
|
||||
Example <presence> stanzas:
|
||||
<presence />
|
||||
|
||||
<presence from="user@example.com">
|
||||
<show>away</show>
|
||||
<status>Getting lunch.</status>
|
||||
<priority>5</priority>
|
||||
</presence>
|
||||
|
||||
<presence type="unavailable" />
|
||||
|
||||
<presence to="user@otherhost.com" type="subscribe" />
|
||||
|
||||
Stanza Interface:
|
||||
priority -- A value used by servers to determine message routing.
|
||||
show -- The type of status, such as away or available for chat.
|
||||
status -- Custom, human readable status message.
|
||||
|
||||
Attributes:
|
||||
types -- One of: available, unavailable, error, probe,
|
||||
subscribe, subscribed, unsubscribe,
|
||||
and unsubscribed.
|
||||
showtypes -- One of: away, chat, dnd, and xa.
|
||||
|
||||
Methods:
|
||||
setup -- Overrides StanzaBase.setup
|
||||
reply -- Overrides StanzaBase.reply
|
||||
set_show -- Set the value of the <show> element.
|
||||
get_type -- Get the value of the type attribute or <show> element.
|
||||
set_type -- Set the value of the type attribute or <show> element.
|
||||
get_priority -- Get the value of the <priority> element.
|
||||
set_priority -- Set the value of the <priority> element.
|
||||
"""
|
||||
|
||||
namespace = 'jabber:client'
|
||||
name = 'presence'
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'show',
|
||||
'status', 'priority'))
|
||||
sub_interfaces = set(('show', 'status', 'priority'))
|
||||
plugin_attrib = name
|
||||
|
||||
types = set(('available', 'unavailable', 'error', 'probe', 'subscribe',
|
||||
'subscribed', 'unsubscribe', 'unsubscribed'))
|
||||
showtypes = set(('dnd', 'chat', 'xa', 'away'))
|
||||
|
||||
def exception(self, e):
|
||||
"""
|
||||
Override exception passback for presence.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_show(self, show):
|
||||
"""
|
||||
Set the value of the <show> element.
|
||||
|
||||
Arguments:
|
||||
show -- Must be one of: away, chat, dnd, or xa.
|
||||
"""
|
||||
if show is None:
|
||||
self._del_sub('show')
|
||||
elif show in self.showtypes:
|
||||
self._set_sub_text('show', text=show)
|
||||
return self
|
||||
|
||||
def get_type(self):
|
||||
"""
|
||||
Return the value of the <presence> stanza's type attribute, or
|
||||
the value of the <show> element.
|
||||
"""
|
||||
out = self._get_attr('type')
|
||||
if not out:
|
||||
out = self['show']
|
||||
if not out or out is None:
|
||||
out = 'available'
|
||||
return out
|
||||
|
||||
def set_type(self, value):
|
||||
"""
|
||||
Set the type attribute's value, and the <show> element
|
||||
if applicable.
|
||||
|
||||
Arguments:
|
||||
value -- Must be in either self.types or self.showtypes.
|
||||
"""
|
||||
if value in self.types:
|
||||
self['show'] = None
|
||||
if value == 'available':
|
||||
value = ''
|
||||
self._set_attr('type', value)
|
||||
elif value in self.showtypes:
|
||||
self['show'] = value
|
||||
return self
|
||||
|
||||
def del_type(self):
|
||||
"""
|
||||
Remove both the type attribute and the <show> element.
|
||||
"""
|
||||
self._del_attr('type')
|
||||
self._del_sub('show')
|
||||
|
||||
def set_priority(self, value):
|
||||
"""
|
||||
Set the entity's priority value. Some server use priority to
|
||||
determine message routing behavior.
|
||||
|
||||
Bot clients should typically use a priority of 0 if the same
|
||||
JID is used elsewhere by a human-interacting client.
|
||||
|
||||
Arguments:
|
||||
value -- An integer value greater than or equal to 0.
|
||||
"""
|
||||
self._set_sub_text('priority', text=str(value))
|
||||
|
||||
def get_priority(self):
|
||||
"""
|
||||
Return the value of the <presence> element as an integer.
|
||||
"""
|
||||
p = self._get_sub_text('priority')
|
||||
if not p:
|
||||
p = 0
|
||||
try:
|
||||
return int(p)
|
||||
except ValueError:
|
||||
# The priority is not a number: we consider it 0 as a default
|
||||
return 0
|
||||
|
||||
def reply(self, clear=True):
|
||||
"""
|
||||
Set the appropriate presence reply type.
|
||||
|
||||
Overrides StanzaBase.reply.
|
||||
|
||||
Arguments:
|
||||
clear -- Indicates if the stanza contents should be removed
|
||||
before replying. Defaults to True.
|
||||
"""
|
||||
if self['type'] == 'unsubscribe':
|
||||
self['type'] = 'unsubscribed'
|
||||
elif self['type'] == 'subscribe':
|
||||
self['type'] = 'subscribed'
|
||||
return StanzaBase.reply(self, clear)
|
||||
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Presence.setShow = Presence.set_show
|
||||
Presence.getType = Presence.get_type
|
||||
Presence.setType = Presence.set_type
|
||||
Presence.delType = Presence.get_type
|
||||
Presence.getPriority = Presence.get_priority
|
||||
Presence.setPriority = Presence.set_priority
|
||||
|
||||
@@ -3,30 +3,69 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import StanzaBase
|
||||
from xml.etree import cElementTree as ET
|
||||
from . error import Error
|
||||
from .. exceptions import XMPPError
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from sleekxmpp.exceptions import XMPPError
|
||||
from sleekxmpp.stanza import Error
|
||||
from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RootStanza(StanzaBase):
|
||||
|
||||
def exception(self, e): #called when a handler raises an exception
|
||||
self.reply()
|
||||
if isinstance(e, XMPPError): # we raised this deliberately
|
||||
self['error']['condition'] = e.condition
|
||||
self['error']['text'] = e.text
|
||||
if e.extension is not None: # extended error tag
|
||||
extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args)
|
||||
self['error'].xml.append(extxml)
|
||||
self['error']['type'] = e.etype
|
||||
else: # we probably didn't raise this on purpose, so send back a traceback
|
||||
self['error']['condition'] = 'undefined-condition'
|
||||
self['error']['text'] = traceback.format_tb(e.__traceback__)
|
||||
self.send()
|
||||
"""
|
||||
A top-level XMPP stanza in an XMLStream.
|
||||
|
||||
# all jabber:client root stanzas should have the error plugin
|
||||
RootStanza.plugin_attrib_map['error'] = Error
|
||||
RootStanza.plugin_tag_map["{%s}%s" % (Error.namespace, Error.name)] = Error
|
||||
The RootStanza class provides a more XMPP specific exception
|
||||
handler than provided by the generic StanzaBase class.
|
||||
|
||||
Methods:
|
||||
exception -- Overrides StanzaBase.exception
|
||||
"""
|
||||
|
||||
def exception(self, e):
|
||||
"""
|
||||
Create and send an error reply.
|
||||
|
||||
Typically called when an event handler raises an exception.
|
||||
The error's type and text content are based on the exception
|
||||
object's type and content.
|
||||
|
||||
Overrides StanzaBase.exception.
|
||||
|
||||
Arguments:
|
||||
e -- Exception object
|
||||
"""
|
||||
if isinstance(e, XMPPError):
|
||||
self.reply(clear=e.clear)
|
||||
# We raised this deliberately
|
||||
self['error']['condition'] = e.condition
|
||||
self['error']['text'] = e.text
|
||||
if e.extension is not None:
|
||||
# Extended error tag
|
||||
extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension),
|
||||
e.extension_args)
|
||||
self['error'].append(extxml)
|
||||
self['error']['type'] = e.etype
|
||||
self.send()
|
||||
else:
|
||||
self.reply()
|
||||
# We probably didn't raise this on purpose, so send an error stanza
|
||||
self['error']['condition'] = 'undefined-condition'
|
||||
self['error']['text'] = "SleekXMPP got into trouble."
|
||||
self.send()
|
||||
# log the error
|
||||
log.exception('Error handling {%s}%s stanza' %
|
||||
(self.namespace, self.name))
|
||||
# Finally raise the exception, so it can be handled (or not)
|
||||
# at a higher level by using sys.excepthook.
|
||||
raise e
|
||||
|
||||
register_stanza_plugin(RootStanza, Error)
|
||||
|
||||
@@ -3,51 +3,112 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
from .. xmlstream.stanzabase import ElementBase, ET, JID
|
||||
import logging
|
||||
|
||||
from sleekxmpp.stanza import Iq
|
||||
from sleekxmpp.xmlstream import JID
|
||||
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
|
||||
|
||||
|
||||
class Roster(ElementBase):
|
||||
namespace = 'jabber:iq:roster'
|
||||
name = 'query'
|
||||
plugin_attrib = 'roster'
|
||||
interfaces = set(('items',))
|
||||
sub_interfaces = set()
|
||||
|
||||
def setItems(self, items):
|
||||
self.delItems()
|
||||
for jid in items:
|
||||
ijid = str(jid)
|
||||
item = ET.Element('{jabber:iq:roster}item', {'jid': ijid})
|
||||
if 'subscription' in items[jid]:
|
||||
item.attrib['subscription'] = items[jid]['subscription']
|
||||
if 'name' in items[jid]:
|
||||
item.attrib['name'] = items[jid]['name']
|
||||
if 'groups' in items[jid]:
|
||||
for group in items[jid]['groups']:
|
||||
groupxml = ET.Element('{jabber:iq:roster}group')
|
||||
groupxml.text = group
|
||||
item.append(groupxml)
|
||||
self.xml.append(item)
|
||||
return self
|
||||
|
||||
def getItems(self):
|
||||
items = {}
|
||||
itemsxml = self.xml.findall('{jabber:iq:roster}item')
|
||||
if itemsxml is not None:
|
||||
for itemxml in itemsxml:
|
||||
item = {}
|
||||
item['name'] = itemxml.get('name', '')
|
||||
item['subscription'] = itemxml.get('subscription', '')
|
||||
item['groups'] = []
|
||||
groupsxml = itemxml.findall('{jabber:iq:roster}group')
|
||||
if groupsxml is not None:
|
||||
for groupxml in groupsxml:
|
||||
item['groups'].append(groupxml.text)
|
||||
items[itemxml.get('jid')] = item
|
||||
return items
|
||||
|
||||
def delItems(self):
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
"""
|
||||
Example roster stanzas:
|
||||
<iq type="set">
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" subscription="both" name="User">
|
||||
<group>Friends</group>
|
||||
</item>
|
||||
</query>
|
||||
</iq>
|
||||
|
||||
Stanza Inteface:
|
||||
items -- A dictionary of roster entries contained
|
||||
in the stanza.
|
||||
|
||||
Methods:
|
||||
get_items -- Return a dictionary of roster entries.
|
||||
set_items -- Add <item> elements.
|
||||
del_items -- Remove all <item> elements.
|
||||
"""
|
||||
|
||||
namespace = 'jabber:iq:roster'
|
||||
name = 'query'
|
||||
plugin_attrib = 'roster'
|
||||
interfaces = set(('items',))
|
||||
|
||||
def set_items(self, items):
|
||||
"""
|
||||
Set the roster entries in the <roster> stanza.
|
||||
|
||||
Uses a dictionary using JIDs as keys, where each entry is itself
|
||||
a dictionary that contains:
|
||||
name -- An alias or nickname for the JID.
|
||||
subscription -- The subscription type. Can be one of 'to',
|
||||
'from', 'both', 'none', or 'remove'.
|
||||
groups -- A list of group names to which the JID
|
||||
has been assigned.
|
||||
|
||||
Arguments:
|
||||
items -- A dictionary of roster entries.
|
||||
"""
|
||||
self.del_items()
|
||||
for jid in items:
|
||||
ijid = str(jid)
|
||||
item = ET.Element('{jabber:iq:roster}item', {'jid': ijid})
|
||||
if 'subscription' in items[jid]:
|
||||
item.attrib['subscription'] = items[jid]['subscription']
|
||||
if 'name' in items[jid]:
|
||||
name = items[jid]['name']
|
||||
if name is not None:
|
||||
item.attrib['name'] = name
|
||||
if 'groups' in items[jid]:
|
||||
for group in items[jid]['groups']:
|
||||
groupxml = ET.Element('{jabber:iq:roster}group')
|
||||
groupxml.text = group
|
||||
item.append(groupxml)
|
||||
self.xml.append(item)
|
||||
return self
|
||||
|
||||
def get_items(self):
|
||||
"""
|
||||
Return a dictionary of roster entries.
|
||||
|
||||
Each item is keyed using its JID, and contains:
|
||||
name -- An assigned alias or nickname for the JID.
|
||||
subscription -- The subscription type. Can be one of 'to',
|
||||
'from', 'both', 'none', or 'remove'.
|
||||
groups -- A list of group names to which the JID has
|
||||
been assigned.
|
||||
"""
|
||||
items = {}
|
||||
itemsxml = self.xml.findall('{jabber:iq:roster}item')
|
||||
if itemsxml is not None:
|
||||
for itemxml in itemsxml:
|
||||
item = {}
|
||||
item['name'] = itemxml.get('name', '')
|
||||
item['subscription'] = itemxml.get('subscription', '')
|
||||
item['groups'] = []
|
||||
groupsxml = itemxml.findall('{jabber:iq:roster}group')
|
||||
if groupsxml is not None:
|
||||
for groupxml in groupsxml:
|
||||
item['groups'].append(groupxml.text)
|
||||
items[itemxml.get('jid')] = item
|
||||
return items
|
||||
|
||||
def del_items(self):
|
||||
"""
|
||||
Remove all <item> elements from the roster stanza.
|
||||
"""
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
|
||||
|
||||
register_stanza_plugin(Iq, Roster)
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Roster.setItems = Roster.set_items
|
||||
Roster.getItems = Roster.get_items
|
||||
Roster.delItems = Roster.del_items
|
||||
|
||||
69
sleekxmpp/stanza/stream_error.py
Normal file
69
sleekxmpp/stanza/stream_error.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.stanza.error import Error
|
||||
from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
|
||||
|
||||
class StreamError(Error, StanzaBase):
|
||||
|
||||
"""
|
||||
XMPP stanzas of type 'error' should include an <error> stanza that
|
||||
describes the nature of the error and how it should be handled.
|
||||
|
||||
Use the 'XEP-0086: Error Condition Mappings' plugin to include error
|
||||
codes used in older XMPP versions.
|
||||
|
||||
The stream:error stanza is used to provide more information for
|
||||
error that occur with the underlying XML stream itself, and not
|
||||
a particular stanza.
|
||||
|
||||
Note: The StreamError stanza is mostly the same as the normal
|
||||
Error stanza, but with different namespaces and
|
||||
condition names.
|
||||
|
||||
Example error stanza:
|
||||
<stream:error>
|
||||
<not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams" />
|
||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-streams">
|
||||
XML was not well-formed.
|
||||
</text>
|
||||
</stream:error>
|
||||
|
||||
Stanza Interface:
|
||||
condition -- The name of the condition element.
|
||||
text -- Human readable description of the error.
|
||||
|
||||
Attributes:
|
||||
conditions -- The set of allowable error condition elements.
|
||||
condition_ns -- The namespace for the condition element.
|
||||
|
||||
Methods:
|
||||
setup -- Overrides ElementBase.setup.
|
||||
get_condition -- Retrieve the name of the condition element.
|
||||
set_condition -- Add a condition element.
|
||||
del_condition -- Remove the condition element.
|
||||
get_text -- Retrieve the contents of the <text> element.
|
||||
set_text -- Set the contents of the <text> element.
|
||||
del_text -- Remove the <text> element.
|
||||
"""
|
||||
|
||||
namespace = 'http://etherx.jabber.org/streams'
|
||||
interfaces = set(('condition', 'text'))
|
||||
conditions = set((
|
||||
'bad-format', 'bad-namespace-prefix', 'conflict',
|
||||
'connection-timeout', 'host-gone', 'host-unknown',
|
||||
'improper-addressing', 'internal-server-error', 'invalid-from',
|
||||
'invalid-namespace', 'invalid-xml', 'not-authorized',
|
||||
'not-well-formed', 'policy-violation', 'remote-connection-failed',
|
||||
'reset', 'resource-constraint', 'restricted-xml', 'see-other-host',
|
||||
'system-shutdown', 'undefined-condition', 'unsupported-encoding',
|
||||
'unsupported-feature', 'unsupported-stanza-type',
|
||||
'unsupported-version'))
|
||||
condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams'
|
||||
11
sleekxmpp/test/__init__.py
Normal file
11
sleekxmpp/test/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.test.mocksocket import TestSocket
|
||||
from sleekxmpp.test.livesocket import TestLiveSocket
|
||||
from sleekxmpp.test.sleektest import *
|
||||
174
sleekxmpp/test/livesocket.py
Normal file
174
sleekxmpp/test/livesocket.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
|
||||
class TestLiveSocket(object):
|
||||
|
||||
"""
|
||||
A live test socket that reads and writes to queues in
|
||||
addition to an actual networking socket.
|
||||
|
||||
Methods:
|
||||
next_sent -- Return the next sent stanza.
|
||||
next_recv -- Return the next received stanza.
|
||||
recv_data -- Dummy method to have same interface as TestSocket.
|
||||
recv -- Read the next stanza from the socket.
|
||||
send -- Write a stanza to the socket.
|
||||
makefile -- Dummy call, returns self.
|
||||
read -- Read the next stanza from the socket.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Create a new, live test socket.
|
||||
|
||||
Arguments:
|
||||
Same as arguments for socket.socket
|
||||
"""
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.recv_buffer = []
|
||||
self.recv_queue = queue.Queue()
|
||||
self.send_queue = queue.Queue()
|
||||
self.send_queue_lock = threading.Lock()
|
||||
self.recv_queue_lock = threading.Lock()
|
||||
self.is_live = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Return attribute values of internal, live socket.
|
||||
|
||||
Arguments:
|
||||
name -- Name of the attribute requested.
|
||||
"""
|
||||
|
||||
return getattr(self.socket, name)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Testing Interface
|
||||
|
||||
def disconnect_errror(self):
|
||||
"""
|
||||
Used to simulate a socket disconnection error.
|
||||
|
||||
Not used by live sockets.
|
||||
"""
|
||||
try:
|
||||
self.socket.shutdown()
|
||||
self.socket.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def next_sent(self, timeout=None):
|
||||
"""
|
||||
Get the next stanza that has been sent.
|
||||
|
||||
Arguments:
|
||||
timeout -- Optional timeout for waiting for a new value.
|
||||
"""
|
||||
args = {'block': False}
|
||||
if timeout is not None:
|
||||
args = {'block': True, 'timeout': timeout}
|
||||
try:
|
||||
return self.send_queue.get(**args)
|
||||
except:
|
||||
return None
|
||||
|
||||
def next_recv(self, timeout=None):
|
||||
"""
|
||||
Get the next stanza that has been received.
|
||||
|
||||
Arguments:
|
||||
timeout -- Optional timeout for waiting for a new value.
|
||||
"""
|
||||
args = {'block': False}
|
||||
if timeout is not None:
|
||||
args = {'block': True, 'timeout': timeout}
|
||||
try:
|
||||
if self.recv_buffer:
|
||||
return self.recv_buffer.pop(0)
|
||||
else:
|
||||
return self.recv_queue.get(**args)
|
||||
except:
|
||||
return None
|
||||
|
||||
def recv_data(self, data):
|
||||
"""
|
||||
Add data to a receive buffer for cases when more than a single stanza
|
||||
was received.
|
||||
"""
|
||||
self.recv_buffer.append(data)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Socket Interface
|
||||
|
||||
def recv(self, *args, **kwargs):
|
||||
"""
|
||||
Read data from the socket.
|
||||
|
||||
Store a copy in the receive queue.
|
||||
|
||||
Arguments:
|
||||
Placeholders. Same as for socket.recv.
|
||||
"""
|
||||
data = self.socket.recv(*args, **kwargs)
|
||||
with self.recv_queue_lock:
|
||||
self.recv_queue.put(data)
|
||||
return data
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Send data on the socket.
|
||||
|
||||
Store a copy in the send queue.
|
||||
|
||||
Arguments:
|
||||
data -- String value to write.
|
||||
"""
|
||||
with self.send_queue_lock:
|
||||
self.send_queue.put(data)
|
||||
self.socket.send(data)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File Socket
|
||||
|
||||
def makefile(self, *args, **kwargs):
|
||||
"""
|
||||
File socket version to use with ElementTree.
|
||||
|
||||
Arguments:
|
||||
Placeholders, same as socket.makefile()
|
||||
"""
|
||||
return self
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
"""
|
||||
Implement the file socket read interface.
|
||||
|
||||
Arguments:
|
||||
Placeholders, same as socket.recv()
|
||||
"""
|
||||
return self.recv(*args, **kwargs)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Empty the send queue, typically done once the session has started to
|
||||
remove the feature negotiation and log in stanzas.
|
||||
"""
|
||||
with self.send_queue_lock:
|
||||
for i in range(0, self.send_queue.qsize()):
|
||||
self.send_queue.get(block=False)
|
||||
with self.recv_queue_lock:
|
||||
for i in range(0, self.recv_queue.qsize()):
|
||||
self.recv_queue.get(block=False)
|
||||
154
sleekxmpp/test/mocksocket.py
Normal file
154
sleekxmpp/test/mocksocket.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import socket
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
|
||||
class TestSocket(object):
|
||||
|
||||
"""
|
||||
A dummy socket that reads and writes to queues instead
|
||||
of an actual networking socket.
|
||||
|
||||
Methods:
|
||||
next_sent -- Return the next sent stanza.
|
||||
recv_data -- Make a stanza available to read next.
|
||||
recv -- Read the next stanza from the socket.
|
||||
send -- Write a stanza to the socket.
|
||||
makefile -- Dummy call, returns self.
|
||||
read -- Read the next stanza from the socket.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Create a new test socket.
|
||||
|
||||
Arguments:
|
||||
Same as arguments for socket.socket
|
||||
"""
|
||||
self.socket = socket.socket(*args, **kwargs)
|
||||
self.recv_queue = queue.Queue()
|
||||
self.send_queue = queue.Queue()
|
||||
self.is_live = False
|
||||
self.disconnected = False
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Return attribute values of internal, dummy socket.
|
||||
|
||||
Some attributes and methods are disabled to prevent the
|
||||
socket from connecting to the network.
|
||||
|
||||
Arguments:
|
||||
name -- Name of the attribute requested.
|
||||
"""
|
||||
|
||||
def dummy(*args):
|
||||
"""Method to do nothing and prevent actual socket connections."""
|
||||
return None
|
||||
|
||||
overrides = {'connect': dummy,
|
||||
'close': dummy,
|
||||
'shutdown': dummy}
|
||||
|
||||
return overrides.get(name, getattr(self.socket, name))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Testing Interface
|
||||
|
||||
def next_sent(self, timeout=None):
|
||||
"""
|
||||
Get the next stanza that has been 'sent'.
|
||||
|
||||
Arguments:
|
||||
timeout -- Optional timeout for waiting for a new value.
|
||||
"""
|
||||
args = {'block': False}
|
||||
if timeout is not None:
|
||||
args = {'block': True, 'timeout': timeout}
|
||||
try:
|
||||
return self.send_queue.get(**args)
|
||||
except:
|
||||
return None
|
||||
|
||||
def recv_data(self, data):
|
||||
"""
|
||||
Add data to the receiving queue.
|
||||
|
||||
Arguments:
|
||||
data -- String data to 'write' to the socket to be received
|
||||
by the XMPP client.
|
||||
"""
|
||||
self.recv_queue.put(data)
|
||||
|
||||
def disconnect_error(self):
|
||||
"""
|
||||
Simulate a disconnect error by raising a socket.error exception
|
||||
for any current or further socket operations.
|
||||
"""
|
||||
self.disconnected = True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Socket Interface
|
||||
|
||||
def recv(self, *args, **kwargs):
|
||||
"""
|
||||
Read a value from the received queue.
|
||||
|
||||
Arguments:
|
||||
Placeholders. Same as for socket.Socket.recv.
|
||||
"""
|
||||
if self.disconnected:
|
||||
raise socket.error
|
||||
return self.read(block=True)
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Send data by placing it in the send queue.
|
||||
|
||||
Arguments:
|
||||
data -- String value to write.
|
||||
"""
|
||||
if self.disconnected:
|
||||
raise socket.error
|
||||
self.send_queue.put(data)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File Socket
|
||||
|
||||
def makefile(self, *args, **kwargs):
|
||||
"""
|
||||
File socket version to use with ElementTree.
|
||||
|
||||
Arguments:
|
||||
Placeholders, same as socket.Socket.makefile()
|
||||
"""
|
||||
return self
|
||||
|
||||
def read(self, block=True, timeout=None, **kwargs):
|
||||
"""
|
||||
Implement the file socket interface.
|
||||
|
||||
Arguments:
|
||||
block -- Indicate if the read should block until a
|
||||
value is ready.
|
||||
timeout -- Time in seconds a block should last before
|
||||
returning None.
|
||||
"""
|
||||
if self.disconnected:
|
||||
raise socket.error
|
||||
if timeout is not None:
|
||||
block = True
|
||||
try:
|
||||
return self.recv_queue.get(block, timeout)
|
||||
except:
|
||||
return None
|
||||
725
sleekxmpp/test/sleektest.py
Normal file
725
sleekxmpp/test/sleektest.py
Normal file
@@ -0,0 +1,725 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
try:
|
||||
import Queue as queue
|
||||
except:
|
||||
import queue
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import ClientXMPP, ComponentXMPP
|
||||
from sleekxmpp.stanza import Message, Iq, Presence
|
||||
from sleekxmpp.test import TestSocket, TestLiveSocket
|
||||
from sleekxmpp.xmlstream import ET, register_stanza_plugin
|
||||
from sleekxmpp.xmlstream import ElementBase, StanzaBase
|
||||
from sleekxmpp.xmlstream.tostring import tostring
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
|
||||
from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
|
||||
|
||||
|
||||
class SleekTest(unittest.TestCase):
|
||||
|
||||
"""
|
||||
A SleekXMPP specific TestCase class that provides
|
||||
methods for comparing message, iq, and presence stanzas.
|
||||
|
||||
Methods:
|
||||
Message -- Create a Message stanza object.
|
||||
Iq -- Create an Iq stanza object.
|
||||
Presence -- Create a Presence stanza object.
|
||||
check_jid -- Check a JID and its component parts.
|
||||
check -- Compare a stanza against an XML string.
|
||||
stream_start -- Initialize a dummy XMPP client.
|
||||
stream_close -- Disconnect the XMPP client.
|
||||
make_header -- Create a stream header.
|
||||
send_header -- Check that the given header has been sent.
|
||||
send_feature -- Send a raw XML element.
|
||||
send -- Check that the XMPP client sent the given
|
||||
generic stanza.
|
||||
recv -- Queue data for XMPP client to receive, or
|
||||
verify the data that was received from a
|
||||
live connection.
|
||||
recv_header -- Check that a given stream header
|
||||
was received.
|
||||
recv_feature -- Check that a given, raw XML element
|
||||
was recveived.
|
||||
fix_namespaces -- Add top-level namespace to an XML object.
|
||||
compare -- Compare XML objects against each other.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
self.xmpp = None
|
||||
|
||||
def runTest(self):
|
||||
pass
|
||||
|
||||
def parse_xml(self, xml_string):
|
||||
try:
|
||||
xml = ET.fromstring(xml_string)
|
||||
return xml
|
||||
except SyntaxError as e:
|
||||
if 'unbound' in e.msg:
|
||||
known_prefixes = {
|
||||
'stream': 'http://etherx.jabber.org/streams'}
|
||||
|
||||
prefix = xml_string.split('<')[1].split(':')[0]
|
||||
if prefix in known_prefixes:
|
||||
xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
|
||||
prefix,
|
||||
known_prefixes[prefix],
|
||||
xml_string)
|
||||
xml = self.parse_xml(xml_string)
|
||||
xml = xml.getchildren()[0]
|
||||
return xml
|
||||
else:
|
||||
self.fail("XML data was mal-formed:\n%s" % xml_string)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Shortcut methods for creating stanza objects
|
||||
|
||||
def Message(self, *args, **kwargs):
|
||||
"""
|
||||
Create a Message stanza.
|
||||
|
||||
Uses same arguments as StanzaBase.__init__
|
||||
|
||||
Arguments:
|
||||
xml -- An XML object to use for the Message's values.
|
||||
"""
|
||||
return Message(self.xmpp, *args, **kwargs)
|
||||
|
||||
def Iq(self, *args, **kwargs):
|
||||
"""
|
||||
Create an Iq stanza.
|
||||
|
||||
Uses same arguments as StanzaBase.__init__
|
||||
|
||||
Arguments:
|
||||
xml -- An XML object to use for the Iq's values.
|
||||
"""
|
||||
return Iq(self.xmpp, *args, **kwargs)
|
||||
|
||||
def Presence(self, *args, **kwargs):
|
||||
"""
|
||||
Create a Presence stanza.
|
||||
|
||||
Uses same arguments as StanzaBase.__init__
|
||||
|
||||
Arguments:
|
||||
xml -- An XML object to use for the Iq's values.
|
||||
"""
|
||||
return Presence(self.xmpp, *args, **kwargs)
|
||||
|
||||
def check_jid(self, jid, user=None, domain=None, resource=None,
|
||||
bare=None, full=None, string=None):
|
||||
"""
|
||||
Verify the components of a JID.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID object to test.
|
||||
user -- Optional. The user name portion of the JID.
|
||||
domain -- Optional. The domain name portion of the JID.
|
||||
resource -- Optional. The resource portion of the JID.
|
||||
bare -- Optional. The bare JID.
|
||||
full -- Optional. The full JID.
|
||||
string -- Optional. The string version of the JID.
|
||||
"""
|
||||
if user is not None:
|
||||
self.assertEqual(jid.user, user,
|
||||
"User does not match: %s" % jid.user)
|
||||
if domain is not None:
|
||||
self.assertEqual(jid.domain, domain,
|
||||
"Domain does not match: %s" % jid.domain)
|
||||
if resource is not None:
|
||||
self.assertEqual(jid.resource, resource,
|
||||
"Resource does not match: %s" % jid.resource)
|
||||
if bare is not None:
|
||||
self.assertEqual(jid.bare, bare,
|
||||
"Bare JID does not match: %s" % jid.bare)
|
||||
if full is not None:
|
||||
self.assertEqual(jid.full, full,
|
||||
"Full JID does not match: %s" % jid.full)
|
||||
if string is not None:
|
||||
self.assertEqual(str(jid), string,
|
||||
"String does not match: %s" % str(jid))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Methods for comparing stanza objects to XML strings
|
||||
|
||||
def check(self, stanza, criteria, method='exact',
|
||||
defaults=None, use_values=True):
|
||||
"""
|
||||
Create and compare several stanza objects to a correct XML string.
|
||||
|
||||
If use_values is False, tests using stanza.values will not be used.
|
||||
|
||||
Some stanzas provide default values for some interfaces, but
|
||||
these defaults can be problematic for testing since they can easily
|
||||
be forgotten when supplying the XML string. A list of interfaces that
|
||||
use defaults may be provided and the generated stanzas will use the
|
||||
default values for those interfaces if needed.
|
||||
|
||||
However, correcting the supplied XML is not possible for interfaces
|
||||
that add or remove XML elements. Only interfaces that map to XML
|
||||
attributes may be set using the defaults parameter. The supplied XML
|
||||
must take into account any extra elements that are included by default.
|
||||
|
||||
Arguments:
|
||||
stanza -- The stanza object to test.
|
||||
criteria -- An expression the stanza must match against.
|
||||
method -- The type of matching to use; one of:
|
||||
'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
|
||||
Defaults to the value of self.match_method.
|
||||
defaults -- A list of stanza interfaces that have default
|
||||
values. These interfaces will be set to their
|
||||
defaults for the given and generated stanzas to
|
||||
prevent unexpected test failures.
|
||||
use_values -- Indicates if testing using stanza.values should
|
||||
be used. Defaults to True.
|
||||
"""
|
||||
if method is None and hasattr(self, 'match_method'):
|
||||
method = getattr(self, 'match_method')
|
||||
|
||||
if method != 'exact':
|
||||
matchers = {'stanzapath': StanzaPath,
|
||||
'xpath': MatchXPath,
|
||||
'mask': MatchXMLMask,
|
||||
'id': MatcherId}
|
||||
Matcher = matchers.get(method, None)
|
||||
if Matcher is None:
|
||||
raise ValueError("Unknown matching method.")
|
||||
test = Matcher(criteria)
|
||||
self.failUnless(test.match(stanza),
|
||||
"Stanza did not match using %s method:\n" % method + \
|
||||
"Criteria:\n%s\n" % str(criteria) + \
|
||||
"Stanza:\n%s" % str(stanza))
|
||||
else:
|
||||
stanza_class = stanza.__class__
|
||||
if not isinstance(criteria, ElementBase):
|
||||
xml = self.parse_xml(criteria)
|
||||
else:
|
||||
xml = criteria.xml
|
||||
|
||||
# Ensure that top level namespaces are used, even if they
|
||||
# were not provided.
|
||||
self.fix_namespaces(stanza.xml, 'jabber:client')
|
||||
self.fix_namespaces(xml, 'jabber:client')
|
||||
|
||||
stanza2 = stanza_class(xml=xml)
|
||||
|
||||
if use_values:
|
||||
# Using stanza.values will add XML for any interface that
|
||||
# has a default value. We need to set those defaults on
|
||||
# the existing stanzas and XML so that they will compare
|
||||
# correctly.
|
||||
default_stanza = stanza_class()
|
||||
if defaults is None:
|
||||
known_defaults = {
|
||||
Message: ['type'],
|
||||
Presence: ['priority']
|
||||
}
|
||||
defaults = known_defaults.get(stanza_class, [])
|
||||
for interface in defaults:
|
||||
stanza[interface] = stanza[interface]
|
||||
stanza2[interface] = stanza2[interface]
|
||||
# Can really only automatically add defaults for top
|
||||
# level attribute values. Anything else must be accounted
|
||||
# for in the provided XML string.
|
||||
if interface not in xml.attrib:
|
||||
if interface in default_stanza.xml.attrib:
|
||||
value = default_stanza.xml.attrib[interface]
|
||||
xml.attrib[interface] = value
|
||||
|
||||
values = stanza2.values
|
||||
stanza3 = stanza_class()
|
||||
stanza3.values = values
|
||||
|
||||
debug = "Three methods for creating stanzas do not match.\n"
|
||||
debug += "Given XML:\n%s\n" % tostring(xml)
|
||||
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
|
||||
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
|
||||
debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
|
||||
result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
|
||||
else:
|
||||
debug = "Two methods for creating stanzas do not match.\n"
|
||||
debug += "Given XML:\n%s\n" % tostring(xml)
|
||||
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
|
||||
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
|
||||
result = self.compare(xml, stanza.xml, stanza2.xml)
|
||||
|
||||
self.failUnless(result, debug)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Methods for simulating stanza streams.
|
||||
|
||||
def stream_disconnect(self):
|
||||
"""
|
||||
Simulate a stream disconnection.
|
||||
"""
|
||||
if self.xmpp:
|
||||
self.xmpp.socket.disconnect_error()
|
||||
|
||||
def stream_start(self, mode='client', skip=True, header=None,
|
||||
socket='mock', jid='tester@localhost',
|
||||
password='test', server='localhost',
|
||||
port=5222, plugins=None):
|
||||
"""
|
||||
Initialize an XMPP client or component using a dummy XML stream.
|
||||
|
||||
Arguments:
|
||||
mode -- Either 'client' or 'component'. Defaults to 'client'.
|
||||
skip -- Indicates if the first item in the sent queue (the
|
||||
stream header) should be removed. Tests that wish
|
||||
to test initializing the stream should set this to
|
||||
False. Otherwise, the default of True should be used.
|
||||
socket -- Either 'mock' or 'live' to indicate if the socket
|
||||
should be a dummy, mock socket or a live, functioning
|
||||
socket. Defaults to 'mock'.
|
||||
jid -- The JID to use for the connection.
|
||||
Defaults to 'tester@localhost'.
|
||||
password -- The password to use for the connection.
|
||||
Defaults to 'test'.
|
||||
server -- The name of the XMPP server. Defaults to 'localhost'.
|
||||
port -- The port to use when connecting to the server.
|
||||
Defaults to 5222.
|
||||
plugins -- List of plugins to register. By default, all plugins
|
||||
are loaded.
|
||||
"""
|
||||
if mode == 'client':
|
||||
self.xmpp = ClientXMPP(jid, password)
|
||||
elif mode == 'component':
|
||||
self.xmpp = ComponentXMPP(jid, password,
|
||||
server, port)
|
||||
else:
|
||||
raise ValueError("Unknown XMPP connection mode.")
|
||||
|
||||
# We will use this to wait for the session_start event
|
||||
# for live connections.
|
||||
skip_queue = queue.Queue()
|
||||
|
||||
if socket == 'mock':
|
||||
self.xmpp.set_socket(TestSocket())
|
||||
|
||||
# Simulate connecting for mock sockets.
|
||||
self.xmpp.auto_reconnect = False
|
||||
self.xmpp.is_client = True
|
||||
self.xmpp.state._set_state('connected')
|
||||
|
||||
# Must have the stream header ready for xmpp.process() to work.
|
||||
if not header:
|
||||
header = self.xmpp.stream_header
|
||||
self.xmpp.socket.recv_data(header)
|
||||
elif socket == 'live':
|
||||
self.xmpp.socket_class = TestLiveSocket
|
||||
def wait_for_session(x):
|
||||
self.xmpp.socket.clear()
|
||||
skip_queue.put('started')
|
||||
self.xmpp.add_event_handler('session_start', wait_for_session)
|
||||
self.xmpp.connect()
|
||||
else:
|
||||
raise ValueError("Unknown socket type.")
|
||||
|
||||
if plugins is None:
|
||||
self.xmpp.register_plugins()
|
||||
else:
|
||||
for plugin in plugins:
|
||||
self.xmpp.register_plugin(plugin)
|
||||
self.xmpp.process(threaded=True)
|
||||
if skip:
|
||||
if socket != 'live':
|
||||
# Mark send queue as usable
|
||||
self.xmpp.session_started_event.set()
|
||||
# Clear startup stanzas
|
||||
self.xmpp.socket.next_sent(timeout=1)
|
||||
if mode == 'component':
|
||||
self.xmpp.socket.next_sent(timeout=1)
|
||||
else:
|
||||
skip_queue.get(block=True, timeout=10)
|
||||
|
||||
def make_header(self, sto='',
|
||||
sfrom='',
|
||||
sid='',
|
||||
stream_ns="http://etherx.jabber.org/streams",
|
||||
default_ns="jabber:client",
|
||||
version="1.0",
|
||||
xml_header=True):
|
||||
"""
|
||||
Create a stream header to be received by the test XMPP agent.
|
||||
|
||||
The header must be saved and passed to stream_start.
|
||||
|
||||
Arguments:
|
||||
sto -- The recipient of the stream header.
|
||||
sfrom -- The agent sending the stream header.
|
||||
sid -- The stream's id.
|
||||
stream_ns -- The namespace of the stream's root element.
|
||||
default_ns -- The default stanza namespace.
|
||||
version -- The stream version.
|
||||
xml_header -- Indicates if the XML version header should be
|
||||
appended before the stream header.
|
||||
"""
|
||||
header = '<stream:stream %s>'
|
||||
parts = []
|
||||
if xml_header:
|
||||
header = '<?xml version="1.0"?>' + header
|
||||
if sto:
|
||||
parts.append('to="%s"' % sto)
|
||||
if sfrom:
|
||||
parts.append('from="%s"' % sfrom)
|
||||
if sid:
|
||||
parts.append('id="%s"' % sid)
|
||||
parts.append('version="%s"' % version)
|
||||
parts.append('xmlns:stream="%s"' % stream_ns)
|
||||
parts.append('xmlns="%s"' % default_ns)
|
||||
return header % ' '.join(parts)
|
||||
|
||||
def recv(self, data, defaults=[], method='exact',
|
||||
use_values=True, timeout=1):
|
||||
"""
|
||||
Pass data to the dummy XMPP client as if it came from an XMPP server.
|
||||
|
||||
If using a live connection, verify what the server has sent.
|
||||
|
||||
Arguments:
|
||||
data -- If a dummy socket is being used, the XML that is to
|
||||
be received next. Otherwise it is the criteria used
|
||||
to match against live data that is received.
|
||||
defaults -- A list of stanza interfaces with default values that
|
||||
may interfere with comparisons.
|
||||
method -- Select the type of comparison to use for
|
||||
verifying the received stanza. Options are 'exact',
|
||||
'id', 'stanzapath', 'xpath', and 'mask'.
|
||||
Defaults to the value of self.match_method.
|
||||
use_values -- Indicates if stanza comparisons should test using
|
||||
stanza.values. Defaults to True.
|
||||
timeout -- Time to wait in seconds for data to be received by
|
||||
a live connection.
|
||||
"""
|
||||
if self.xmpp.socket.is_live:
|
||||
# we are working with a live connection, so we should
|
||||
# verify what has been received instead of simulating
|
||||
# receiving data.
|
||||
recv_data = self.xmpp.socket.next_recv(timeout)
|
||||
if recv_data is None:
|
||||
self.fail("No stanza was received.")
|
||||
xml = self.parse_xml(recv_data)
|
||||
self.fix_namespaces(xml, 'jabber:client')
|
||||
stanza = self.xmpp._build_stanza(xml, 'jabber:client')
|
||||
self.check(stanza, data,
|
||||
method=method,
|
||||
defaults=defaults,
|
||||
use_values=use_values)
|
||||
else:
|
||||
# place the data in the dummy socket receiving queue.
|
||||
data = str(data)
|
||||
self.xmpp.socket.recv_data(data)
|
||||
|
||||
def recv_header(self, sto='',
|
||||
sfrom='',
|
||||
sid='',
|
||||
stream_ns="http://etherx.jabber.org/streams",
|
||||
default_ns="jabber:client",
|
||||
version="1.0",
|
||||
xml_header=False,
|
||||
timeout=1):
|
||||
"""
|
||||
Check that a given stream header was received.
|
||||
|
||||
Arguments:
|
||||
sto -- The recipient of the stream header.
|
||||
sfrom -- The agent sending the stream header.
|
||||
sid -- The stream's id. Set to None to ignore.
|
||||
stream_ns -- The namespace of the stream's root element.
|
||||
default_ns -- The default stanza namespace.
|
||||
version -- The stream version.
|
||||
xml_header -- Indicates if the XML version header should be
|
||||
appended before the stream header.
|
||||
timeout -- Length of time to wait in seconds for a
|
||||
response.
|
||||
"""
|
||||
header = self.make_header(sto, sfrom, sid,
|
||||
stream_ns=stream_ns,
|
||||
default_ns=default_ns,
|
||||
version=version,
|
||||
xml_header=xml_header)
|
||||
recv_header = self.xmpp.socket.next_recv(timeout)
|
||||
if recv_header is None:
|
||||
raise ValueError("Socket did not return data.")
|
||||
|
||||
# Apply closing elements so that we can construct
|
||||
# XML objects for comparison.
|
||||
header2 = header + '</stream:stream>'
|
||||
recv_header2 = recv_header + '</stream:stream>'
|
||||
|
||||
xml = self.parse_xml(header2)
|
||||
recv_xml = self.parse_xml(recv_header2)
|
||||
|
||||
if sid is None:
|
||||
# Ignore the id sent by the server since
|
||||
# we can't know in advance what it will be.
|
||||
if 'id' in recv_xml.attrib:
|
||||
del recv_xml.attrib['id']
|
||||
|
||||
# Ignore the xml:lang attribute for now.
|
||||
if 'xml:lang' in recv_xml.attrib:
|
||||
del recv_xml.attrib['xml:lang']
|
||||
xml_ns = 'http://www.w3.org/XML/1998/namespace'
|
||||
if '{%s}lang' % xml_ns in recv_xml.attrib:
|
||||
del recv_xml.attrib['{%s}lang' % xml_ns]
|
||||
|
||||
if recv_xml.getchildren:
|
||||
# We received more than just the header
|
||||
for xml in recv_xml.getchildren():
|
||||
self.xmpp.socket.recv_data(tostring(xml))
|
||||
|
||||
attrib = recv_xml.attrib
|
||||
recv_xml.clear()
|
||||
recv_xml.attrib = attrib
|
||||
|
||||
self.failUnless(
|
||||
self.compare(xml, recv_xml),
|
||||
"Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
|
||||
'%s %s' % (xml.tag, xml.attrib),
|
||||
'%s %s' % (recv_xml.tag, recv_xml.attrib)))
|
||||
|
||||
def recv_feature(self, data, method='mask', use_values=True, timeout=1):
|
||||
"""
|
||||
"""
|
||||
if method is None and hasattr(self, 'match_method'):
|
||||
method = getattr(self, 'match_method')
|
||||
|
||||
if self.xmpp.socket.is_live:
|
||||
# we are working with a live connection, so we should
|
||||
# verify what has been received instead of simulating
|
||||
# receiving data.
|
||||
recv_data = self.xmpp.socket.next_recv(timeout)
|
||||
xml = self.parse_xml(data)
|
||||
recv_xml = self.parse_xml(recv_data)
|
||||
if recv_data is None:
|
||||
self.fail("No stanza was received.")
|
||||
if method == 'exact':
|
||||
self.failUnless(self.compare(xml, recv_xml),
|
||||
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
|
||||
tostring(xml), tostring(recv_xml)))
|
||||
elif method == 'mask':
|
||||
matcher = MatchXMLMask(xml)
|
||||
self.failUnless(matcher.match(recv_xml),
|
||||
"Stanza did not match using %s method:\n" % method + \
|
||||
"Criteria:\n%s\n" % tostring(xml) + \
|
||||
"Stanza:\n%s" % tostring(recv_xml))
|
||||
else:
|
||||
raise ValueError("Uknown matching method: %s" % method)
|
||||
else:
|
||||
# place the data in the dummy socket receiving queue.
|
||||
data = str(data)
|
||||
self.xmpp.socket.recv_data(data)
|
||||
|
||||
def send_header(self, sto='',
|
||||
sfrom='',
|
||||
sid='',
|
||||
stream_ns="http://etherx.jabber.org/streams",
|
||||
default_ns="jabber:client",
|
||||
version="1.0",
|
||||
xml_header=False,
|
||||
timeout=1):
|
||||
"""
|
||||
Check that a given stream header was sent.
|
||||
|
||||
Arguments:
|
||||
sto -- The recipient of the stream header.
|
||||
sfrom -- The agent sending the stream header.
|
||||
sid -- The stream's id.
|
||||
stream_ns -- The namespace of the stream's root element.
|
||||
default_ns -- The default stanza namespace.
|
||||
version -- The stream version.
|
||||
xml_header -- Indicates if the XML version header should be
|
||||
appended before the stream header.
|
||||
timeout -- Length of time to wait in seconds for a
|
||||
response.
|
||||
"""
|
||||
header = self.make_header(sto, sfrom, sid,
|
||||
stream_ns=stream_ns,
|
||||
default_ns=default_ns,
|
||||
version=version,
|
||||
xml_header=xml_header)
|
||||
sent_header = self.xmpp.socket.next_sent(timeout)
|
||||
if sent_header is None:
|
||||
raise ValueError("Socket did not return data.")
|
||||
|
||||
# Apply closing elements so that we can construct
|
||||
# XML objects for comparison.
|
||||
header2 = header + '</stream:stream>'
|
||||
sent_header2 = sent_header + b'</stream:stream>'
|
||||
|
||||
xml = self.parse_xml(header2)
|
||||
sent_xml = self.parse_xml(sent_header2)
|
||||
|
||||
self.failUnless(
|
||||
self.compare(xml, sent_xml),
|
||||
"Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
|
||||
header, sent_header))
|
||||
|
||||
def send_feature(self, data, method='mask', use_values=True, timeout=1):
|
||||
"""
|
||||
"""
|
||||
sent_data = self.xmpp.socket.next_sent(timeout)
|
||||
xml = self.parse_xml(data)
|
||||
sent_xml = self.parse_xml(sent_data)
|
||||
if sent_data is None:
|
||||
self.fail("No stanza was sent.")
|
||||
if method == 'exact':
|
||||
self.failUnless(self.compare(xml, sent_xml),
|
||||
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
|
||||
tostring(xml), tostring(sent_xml)))
|
||||
elif method == 'mask':
|
||||
matcher = MatchXMLMask(xml)
|
||||
self.failUnless(matcher.match(sent_xml),
|
||||
"Stanza did not match using %s method:\n" % method + \
|
||||
"Criteria:\n%s\n" % tostring(xml) + \
|
||||
"Stanza:\n%s" % tostring(sent_xml))
|
||||
else:
|
||||
raise ValueError("Uknown matching method: %s" % method)
|
||||
|
||||
def send(self, data, defaults=None, use_values=True,
|
||||
timeout=.5, method='exact'):
|
||||
"""
|
||||
Check that the XMPP client sent the given stanza XML.
|
||||
|
||||
Extracts the next sent stanza and compares it with the given
|
||||
XML using check.
|
||||
|
||||
Arguments:
|
||||
stanza_class -- The class of the sent stanza object.
|
||||
data -- The XML string of the expected Message stanza,
|
||||
or an equivalent stanza object.
|
||||
use_values -- Modifies the type of tests used by check_message.
|
||||
defaults -- A list of stanza interfaces that have defaults
|
||||
values which may interfere with comparisons.
|
||||
timeout -- Time in seconds to wait for a stanza before
|
||||
failing the check.
|
||||
method -- Select the type of comparison to use for
|
||||
verifying the sent stanza. Options are 'exact',
|
||||
'id', 'stanzapath', 'xpath', and 'mask'.
|
||||
Defaults to the value of self.match_method.
|
||||
"""
|
||||
sent = self.xmpp.socket.next_sent(timeout)
|
||||
if data is None and sent is None:
|
||||
return
|
||||
if data is None and sent is not None:
|
||||
self.fail("Stanza data was sent: %s" % sent)
|
||||
if sent is None:
|
||||
self.fail("No stanza was sent.")
|
||||
|
||||
xml = self.parse_xml(sent)
|
||||
self.fix_namespaces(xml, 'jabber:client')
|
||||
sent = self.xmpp._build_stanza(xml, 'jabber:client')
|
||||
self.check(sent, data,
|
||||
method=method,
|
||||
defaults=defaults,
|
||||
use_values=use_values)
|
||||
|
||||
def stream_close(self):
|
||||
"""
|
||||
Disconnect the dummy XMPP client.
|
||||
|
||||
Can be safely called even if stream_start has not been called.
|
||||
|
||||
Must be placed in the tearDown method of a test class to ensure
|
||||
that the XMPP client is disconnected after an error.
|
||||
"""
|
||||
if hasattr(self, 'xmpp') and self.xmpp is not None:
|
||||
self.xmpp.socket.recv_data(self.xmpp.stream_footer)
|
||||
self.xmpp.disconnect()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# XML Comparison and Cleanup
|
||||
|
||||
def fix_namespaces(self, xml, ns):
|
||||
"""
|
||||
Assign a namespace to an element and any children that
|
||||
don't have a namespace.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML object to fix.
|
||||
ns -- The namespace to add to the XML object.
|
||||
"""
|
||||
if xml.tag.startswith('{'):
|
||||
return
|
||||
xml.tag = '{%s}%s' % (ns, xml.tag)
|
||||
for child in xml.getchildren():
|
||||
self.fix_namespaces(child, ns)
|
||||
|
||||
def compare(self, xml, *other):
|
||||
"""
|
||||
Compare XML objects.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML object to compare against.
|
||||
*other -- The list of XML objects to compare.
|
||||
"""
|
||||
if not other:
|
||||
return False
|
||||
|
||||
# Compare multiple objects
|
||||
if len(other) > 1:
|
||||
for xml2 in other:
|
||||
if not self.compare(xml, xml2):
|
||||
return False
|
||||
return True
|
||||
|
||||
other = other[0]
|
||||
|
||||
# Step 1: Check tags
|
||||
if xml.tag != other.tag:
|
||||
return False
|
||||
|
||||
# Step 2: Check attributes
|
||||
if xml.attrib != other.attrib:
|
||||
return False
|
||||
|
||||
# Step 3: Check text
|
||||
if xml.text is None:
|
||||
xml.text = ""
|
||||
if other.text is None:
|
||||
other.text = ""
|
||||
xml.text = xml.text.strip()
|
||||
other.text = other.text.strip()
|
||||
|
||||
if xml.text != other.text:
|
||||
return False
|
||||
|
||||
# Step 4: Check children count
|
||||
if len(xml.getchildren()) != len(other.getchildren()):
|
||||
return False
|
||||
|
||||
# Step 5: Recursively check children
|
||||
for child in xml:
|
||||
child2s = other.findall("%s" % child.tag)
|
||||
if child2s is None:
|
||||
return False
|
||||
for child2 in child2s:
|
||||
if self.compare(child, child2):
|
||||
break
|
||||
else:
|
||||
return False
|
||||
|
||||
# Step 6: Recursively check children the other way.
|
||||
for child in other:
|
||||
child2s = xml.findall("%s" % child.tag)
|
||||
if child2s is None:
|
||||
return False
|
||||
for child2 in child2s:
|
||||
if self.compare(child, child2):
|
||||
break
|
||||
else:
|
||||
return False
|
||||
|
||||
# Everything matches
|
||||
return True
|
||||
4
sleekxmpp/thirdparty/__init__.py
vendored
Normal file
4
sleekxmpp/thirdparty/__init__.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except:
|
||||
from sleekxmpp.thirdparty.ordereddict import OrderedDict
|
||||
127
sleekxmpp/thirdparty/ordereddict.py
vendored
Normal file
127
sleekxmpp/thirdparty/ordereddict.py
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
# Copyright (c) 2009 Raymond Hettinger
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from UserDict import DictMixin
|
||||
|
||||
class OrderedDict(dict, DictMixin):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__end
|
||||
except AttributeError:
|
||||
self.clear()
|
||||
self.update(*args, **kwds)
|
||||
|
||||
def clear(self):
|
||||
self.__end = end = []
|
||||
end += [None, end, end] # sentinel node for doubly linked list
|
||||
self.__map = {} # key --> [key, prev, next]
|
||||
dict.clear(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
key, prev, next = self.__map.pop(key)
|
||||
prev[2] = next
|
||||
next[1] = prev
|
||||
|
||||
def __iter__(self):
|
||||
end = self.__end
|
||||
curr = end[2]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[2]
|
||||
|
||||
def __reversed__(self):
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[1]
|
||||
|
||||
def popitem(self, last=True):
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
if last:
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
|
||||
def __reduce__(self):
|
||||
items = [[k, self[k]] for k in self]
|
||||
tmp = self.__map, self.__end
|
||||
del self.__map, self.__end
|
||||
inst_dict = vars(self).copy()
|
||||
self.__map, self.__end = tmp
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
setdefault = DictMixin.setdefault
|
||||
update = DictMixin.update
|
||||
pop = DictMixin.pop
|
||||
values = DictMixin.values
|
||||
items = DictMixin.items
|
||||
iterkeys = DictMixin.iterkeys
|
||||
itervalues = DictMixin.itervalues
|
||||
iteritems = DictMixin.iteritems
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, OrderedDict):
|
||||
if len(self) != len(other):
|
||||
return False
|
||||
for p, q in zip(self.items(), other.items()):
|
||||
if p != q:
|
||||
return False
|
||||
return True
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
287
sleekxmpp/thirdparty/statemachine.py
vendored
Normal file
287
sleekxmpp/thirdparty/statemachine.py
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StateMachine(object):
|
||||
|
||||
def __init__(self, states=[]):
|
||||
self.lock = threading.Lock()
|
||||
self.notifier = threading.Event()
|
||||
self.__states = []
|
||||
self.addStates(states)
|
||||
self.__default_state = self.__states[0]
|
||||
self.__current_state = self.__default_state
|
||||
|
||||
def addStates(self, states):
|
||||
self.lock.acquire()
|
||||
try:
|
||||
for state in states:
|
||||
if state in self.__states:
|
||||
raise IndexError("The state '%s' is already in the StateMachine." % state)
|
||||
self.__states.append(state)
|
||||
finally: self.lock.release()
|
||||
|
||||
|
||||
def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}):
|
||||
'''
|
||||
Transition from the given `from_state` to the given `to_state`.
|
||||
This method will return `True` if the state machine is now in `to_state`. It
|
||||
will return `False` if a timeout occurred the transition did not occur.
|
||||
If `wait` is 0 (the default,) this method returns immediately if the state machine
|
||||
is not in `from_state`.
|
||||
|
||||
If you want the thread to block and transition once the state machine to enters
|
||||
`from_state`, set `wait` to a non-negative value. Note there is no 'block
|
||||
indefinitely' flag since this leads to deadlock. If you want to wait indefinitely,
|
||||
choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so:
|
||||
|
||||
::
|
||||
|
||||
while not thread_should_exit and not state_machine.transition('disconnected', 'connecting', wait=20 ):
|
||||
pass # timeout will occur every 20s unless transition occurs
|
||||
if thread_should_exit: return
|
||||
# perform actions here after successful transition
|
||||
|
||||
This allows the thread to be responsive by setting `thread_should_exit=True`.
|
||||
|
||||
The optional `func` argument allows the user to pass a callable operation which occurs
|
||||
within the context of the state transition (e.g. while the state machine is locked.)
|
||||
If `func` returns a True value, the transition will occur. If `func` returns a non-
|
||||
True value or if an exception is thrown, the transition will not occur. Any thrown
|
||||
exception is not caught by the state machine and is the caller's responsibility to handle.
|
||||
If `func` completes normally, this method will return the value returned by `func.` If
|
||||
values for `args` and `kwargs` are provided, they are expanded and passed like so:
|
||||
`func( *args, **kwargs )`.
|
||||
'''
|
||||
|
||||
return self.transition_any((from_state,), to_state, wait=wait,
|
||||
func=func, args=args, kwargs=kwargs)
|
||||
|
||||
|
||||
def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}):
|
||||
'''
|
||||
Transition from any of the given `from_states` to the given `to_state`.
|
||||
'''
|
||||
|
||||
if not (isinstance(from_states,tuple) or isinstance(from_states,list)):
|
||||
raise ValueError("from_states should be a list or tuple")
|
||||
|
||||
for state in from_states:
|
||||
if not state in self.__states:
|
||||
raise ValueError("StateMachine does not contain from_state %s." % state)
|
||||
if not to_state in self.__states:
|
||||
raise ValueError("StateMachine does not contain to_state %s." % to_state)
|
||||
|
||||
start = time.time()
|
||||
while not self.lock.acquire(False):
|
||||
time.sleep(.001)
|
||||
if (start + wait - time.time()) <= 0.0:
|
||||
log.debug("Could not acquire lock")
|
||||
return False
|
||||
|
||||
while not self.__current_state in from_states:
|
||||
# detect timeout:
|
||||
remainder = start + wait - time.time()
|
||||
if remainder > 0:
|
||||
self.notifier.wait(remainder)
|
||||
else:
|
||||
log.debug("State was not ready")
|
||||
self.lock.release()
|
||||
return False
|
||||
|
||||
try: # lock is acquired; all other threads will return false or wait until notify/timeout
|
||||
if self.__current_state in from_states: # should always be True due to lock
|
||||
|
||||
# Note that func might throw an exception, but that's OK, it aborts the transition
|
||||
return_val = func(*args,**kwargs) if func is not None else True
|
||||
|
||||
# some 'false' value returned from func,
|
||||
# indicating that transition should not occur:
|
||||
if not return_val: return return_val
|
||||
|
||||
log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state)
|
||||
self._set_state(to_state)
|
||||
return return_val # some 'true' value returned by func or True if func was None
|
||||
else:
|
||||
log.error("StateMachine bug!! The lock should ensure this doesn't happen!")
|
||||
return False
|
||||
finally:
|
||||
self.notifier.set() # notify any waiting threads that the state has changed.
|
||||
self.notifier.clear()
|
||||
self.lock.release()
|
||||
|
||||
|
||||
def transition_ctx(self, from_state, to_state, wait=0.0):
|
||||
'''
|
||||
Use the state machine as a context manager. The transition occurs on /exit/ from
|
||||
the `with` context, so long as no exception is thrown. For example:
|
||||
|
||||
::
|
||||
|
||||
with state_machine.transition_ctx('one','two', wait=5) as locked:
|
||||
if locked:
|
||||
# the state machine is currently locked in state 'one', and will
|
||||
# transition to 'two' when the 'with' statement ends, so long as
|
||||
# no exception is thrown.
|
||||
print 'Currently locked in state one: %s' % state_machine['one']
|
||||
|
||||
else:
|
||||
# The 'wait' timed out, and no lock has been acquired
|
||||
print 'Timed out before entering state "one"'
|
||||
|
||||
print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two']
|
||||
|
||||
|
||||
The other main difference between this method and `transition()` is that the
|
||||
state machine is locked for the duration of the `with` statement. Normally,
|
||||
after a `transition()` occurs, the state machine is immediately unlocked and
|
||||
available to another thread to call `transition()` again.
|
||||
'''
|
||||
|
||||
if not from_state in self.__states:
|
||||
raise ValueError("StateMachine does not contain from_state %s." % from_state)
|
||||
if not to_state in self.__states:
|
||||
raise ValueError("StateMachine does not contain to_state %s." % to_state)
|
||||
|
||||
return _StateCtx(self, from_state, to_state, wait)
|
||||
|
||||
|
||||
def ensure(self, state, wait=0.0, block_on_transition=False):
|
||||
'''
|
||||
Ensure the state machine is currently in `state`, or wait until it enters `state`.
|
||||
'''
|
||||
return self.ensure_any((state,), wait=wait, block_on_transition=block_on_transition)
|
||||
|
||||
|
||||
def ensure_any(self, states, wait=0.0, block_on_transition=False):
|
||||
'''
|
||||
Ensure we are currently in one of the given `states` or wait until
|
||||
we enter one of those states.
|
||||
|
||||
Note that due to the nature of the function, you cannot guarantee that
|
||||
the entirety of some operation completes while you remain in a given
|
||||
state. That would require acquiring and holding a lock, which
|
||||
would mean no other threads could do the same. (You'd essentially
|
||||
be serializing all of the threads that are 'ensuring' their tasks
|
||||
occurred in some state.
|
||||
'''
|
||||
if not (isinstance(states,tuple) or isinstance(states,list)):
|
||||
raise ValueError('states arg should be a tuple or list')
|
||||
|
||||
for state in states:
|
||||
if not state in self.__states:
|
||||
raise ValueError("StateMachine does not contain state '%s'" % state)
|
||||
|
||||
# if we're in the middle of a transition, determine whether we should
|
||||
# 'fall back' to the 'current' state, or wait for the new state, in order to
|
||||
# avoid an operation occurring in the wrong state.
|
||||
# TODO another option would be an ensure_ctx that uses a semaphore to allow
|
||||
# threads to indicate they want to remain in a particular state.
|
||||
|
||||
# will return immediately if no transition is in process.
|
||||
if block_on_transition:
|
||||
# we're not in the middle of a transition; don't hold the lock
|
||||
if self.lock.acquire(False): self.lock.release()
|
||||
# wait for the transition to complete
|
||||
else: self.notifier.wait()
|
||||
|
||||
start = time.time()
|
||||
while not self.__current_state in states:
|
||||
# detect timeout:
|
||||
remainder = start + wait - time.time()
|
||||
if remainder > 0: self.notifier.wait(remainder)
|
||||
else: return False
|
||||
return True
|
||||
|
||||
|
||||
def reset(self):
|
||||
# TODO need to lock before calling this?
|
||||
self.transition(self.__current_state, self.__default_state)
|
||||
|
||||
|
||||
def _set_state(self, state): #unsynchronized, only call internally after lock is acquired
|
||||
self.__current_state = state
|
||||
return state
|
||||
|
||||
|
||||
def current_state(self):
|
||||
'''
|
||||
Return the current state name.
|
||||
'''
|
||||
return self.__current_state
|
||||
|
||||
|
||||
def __getitem__(self, state):
|
||||
'''
|
||||
Non-blocking, non-synchronized test to determine if we are in the given state.
|
||||
Use `StateMachine.ensure(state)` to wait until the machine enters a certain state.
|
||||
'''
|
||||
return self.__current_state == state
|
||||
|
||||
def __str__(self):
|
||||
return "".join(("StateMachine(", ','.join(self.__states), "): ", self.__current_state))
|
||||
|
||||
|
||||
|
||||
class _StateCtx:
|
||||
|
||||
def __init__(self, state_machine, from_state, to_state, wait):
|
||||
self.state_machine = state_machine
|
||||
self.from_state = from_state
|
||||
self.to_state = to_state
|
||||
self.wait = wait
|
||||
self._locked = False
|
||||
|
||||
def __enter__(self):
|
||||
start = time.time()
|
||||
while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False):
|
||||
# detect timeout:
|
||||
remainder = start + self.wait - time.time()
|
||||
if remainder > 0: self.state_machine.notifier.wait(remainder)
|
||||
else:
|
||||
log.debug('StateMachine timeout while waiting for state: %s', self.from_state)
|
||||
return False
|
||||
|
||||
self._locked = True # lock has been acquired at this point
|
||||
self.state_machine.notifier.clear()
|
||||
log.debug('StateMachine entered context in state: %s',
|
||||
self.state_machine.current_state())
|
||||
return True
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_val is not None:
|
||||
log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s",
|
||||
self.state_machine.current_state(), exc_type.__name__, exc_val)
|
||||
|
||||
if self._locked:
|
||||
if exc_val is None:
|
||||
log.debug(' ==== TRANSITION %s -> %s',
|
||||
self.state_machine.current_state(), self.to_state)
|
||||
self.state_machine._set_state(self.to_state)
|
||||
|
||||
self.state_machine.notifier.set()
|
||||
self.state_machine.lock.release()
|
||||
|
||||
return False # re-raise any exception
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
def callback(s, s2):
|
||||
print((1, s.transition('on', 'off', wait=0.0, func=callback, args=[s,s2])))
|
||||
print((2, s2.transition('off', 'on', func=callback, args=[s,s2])))
|
||||
return True
|
||||
|
||||
s = StateMachine(('off', 'on'))
|
||||
s2 = StateMachine(('off', 'on'))
|
||||
print((3, s.transition('off', 'on', wait=0.0, func=callback, args=[s,s2]),))
|
||||
print((s.current_state(), s2.current_state()))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user