Compare commits
264 Commits
sleek-0.9-
...
1.0-Beta3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,2 @@
|
||||
*.pyc
|
||||
.project
|
||||
build/
|
||||
*.swp
|
||||
.pydevproject
|
||||
.settings
|
||||
|
||||
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
|
||||
5
README
5
README
@@ -4,6 +4,11 @@ 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.
|
||||
- Every XEP as a plugin.
|
||||
|
||||
@@ -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,9 +24,9 @@ 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']
|
||||
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_0030')
|
||||
xmpp.registerPlugin('xep_0004')
|
||||
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.")
|
||||
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.")
|
||||
129
examples/echo_client.py
Executable file
129
examples/echo_client.py
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/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 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.getRoster()
|
||||
self.sendPresence()
|
||||
|
||||
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')
|
||||
|
||||
# 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.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():
|
||||
# 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.")
|
||||
28
setup.py
28
setup.py
@@ -16,16 +16,16 @@ import sys
|
||||
# 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 = '1.0.0.0'
|
||||
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 +37,20 @@ 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/plugins',
|
||||
'sleekxmpp/stanza',
|
||||
'sleekxmpp/test',
|
||||
'sleekxmpp/xmlstream',
|
||||
'sleekxmpp/xmlstream/matcher',
|
||||
'sleekxmpp/xmlstream/handler',
|
||||
'sleekxmpp/thirdparty',
|
||||
]
|
||||
|
||||
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 +62,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,275 +1,16 @@
|
||||
#!/usr/bin/python2.5
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 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.
|
||||
|
||||
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
|
||||
import dns.rdatatype
|
||||
except ImportError:
|
||||
srvsupport = False
|
||||
|
||||
|
||||
|
||||
#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.server = None
|
||||
self.port = 5222 # not used if DNS SRV is used
|
||||
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.domain,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.bound = False
|
||||
self.bindfail = 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, host=None, port=None):
|
||||
"""Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
|
||||
the JID server."""
|
||||
|
||||
if self.state['connected']: return True
|
||||
|
||||
if host:
|
||||
self.server = host
|
||||
if port is None: port = self.port
|
||||
else:
|
||||
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 domain from JID.")
|
||||
else:
|
||||
logging.debug("Since no address is supplied, attempting SRV lookup.")
|
||||
try:
|
||||
answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.domain,
|
||||
dns.rdatatype.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:
|
||||
(host,port) = addresses[priority]
|
||||
break
|
||||
# if SRV lookup was successful, we aren't using a particular server.
|
||||
self.server = None
|
||||
|
||||
if not host:
|
||||
# if all else fails take server from JID.
|
||||
(host,port) = (self.domain, self.port)
|
||||
self.server = None
|
||||
|
||||
logging.debug('Attempting connection to %s:%d', host, port )
|
||||
#TODO option to not use TLS?
|
||||
result = XMLStream.connect(self, host, port, 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):
|
||||
self.disconnect(reconnect=True)
|
||||
|
||||
def disconnect(self, reconnect=False):
|
||||
self.event("disconnected")
|
||||
self.authenticated = False
|
||||
self.sessionstarted = False
|
||||
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):
|
||||
logging.debug('handling stream 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):
|
||||
# This calls the feature handler & optionally breaks
|
||||
if feature[1](subelement) and feature[2]: #if breaker, don't continue
|
||||
return True
|
||||
|
||||
def handler_starttls(self, xml):
|
||||
logging.debug( 'TLS start handler; SSL support: %s', self.ssl_support )
|
||||
if not self.authenticated and self.ssl_support:
|
||||
_stanza = "<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />"
|
||||
if not self.event_handlers.get(_stanza,None): # don't add handler > once
|
||||
self.add_handler( _stanza, 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):
|
||||
logging.debug("Authentication successful.")
|
||||
self.authenticated = True
|
||||
self.features = []
|
||||
raise RestartStream()
|
||||
|
||||
def handler_auth_fail(self, xml):
|
||||
logging.warning("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)
|
||||
self.bound = True
|
||||
logging.info("Node set to: %s" % self.fulljid)
|
||||
if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail:
|
||||
logging.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.event("session_start")
|
||||
|
||||
def handler_start_session(self, xml):
|
||||
if self.authenticated and self.bound:
|
||||
iq = self.makeIqSet(xml)
|
||||
response = iq.send()
|
||||
logging.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.event("session_start")
|
||||
else:
|
||||
#bind probably hasn't happened yet
|
||||
self.bindfail = True
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -1,318 +1,634 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 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.
|
||||
|
||||
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.waiter import Waiter
|
||||
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
|
||||
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.domain = ''
|
||||
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.domain = jid.split('@',1)[-1].split('/', 1)[0]
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
for idx in self.plugin:
|
||||
if not self.plugin[idx].post_inited: self.plugin[idx].post_init()
|
||||
return super(basexmpp, self).process(*args, **kwargs)
|
||||
|
||||
def registerPlugin(self, plugin, pconfig = {}, pluginModule = None):
|
||||
"""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
|
||||
try:
|
||||
if pluginModule:
|
||||
module = __import__(pluginModule, globals(), locals(), [plugin])
|
||||
else:
|
||||
module = __import__("%s.%s" % (globals()['plugins'].__name__, plugin), globals(), locals(), [plugin])
|
||||
# init the plugin class
|
||||
self.plugin[plugin] = getattr(module, 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))
|
||||
except:
|
||||
logging.error("Unable to load plugin: %s" %(plugin) )
|
||||
|
||||
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, {}), False)
|
||||
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:
|
||||
logging.warning("Use of send mask waiters is deprecated")
|
||||
waitfor = Waiter('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.registerPlugin = self.register_plugin
|
||||
self.makeIq = self.make_iq
|
||||
self.makeIqGet = self.make_iq_get
|
||||
self.makeIqResult = self.make_iq_result
|
||||
self.makeIqSet = self.make_iq_set
|
||||
self.makeIqError = self.make_iq_error
|
||||
self.makeIqQuery = self.make_iq_query
|
||||
self.makeQueryRoster = self.make_query_roster
|
||||
self.makeMessage = self.make_message
|
||||
self.makePresence = self.make_presence
|
||||
self.sendMessage = self.send_message
|
||||
self.sendPresence = self.send_presence
|
||||
self.sendPresenceSubscription = self.send_presence_subscription
|
||||
|
||||
def del_event_handler(self, name, pointer):
|
||||
"""Remove a handler for an event."""
|
||||
if not name in self.event_handlers:
|
||||
return
|
||||
|
||||
# Need to keep handlers that do not use
|
||||
# the given function pointer
|
||||
def filter_pointers(handler):
|
||||
return handler[0] != pointer
|
||||
self.default_ns = default_ns
|
||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||
|
||||
self.event_handlers[name] = filter(filter_pointers,
|
||||
self.event_handlers[name])
|
||||
self.boundjid = JID("")
|
||||
|
||||
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']['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.plugin = {}
|
||||
self.roster = {}
|
||||
self.is_component = False
|
||||
self.auto_authorize = True
|
||||
self.auto_subscribe = True
|
||||
|
||||
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.sentpresence = False
|
||||
|
||||
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']:
|
||||
if (show == 'available' or show in presence.showtypes):
|
||||
self.event("got_online", presence)
|
||||
wasoffline = True
|
||||
self.roster[jid]['presence'][resource] = {}
|
||||
if self.roster[jid]['presence'][resource].get('show', 'unavailable') == 'unavailable':
|
||||
wasoffline = True
|
||||
self.roster[jid]['presence'][resource] = {'show': show, 'status': status, 'priority': priority}
|
||||
name = self.roster[jid].get('name', '')
|
||||
if show == 'unavailable':
|
||||
logging.debug("%s %s got offline" % (jid, resource))
|
||||
del self.roster[jid]['presence'][resource]
|
||||
if len(self.roster[jid]['presence']) == 0 and not self.roster[jid]['in_roster']:
|
||||
del self.roster[jid]
|
||||
if not wasoffline:
|
||||
self.event("got_offline", presence)
|
||||
else:
|
||||
return False
|
||||
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.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.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.registerStanza(Message)
|
||||
self.registerStanza(Iq)
|
||||
self.registerStanza(Presence)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return self.Iq()._set_stanza_values({'id': str(id),
|
||||
'from': ifrom})
|
||||
|
||||
def make_iq_get(self, queryxmlns=None):
|
||||
"""
|
||||
Create an Iq stanza of type 'get'.
|
||||
|
||||
Optionally, a query element may be added.
|
||||
|
||||
Arguments:
|
||||
queryxmlns -- The namespace of the query to use.
|
||||
"""
|
||||
return self.Iq()._set_stanza_values({'type': 'get',
|
||||
'query': queryxmlns})
|
||||
|
||||
def make_iq_result(self, id):
|
||||
"""
|
||||
Create an Iq stanza of type 'result' with the given ID value.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value. May use self.new_id().
|
||||
"""
|
||||
return self.Iq()._set_stanza_values({'id': id,
|
||||
'type': 'result'})
|
||||
|
||||
def make_iq_set(self, sub=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.
|
||||
"""
|
||||
iq = self.Iq()._set_stanza_values({'type': 'set'})
|
||||
if sub != None:
|
||||
iq.append(sub)
|
||||
return iq
|
||||
|
||||
def make_iq_error(self, id, type='cancel',
|
||||
condition='feature-not-implemented', text=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.
|
||||
"""
|
||||
iq = self.Iq()._set_stanza_values({'id': id})
|
||||
iq['error']._set_stanza_values({'type': type,
|
||||
'condition': condition,
|
||||
'text': text})
|
||||
return iq
|
||||
|
||||
def make_iq_query(self, iq=None, xmlns=''):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
iq['query'] = xmlns
|
||||
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_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]['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]['in_roster']:
|
||||
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
|
||||
|
||||
436
sleekxmpp/clientxmpp.py
Normal file
436
sleekxmpp/clientxmpp.py
Normal file
@@ -0,0 +1,436 @@
|
||||
"""
|
||||
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')
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.updateRoster = self.update_roster
|
||||
self.delRosterItem = self.del_roster_item
|
||||
self.getRoster = self.get_roster
|
||||
self.registerFeature = self.register_feature
|
||||
|
||||
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.session_started_event = threading.Event()
|
||||
self.session_started_event.clear()
|
||||
|
||||
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()):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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.server
|
||||
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.")
|
||||
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=True)
|
||||
|
||||
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=[]):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
iq = self.Iq()._set_stanza_values({'type': 'set'})
|
||||
iq['roster']['items'] = {jid: {'name': name,
|
||||
'subscription': subscription,
|
||||
'groups': groups}}
|
||||
response = iq.send()
|
||||
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):
|
||||
"""Request the roster from the server."""
|
||||
iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster')
|
||||
response = iq.send()
|
||||
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.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)
|
||||
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 '{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))
|
||||
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
|
||||
self.send("<auth xmlns='%s' mechanism='%s' />" % (
|
||||
sasl_ns,
|
||||
'ANONYMOUS'))
|
||||
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()
|
||||
|
||||
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.fulljid)
|
||||
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()
|
||||
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_update", iq)
|
||||
if iq['type'] == 'set':
|
||||
iq.reply()
|
||||
iq.enable('roster')
|
||||
iq.send()
|
||||
@@ -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.")
|
||||
199
sleekxmpp/componentxmpp.py
Executable file → Normal file
199
sleekxmpp/componentxmpp.py
Executable file → Normal file
@@ -1,88 +1,141 @@
|
||||
#!/usr/bin/python2.6
|
||||
|
||||
"""
|
||||
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 __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 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)
|
||||
|
||||
def _handle_handshake(self, xml):
|
||||
"""
|
||||
The handshake has been accepted.
|
||||
|
||||
Arguments:
|
||||
xml -- The reply handshake stanza.
|
||||
"""
|
||||
self.event("session_start")
|
||||
|
||||
@@ -3,14 +3,47 @@
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if extension_args is None:
|
||||
extension_args = {}
|
||||
|
||||
self.condition = condition
|
||||
self.text = text
|
||||
self.etype = etype
|
||||
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_0012', 'xep_0030', 'xep_0033', 'xep_0045',
|
||||
'xep_0050', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify',
|
||||
'xep_0060', 'xep_0202']
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
"""
|
||||
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.post_inited = False
|
||||
self.enable = config.get('enable', True)
|
||||
if self.enable:
|
||||
self.plugin_init()
|
||||
|
||||
def plugin_init(self):
|
||||
pass
|
||||
|
||||
def post_init(self):
|
||||
self.post_inited = True
|
||||
|
||||
def __init__(self, xmpp, 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):
|
||||
pass
|
||||
|
||||
def post_init(self):
|
||||
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 = iq.send()
|
||||
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.jid
|
||||
iq['gmail']['q'] = query
|
||||
iq['gmail']['newer-than-time'] = newer
|
||||
return iq.send()
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
import types
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -20,7 +25,7 @@ class jobs(base.base_plugin):
|
||||
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, jobid):
|
||||
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):
|
||||
@@ -38,7 +43,8 @@ class jobs(base.base_plugin):
|
||||
iq['psstate']['item'] = jobid
|
||||
iq['psstate']['payload'] = state
|
||||
result = iq.send()
|
||||
if result is None or result['type'] != 'result':
|
||||
if result is None or type(result) == types.BooleanType 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 log
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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
|
||||
@@ -6,9 +6,6 @@ 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'
|
||||
@@ -30,7 +27,7 @@ class PubsubState(ElementBase):
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
|
||||
stanzaPlugin(Iq, PubsubState)
|
||||
registerStanzaPlugin(Iq, PubsubState)
|
||||
|
||||
class PubsubStateEvent(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/psstate#event'
|
||||
@@ -40,8 +37,8 @@ class PubsubStateEvent(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Message, PubsubStateEvent)
|
||||
stanzaPlugin(PubsubStateEvent, PubsubState)
|
||||
registerStanzaPlugin(Message, PubsubStateEvent)
|
||||
registerStanzaPlugin(PubsubStateEvent, PubsubState)
|
||||
|
||||
class Pubsub(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -51,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'
|
||||
@@ -61,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'
|
||||
@@ -86,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):
|
||||
@@ -103,7 +100,7 @@ class Subscription(ElementBase):
|
||||
def getjid(self):
|
||||
return jid(self._getattr('jid'))
|
||||
|
||||
stanzaPlugin(Pubsub, Subscription)
|
||||
registerStanzaPlugin(Pubsub, Subscription)
|
||||
|
||||
class Subscriptions(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -114,7 +111,7 @@ class Subscriptions(ElementBase):
|
||||
plugin_tag_map = {}
|
||||
subitem = (Subscription,)
|
||||
|
||||
stanzaPlugin(Pubsub, Subscriptions)
|
||||
registerStanzaPlugin(Pubsub, Subscriptions)
|
||||
|
||||
class OptionalSetting(object):
|
||||
interfaces = set(('required',))
|
||||
@@ -147,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'
|
||||
@@ -173,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,)
|
||||
|
||||
stanzaPlugin(Pubsub, Items)
|
||||
registerStanzaPlugin(Pubsub, Items)
|
||||
|
||||
class Create(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -188,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'
|
||||
@@ -203,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'
|
||||
@@ -214,7 +211,7 @@ class Publish(Items):
|
||||
plugin_tag_map = {}
|
||||
subitem = (Item,)
|
||||
|
||||
stanzaPlugin(Pubsub, Publish)
|
||||
registerStanzaPlugin(Pubsub, Publish)
|
||||
|
||||
class Retract(Items):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -224,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'
|
||||
@@ -254,13 +251,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 = {}
|
||||
|
||||
@@ -269,22 +266,8 @@ 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)
|
||||
registerStanzaPlugin(Pubsub, Configure)
|
||||
registerStanzaPlugin(Configure, xep_0004.Form)
|
||||
|
||||
class DefaultConfig(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -296,28 +279,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'
|
||||
return t
|
||||
|
||||
def getConfig(self):
|
||||
return self['form']
|
||||
|
||||
def setConfig(self, value):
|
||||
self['form'].setStanzaValues(value.getStanzaValues())
|
||||
return self
|
||||
|
||||
stanzaPlugin(PubsubOwner, DefaultConfig)
|
||||
registerStanzaPlugin(PubsubOwner, DefaultConfig)
|
||||
registerStanzaPlugin(DefaultConfig, xep_0004.Form)
|
||||
|
||||
class Options(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
@@ -351,8 +327,8 @@ class Options(ElementBase):
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
stanzaPlugin(Pubsub, Options)
|
||||
stanzaPlugin(Subscribe, Options)
|
||||
registerStanzaPlugin(Pubsub, Options)
|
||||
registerStanzaPlugin(Subscribe, Options)
|
||||
|
||||
class OwnerAffiliations(Affiliations):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -366,7 +342,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'
|
||||
@@ -380,15 +356,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'
|
||||
@@ -398,7 +382,7 @@ class OwnerDelete(ElementBase, OptionalSetting):
|
||||
plugin_tag_map = {}
|
||||
interfaces = set(('node',))
|
||||
|
||||
stanzaPlugin(PubsubOwner, OwnerDelete)
|
||||
registerStanzaPlugin(PubsubOwner, OwnerDelete)
|
||||
|
||||
class OwnerPurge(ElementBase, OptionalSetting):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -407,7 +391,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'
|
||||
@@ -423,7 +407,7 @@ class OwnerRedirect(ElementBase):
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
stanzaPlugin(OwnerDelete, OwnerRedirect)
|
||||
registerStanzaPlugin(OwnerDelete, OwnerRedirect)
|
||||
|
||||
class OwnerSubscriptions(Subscriptions):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
@@ -437,7 +421,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'
|
||||
@@ -461,7 +445,7 @@ class Event(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Message, Event)
|
||||
registerStanzaPlugin(Message, Event)
|
||||
|
||||
class EventItem(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -501,7 +485,7 @@ class EventItems(ElementBase):
|
||||
plugin_tag_map = {}
|
||||
subitem = (EventItem, EventRetract)
|
||||
|
||||
stanzaPlugin(Event, EventItems)
|
||||
registerStanzaPlugin(Event, EventItems)
|
||||
|
||||
class EventCollection(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -511,7 +495,7 @@ class EventCollection(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Event, EventCollection)
|
||||
registerStanzaPlugin(Event, EventCollection)
|
||||
|
||||
class EventAssociate(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -521,7 +505,7 @@ class EventAssociate(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(EventCollection, EventAssociate)
|
||||
registerStanzaPlugin(EventCollection, EventAssociate)
|
||||
|
||||
class EventDisassociate(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -531,7 +515,7 @@ class EventDisassociate(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(EventCollection, EventDisassociate)
|
||||
registerStanzaPlugin(EventCollection, EventDisassociate)
|
||||
|
||||
class EventConfiguration(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -541,22 +525,8 @@ class EventConfiguration(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
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(Event, EventConfiguration)
|
||||
registerStanzaPlugin(Event, EventConfiguration)
|
||||
registerStanzaPlugin(EventConfiguration, xep_0004.Form)
|
||||
|
||||
class EventPurge(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -566,7 +536,7 @@ class EventPurge(ElementBase):
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Event, EventPurge)
|
||||
registerStanzaPlugin(Event, EventPurge)
|
||||
|
||||
class EventSubscription(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#event'
|
||||
@@ -582,4 +552,4 @@ class EventSubscription(ElementBase):
|
||||
def getJid(self):
|
||||
return JID(self._getAttr('jid'))
|
||||
|
||||
stanzaPlugin(Event, EventSubscription)
|
||||
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
|
||||
import types
|
||||
|
||||
|
||||
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'):
|
||||
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) == types.DictType:
|
||||
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)
|
||||
|
||||
|
||||
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=''):
|
||||
f = Form()
|
||||
f['type'] = ftype
|
||||
f['title'] = title
|
||||
f['instructions'] = instructions
|
||||
return f
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['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 handle_form(self, message):
|
||||
self.xmpp.event("message_xform", message)
|
||||
|
||||
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
|
||||
|
||||
return Form(xml=xml)
|
||||
|
||||
@@ -178,9 +178,12 @@ 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 = []
|
||||
|
||||
|
||||
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
|
||||
@@ -3,322 +3,327 @@
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permissio
|
||||
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 ElementBase, ET, JID
|
||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||
from .. stanza.iq import Iq
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DiscoInfo(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/disco#info'
|
||||
name = 'query'
|
||||
plugin_attrib = 'disco_info'
|
||||
interfaces = set(('node', 'features', 'identities'))
|
||||
namespace = 'http://jabber.org/protocol/disco#info'
|
||||
name = 'query'
|
||||
plugin_attrib = 'disco_info'
|
||||
interfaces = set(('node', 'features', 'identities'))
|
||||
|
||||
def getFeatures(self):
|
||||
features = []
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for feature in featuresXML:
|
||||
features.append(feature.attrib['var'])
|
||||
return features
|
||||
def getFeatures(self):
|
||||
features = []
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for feature in featuresXML:
|
||||
features.append(feature.attrib['var'])
|
||||
return features
|
||||
|
||||
def setFeatures(self, features):
|
||||
self.delFeatures()
|
||||
for name in features:
|
||||
self.addFeature(name)
|
||||
def setFeatures(self, features):
|
||||
self.delFeatures()
|
||||
for name in features:
|
||||
self.addFeature(name)
|
||||
|
||||
def delFeatures(self):
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for feature in featuresXML:
|
||||
self.xml.remove(feature)
|
||||
def delFeatures(self):
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for feature in featuresXML:
|
||||
self.xml.remove(feature)
|
||||
|
||||
def addFeature(self, feature):
|
||||
featureXML = ET.Element('{%s}feature' % self.namespace,
|
||||
{'var': feature})
|
||||
self.xml.append(featureXML)
|
||||
def addFeature(self, feature):
|
||||
featureXML = ET.Element('{%s}feature' % self.namespace,
|
||||
{'var': feature})
|
||||
self.xml.append(featureXML)
|
||||
|
||||
def delFeature(self, feature):
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for featureXML in featuresXML:
|
||||
if featureXML.attrib['var'] == feature:
|
||||
self.xml.remove(featureXML)
|
||||
def delFeature(self, feature):
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for featureXML in featuresXML:
|
||||
if featureXML.attrib['var'] == feature:
|
||||
self.xml.remove(featureXML)
|
||||
|
||||
def getIdentities(self):
|
||||
ids = []
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
idData = (idXML.attrib['category'],
|
||||
idXML.attrib['type'],
|
||||
idXML.attrib.get('name', ''))
|
||||
ids.append(idData)
|
||||
return ids
|
||||
def getIdentities(self):
|
||||
ids = []
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
idData = (idXML.attrib['category'],
|
||||
idXML.attrib['type'],
|
||||
idXML.attrib.get('name', ''))
|
||||
ids.append(idData)
|
||||
return ids
|
||||
|
||||
def setIdentities(self, ids):
|
||||
self.delIdentities()
|
||||
for idData in ids:
|
||||
self.addIdentity(*idData)
|
||||
def setIdentities(self, ids):
|
||||
self.delIdentities()
|
||||
for idData in ids:
|
||||
self.addIdentity(*idData)
|
||||
|
||||
def delIdentities(self):
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
self.xml.remove(idXML)
|
||||
def delIdentities(self):
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
self.xml.remove(idXML)
|
||||
|
||||
def addIdentity(self, category, id_type, name=''):
|
||||
idXML = ET.Element('{%s}identity' % self.namespace,
|
||||
{'category': category,
|
||||
'type': id_type,
|
||||
'name': name})
|
||||
self.xml.append(idXML)
|
||||
def addIdentity(self, category, id_type, name=''):
|
||||
idXML = ET.Element('{%s}identity' % self.namespace,
|
||||
{'category': category,
|
||||
'type': id_type,
|
||||
'name': name})
|
||||
self.xml.append(idXML)
|
||||
|
||||
def delIdentity(self, category, id_type, name=''):
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
idData = (idXML.attrib['category'],
|
||||
idXML.attrib['type'])
|
||||
delId = (category, id_type)
|
||||
if idData == delId:
|
||||
self.xml.remove(idXML)
|
||||
def delIdentity(self, category, id_type, name=''):
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
idData = (idXML.attrib['category'],
|
||||
idXML.attrib['type'])
|
||||
delId = (category, id_type)
|
||||
if idData == delId:
|
||||
self.xml.remove(idXML)
|
||||
|
||||
|
||||
class DiscoItems(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/disco#items'
|
||||
name = 'query'
|
||||
plugin_attrib = 'disco_items'
|
||||
interfaces = set(('node', 'items'))
|
||||
namespace = 'http://jabber.org/protocol/disco#items'
|
||||
name = 'query'
|
||||
plugin_attrib = 'disco_items'
|
||||
interfaces = set(('node', 'items'))
|
||||
|
||||
def getItems(self):
|
||||
items = []
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for item in itemsXML:
|
||||
itemData = (item.attrib['jid'],
|
||||
item.attrib.get('node'),
|
||||
item.attrib.get('name'))
|
||||
items.append(itemData)
|
||||
return items
|
||||
def getItems(self):
|
||||
items = []
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for item in itemsXML:
|
||||
itemData = (item.attrib['jid'],
|
||||
item.attrib.get('node'),
|
||||
item.attrib.get('name'))
|
||||
items.append(itemData)
|
||||
return items
|
||||
|
||||
def setItems(self, items):
|
||||
self.delItems()
|
||||
for item in items:
|
||||
self.addItem(*item)
|
||||
def setItems(self, items):
|
||||
self.delItems()
|
||||
for item in items:
|
||||
self.addItem(*item)
|
||||
|
||||
def delItems(self):
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for item in itemsXML:
|
||||
self.xml.remove(item)
|
||||
def delItems(self):
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for item in itemsXML:
|
||||
self.xml.remove(item)
|
||||
|
||||
def addItem(self, jid, node='', name=''):
|
||||
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
|
||||
if name:
|
||||
itemXML.attrib['name'] = name
|
||||
if node:
|
||||
itemXML.attrib['node'] = node
|
||||
self.xml.append(itemXML)
|
||||
def addItem(self, jid, node='', name=''):
|
||||
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
|
||||
if name:
|
||||
itemXML.attrib['name'] = name
|
||||
if node:
|
||||
itemXML.attrib['node'] = node
|
||||
self.xml.append(itemXML)
|
||||
|
||||
def delItem(self, jid, node=''):
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
itemData = (itemXML.attrib['jid'],
|
||||
itemXML.attrib.get('node', ''))
|
||||
itemDel = (jid, node)
|
||||
if itemData == itemDel:
|
||||
self.xml.remove(itemXML)
|
||||
|
||||
def delItem(self, jid, node=''):
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
itemData = (itemXML.attrib['jid'],
|
||||
itemXML.attrib.get('node', ''))
|
||||
itemDel = (jid, node)
|
||||
if itemData == itemDel:
|
||||
self.xml.remove(itemXML)
|
||||
|
||||
|
||||
class DiscoNode(object):
|
||||
"""
|
||||
Collection object for grouping info and item information
|
||||
into nodes.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.info = DiscoInfo()
|
||||
self.items = DiscoItems()
|
||||
"""
|
||||
Collection object for grouping info and item information
|
||||
into nodes.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.info = DiscoInfo()
|
||||
self.items = DiscoItems()
|
||||
|
||||
# This is a bit like poor man's inheritance, but
|
||||
# to simplify adding information to the node we
|
||||
# map node functions to either the info or items
|
||||
# stanza objects.
|
||||
#
|
||||
# We don't want to make DiscoNode inherit from
|
||||
# DiscoInfo and DiscoItems because DiscoNode is
|
||||
# not an actual stanza, and doing so would create
|
||||
# confusion and potential bugs.
|
||||
self.info['node'] = name
|
||||
self.items['node'] = name
|
||||
|
||||
self._map(self.items, 'items', ['get', 'set', 'del'])
|
||||
self._map(self.items, 'item', ['add', 'del'])
|
||||
self._map(self.info, 'identities', ['get', 'set', 'del'])
|
||||
self._map(self.info, 'identity', ['add', 'del'])
|
||||
self._map(self.info, 'features', ['get', 'set', 'del'])
|
||||
self._map(self.info, 'feature', ['add', 'del'])
|
||||
# This is a bit like poor man's inheritance, but
|
||||
# to simplify adding information to the node we
|
||||
# map node functions to either the info or items
|
||||
# stanza objects.
|
||||
#
|
||||
# We don't want to make DiscoNode inherit from
|
||||
# DiscoInfo and DiscoItems because DiscoNode is
|
||||
# not an actual stanza, and doing so would create
|
||||
# confusion and potential bugs.
|
||||
|
||||
def isEmpty(self):
|
||||
"""
|
||||
Test if the node contains any information. Useful for
|
||||
determining if a node can be deleted.
|
||||
"""
|
||||
ids = self.getIdentities()
|
||||
features = self.getFeatures()
|
||||
items = self.getItems()
|
||||
self._map(self.items, 'items', ['get', 'set', 'del'])
|
||||
self._map(self.items, 'item', ['add', 'del'])
|
||||
self._map(self.info, 'identities', ['get', 'set', 'del'])
|
||||
self._map(self.info, 'identity', ['add', 'del'])
|
||||
self._map(self.info, 'features', ['get', 'set', 'del'])
|
||||
self._map(self.info, 'feature', ['add', 'del'])
|
||||
|
||||
if not ids and not features and not items:
|
||||
return True
|
||||
return False
|
||||
def isEmpty(self):
|
||||
"""
|
||||
Test if the node contains any information. Useful for
|
||||
determining if a node can be deleted.
|
||||
"""
|
||||
ids = self.getIdentities()
|
||||
features = self.getFeatures()
|
||||
items = self.getItems()
|
||||
|
||||
def _map(self, obj, interface, access):
|
||||
"""
|
||||
Map functions of the form obj.accessInterface
|
||||
to self.accessInterface for each given access type.
|
||||
"""
|
||||
interface = interface.title()
|
||||
for access_type in access:
|
||||
method = access_type + interface
|
||||
if hasattr(obj, method):
|
||||
setattr(self, method, getattr(obj, method))
|
||||
if not ids and not features and not items:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _map(self, obj, interface, access):
|
||||
"""
|
||||
Map functions of the form obj.accessInterface
|
||||
to self.accessInterface for each given access type.
|
||||
"""
|
||||
interface = interface.title()
|
||||
for access_type in access:
|
||||
method = access_type + interface
|
||||
if hasattr(obj, method):
|
||||
setattr(self, method, getattr(obj, method))
|
||||
|
||||
|
||||
class xep_0030(base.base_plugin):
|
||||
"""
|
||||
XEP-0030 Service Discovery
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0030'
|
||||
self.description = 'Service Discovery'
|
||||
"""
|
||||
XEP-0030 Service Discovery
|
||||
"""
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Disco Items',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
DiscoItems.namespace)),
|
||||
self.handle_item_query))
|
||||
def plugin_init(self):
|
||||
self.xep = '0030'
|
||||
self.description = 'Service Discovery'
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Disco Info',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
DiscoInfo.namespace)),
|
||||
self.handle_info_query))
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Disco Items',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
DiscoItems.namespace)),
|
||||
self.handle_item_query))
|
||||
|
||||
self.xmpp.stanzaPlugin(Iq, DiscoInfo)
|
||||
self.xmpp.stanzaPlugin(Iq, DiscoItems)
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Disco Info',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
DiscoInfo.namespace)),
|
||||
self.handle_info_query))
|
||||
|
||||
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
|
||||
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
|
||||
registerStanzaPlugin(Iq, DiscoInfo)
|
||||
registerStanzaPlugin(Iq, DiscoItems)
|
||||
|
||||
self.nodes = {'main': DiscoNode('main')}
|
||||
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
|
||||
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
|
||||
|
||||
def add_node(self, node):
|
||||
if node not in self.nodes:
|
||||
self.nodes[node] = DiscoNode(node)
|
||||
self.nodes = {'main': DiscoNode('main')}
|
||||
|
||||
def del_node(self, node):
|
||||
if node in self.nodes:
|
||||
del self.nodes[node]
|
||||
def add_node(self, node):
|
||||
if node not in self.nodes:
|
||||
self.nodes[node] = DiscoNode(node)
|
||||
|
||||
def handle_item_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
logging.debug("Items requested by %s" % iq['from'])
|
||||
self.xmpp.event('disco_items_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
logging.debug("Items result from %s" % iq['from'])
|
||||
self.xmpp.event('disco_items', iq)
|
||||
def del_node(self, node):
|
||||
if node in self.nodes:
|
||||
del self.nodes[node]
|
||||
|
||||
def handle_info_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
logging.debug("Info requested by %s" % iq['from'])
|
||||
self.xmpp.event('disco_info_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
logging.debug("Info result from %s" % iq['from'])
|
||||
self.xmpp.event('disco_info', iq)
|
||||
def handle_item_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Items requested by %s" % iq['from'])
|
||||
self.xmpp.event('disco_items_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Items result from %s" % iq['from'])
|
||||
self.xmpp.event('disco_items', iq)
|
||||
|
||||
def handle_disco_info(self, iq, forwarded=False):
|
||||
"""
|
||||
A default handler for disco#info requests. If another
|
||||
handler is registered, this one will defer and not run.
|
||||
"""
|
||||
handlers = self.xmpp.event_handlers['disco_info_request']
|
||||
if not forwarded and len(handlers) > 1:
|
||||
return
|
||||
def handle_info_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Info requested by %s" % iq['from'])
|
||||
self.xmpp.event('disco_info_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Info result from %s" % iq['from'])
|
||||
self.xmpp.event('disco_info', iq)
|
||||
|
||||
node_name = iq['disco_info']['node']
|
||||
if not node_name:
|
||||
node_name = 'main'
|
||||
def handle_disco_info(self, iq, forwarded=False):
|
||||
"""
|
||||
A default handler for disco#info requests. If another
|
||||
handler is registered, this one will defer and not run.
|
||||
"""
|
||||
if not forwarded and self.xmpp.event_handled('disco_info_request'):
|
||||
return
|
||||
|
||||
logging.debug("Using default handler for disco#info on node '%s'." % node_name)
|
||||
node_name = iq['disco_info']['node']
|
||||
if not node_name:
|
||||
node_name = 'main'
|
||||
|
||||
if node_name in self.nodes:
|
||||
node = self.nodes[node_name]
|
||||
iq.reply().setPayload(node.info.xml).send()
|
||||
else:
|
||||
logging.debug("Node %s requested, but does not exist." % node_name)
|
||||
iq.reply().error().setPayload(iq['disco_info'].xml)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
iq.send()
|
||||
|
||||
def handle_disco_items(self, iq, forwarded=False):
|
||||
"""
|
||||
A default handler for disco#items requests. If another
|
||||
handler is registered, this one will defer and not run.
|
||||
log.debug("Using default handler for disco#info on node '%s'." % node_name)
|
||||
|
||||
If this handler is called by your own custom handler with
|
||||
forwarded set to True, then it will run as normal.
|
||||
"""
|
||||
handlers = self.xmpp.event_handlers['disco_items_request']
|
||||
if not forwarded and len(handlers) > 1:
|
||||
return
|
||||
if node_name in self.nodes:
|
||||
node = self.nodes[node_name]
|
||||
iq.reply().setPayload(node.info.xml).send()
|
||||
else:
|
||||
log.debug("Node %s requested, but does not exist." % node_name)
|
||||
iq.reply().error().setPayload(iq['disco_info'].xml)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
iq.send()
|
||||
|
||||
node_name = iq['disco_items']['node']
|
||||
if not node_name:
|
||||
node_name = 'main'
|
||||
def handle_disco_items(self, iq, forwarded=False):
|
||||
"""
|
||||
A default handler for disco#items requests. If another
|
||||
handler is registered, this one will defer and not run.
|
||||
|
||||
logging.debug("Using default handler for disco#items on node '%s'." % node_name)
|
||||
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('disco_items_request'):
|
||||
return
|
||||
|
||||
if node_name in self.nodes:
|
||||
node = self.nodes[node_name]
|
||||
iq.reply().setPayload(node.items.xml).send()
|
||||
else:
|
||||
logging.debug("Node %s requested, but does not exist." % node_name)
|
||||
iq.reply().error().setPayload(iq['disco_items'].xml)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
iq.send()
|
||||
node_name = iq['disco_items']['node']
|
||||
if not node_name:
|
||||
node_name = 'main'
|
||||
|
||||
# Older interface methods for backwards compatibility
|
||||
log.debug("Using default handler for disco#items on node '%s'." % node_name)
|
||||
|
||||
def getInfo(self, jid, node=''):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
iq['from'] = self.xmpp.fulljid
|
||||
iq['disco_info']['node'] = node
|
||||
iq.send()
|
||||
if node_name in self.nodes:
|
||||
node = self.nodes[node_name]
|
||||
iq.reply().setPayload(node.items.xml).send()
|
||||
else:
|
||||
log.debug("Node %s requested, but does not exist." % node_name)
|
||||
iq.reply().error().setPayload(iq['disco_items'].xml)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
iq.send()
|
||||
|
||||
def getItems(self, jid, node=''):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
iq['from'] = self.xmpp.fulljid
|
||||
iq['disco_items']['node'] = node
|
||||
iq.send()
|
||||
|
||||
def add_feature(self, feature, node='main'):
|
||||
self.add_node(node)
|
||||
self.nodes[node].addFeature(feature)
|
||||
|
||||
def add_identity(self, category='', itype='', name='', node='main'):
|
||||
self.add_node(node)
|
||||
self.nodes[node].addIdentity(category=category,
|
||||
id_type=itype,
|
||||
name=name)
|
||||
|
||||
def add_item(self, jid=None, name='', node='main', subnode=''):
|
||||
self.add_node(node)
|
||||
self.add_node(subnode)
|
||||
if jid is None:
|
||||
jid = self.xmpp.fulljid
|
||||
self.nodes[node].addItem(jid=jid, name=name, node=subnode)
|
||||
# Older interface methods for backwards compatibility
|
||||
|
||||
def getInfo(self, jid, node='', dfrom=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
iq['from'] = dfrom
|
||||
iq['disco_info']['node'] = node
|
||||
return iq.send()
|
||||
|
||||
def getItems(self, jid, node='', dfrom=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
iq['from'] = dfrom
|
||||
iq['disco_items']['node'] = node
|
||||
return iq.send()
|
||||
|
||||
def add_feature(self, feature, node='main'):
|
||||
self.add_node(node)
|
||||
self.nodes[node].addFeature(feature)
|
||||
|
||||
def add_identity(self, category='', itype='', name='', node='main'):
|
||||
self.add_node(node)
|
||||
self.nodes[node].addIdentity(category=category,
|
||||
id_type=itype,
|
||||
name=name)
|
||||
|
||||
def add_item(self, jid=None, name='', node='main', subnode=''):
|
||||
self.add_node(node)
|
||||
self.add_node(subnode)
|
||||
if jid is None:
|
||||
jid = self.xmpp.fulljid
|
||||
self.nodes[node].addItem(jid=jid, name=name, node=subnode)
|
||||
|
||||
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,32 +1,24 @@
|
||||
"""
|
||||
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'
|
||||
@@ -46,107 +38,128 @@ class MUCPresence(ElementBase):
|
||||
#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.")
|
||||
log.warning("Cannot set nick through mucpresence plugin.")
|
||||
return self
|
||||
|
||||
|
||||
def setRoom(self, value):
|
||||
logging.warning("Cannot set room through mucpresence plugin.")
|
||||
log.warning("Cannot set room through mucpresence plugin.")
|
||||
return self
|
||||
|
||||
|
||||
def delNick(self):
|
||||
logging.warning("Cannot delete nick through mucpresence plugin.")
|
||||
log.warning("Cannot delete nick through mucpresence plugin.")
|
||||
return self
|
||||
|
||||
|
||||
def delRoom(self):
|
||||
logging.warning("Cannot delete room through mucpresence plugin.")
|
||||
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)
|
||||
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))
|
||||
|
||||
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'].getValues()
|
||||
entry = pr['muc'].getStanzaValues()
|
||||
entry['show'] = pr['show']
|
||||
entry['status'] = pr['status']
|
||||
if pr['type'] == 'unavailable':
|
||||
del self.rooms[entry['room']][entry['nick']]
|
||||
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
|
||||
logging.debug("MUC presence from %s/%s : %s" % (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]
|
||||
@@ -154,6 +167,12 @@ class xep_0045(base.base_plugin):
|
||||
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
|
||||
@@ -166,14 +185,14 @@ class xep_0045(base.base_plugin):
|
||||
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)
|
||||
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['xep_0004'].makeForm(ftype='submit')
|
||||
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
||||
#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:
|
||||
@@ -186,7 +205,7 @@ class xep_0045(base.base_plugin):
|
||||
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.
|
||||
"""
|
||||
@@ -196,9 +215,13 @@ class xep_0045(base.base_plugin):
|
||||
passelement = ET.Element('password')
|
||||
passelement.text = password
|
||||
x.append(passelement)
|
||||
history = ET.Element('history')
|
||||
history.attrib['maxstanzas'] = maxhistory
|
||||
x.append(history)
|
||||
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)
|
||||
@@ -208,7 +231,7 @@ class xep_0045(base.base_plugin):
|
||||
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:
|
||||
@@ -234,9 +257,9 @@ class xep_0045(base.base_plugin):
|
||||
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})
|
||||
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
|
||||
else:
|
||||
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||
query.append(item)
|
||||
iq = self.xmpp.makeIqSet(query)
|
||||
iq['to'] = room
|
||||
@@ -244,7 +267,7 @@ class xep_0045(base.base_plugin):
|
||||
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)
|
||||
@@ -259,15 +282,19 @@ class xep_0045(base.base_plugin):
|
||||
msg.append(x)
|
||||
self.xmpp.send(msg)
|
||||
|
||||
def leaveMUC(self, room, nick):
|
||||
def leaveMUC(self, room, nick, msg=''):
|
||||
""" Leave the specified room.
|
||||
"""
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||
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):
|
||||
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
||||
iq['to'] = room
|
||||
iq['from'] = self.xmpp.jid
|
||||
result = iq.send()
|
||||
if result is None or result['type'] != 'result':
|
||||
raise ValueError
|
||||
@@ -275,30 +302,31 @@ class xep_0045(base.base_plugin):
|
||||
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['from'] = self.xmpp.jid
|
||||
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.
|
||||
@@ -307,7 +335,7 @@ class xep_0045(base.base_plugin):
|
||||
return self.rooms[room][nick][jidProperty]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def getRoster(self, room):
|
||||
""" Get the list of nicks in a room.
|
||||
"""
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
"""
|
||||
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):
|
||||
@@ -32,11 +19,11 @@ 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']
|
||||
@@ -83,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=[]))
|
||||
@@ -94,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
|
||||
|
||||
@@ -2,8 +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):
|
||||
"""
|
||||
@@ -13,7 +18,7 @@ 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, ntype=None):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
create = ET.Element('create')
|
||||
@@ -41,7 +46,8 @@ class xep_0060(base.base_plugin):
|
||||
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
|
||||
@@ -50,7 +56,7 @@ class xep_0060(base.base_plugin):
|
||||
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')
|
||||
@@ -70,7 +76,7 @@ class xep_0060(base.base_plugin):
|
||||
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')
|
||||
@@ -90,7 +96,7 @@ class xep_0060(base.base_plugin):
|
||||
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:
|
||||
@@ -108,17 +114,17 @@ class xep_0060(base.base_plugin):
|
||||
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
||||
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')
|
||||
@@ -131,7 +137,7 @@ class xep_0060(base.base_plugin):
|
||||
id = iq['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')
|
||||
@@ -154,7 +160,7 @@ class xep_0060(base.base_plugin):
|
||||
id = iq['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')
|
||||
@@ -179,8 +185,8 @@ class xep_0060(base.base_plugin):
|
||||
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')
|
||||
@@ -193,10 +199,10 @@ class xep_0060(base.base_plugin):
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
id = iq['id']
|
||||
result = iq.send()
|
||||
if result is None or result['type'] == 'error':
|
||||
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')
|
||||
@@ -216,7 +222,7 @@ class xep_0060(base.base_plugin):
|
||||
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)
|
||||
|
||||
@@ -235,7 +241,7 @@ class xep_0060(base.base_plugin):
|
||||
result = iq.send()
|
||||
if result is None or result is False or result['type'] == 'error': return False
|
||||
return True
|
||||
|
||||
|
||||
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')
|
||||
@@ -244,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')
|
||||
@@ -262,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
|
||||
@@ -296,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,14 +26,14 @@ 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
|
||||
@@ -59,12 +50,12 @@ 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)
|
||||
@@ -76,6 +67,6 @@ class xep_0078(base.base_plugin):
|
||||
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")
|
||||
|
||||
104
sleekxmpp/plugins/xep_0085.py
Normal file
104
sleekxmpp/plugins/xep_0085.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
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 permissio
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatState(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/chatstates'
|
||||
plugin_attrib = 'chat_state'
|
||||
interface = set(('state',))
|
||||
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
|
||||
|
||||
def active(self):
|
||||
self.setState('active')
|
||||
|
||||
def composing(self):
|
||||
self.setState('composing')
|
||||
|
||||
def gone(self):
|
||||
self.setState('gone')
|
||||
|
||||
def inactive(self):
|
||||
self.setState('inactive')
|
||||
|
||||
def paused(self):
|
||||
self.setState('paused')
|
||||
|
||||
def setState(self, state):
|
||||
if state in self.states:
|
||||
self.name = state
|
||||
self.xml.tag = '{%s}%s' % (self.namespace, state)
|
||||
else:
|
||||
raise ValueError('Invalid chat state')
|
||||
|
||||
def getState(self):
|
||||
return self.name
|
||||
|
||||
# In order to match the various chat state elements,
|
||||
# we need one stanza object per state, even though
|
||||
# they are all the same except for the initial name
|
||||
# value. Do not depend on the type of the chat state
|
||||
# stanza object for the actual state.
|
||||
|
||||
class Active(ChatState):
|
||||
name = 'active'
|
||||
class Composing(ChatState):
|
||||
name = 'composing'
|
||||
class Gone(ChatState):
|
||||
name = 'gone'
|
||||
class Inactive(ChatState):
|
||||
name = 'inactive'
|
||||
class Paused(ChatState):
|
||||
name = 'paused'
|
||||
|
||||
|
||||
class xep_0085(base.base_plugin):
|
||||
"""
|
||||
XEP-0085 Chat State Notifications
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0085'
|
||||
self.description = 'Chat State Notifications'
|
||||
|
||||
handlers = [('Active Chat State', 'active'),
|
||||
('Composing Chat State', 'composing'),
|
||||
('Gone Chat State', 'gone'),
|
||||
('Inactive Chat State', 'inactive'),
|
||||
('Paused Chat State', 'paused')]
|
||||
for handler in handlers:
|
||||
self.xmpp.registerHandler(
|
||||
Callback(handler[0],
|
||||
MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns,
|
||||
ChatState.namespace,
|
||||
handler[1])),
|
||||
self._handleChatState))
|
||||
|
||||
registerStanzaPlugin(Message, Active)
|
||||
registerStanzaPlugin(Message, Composing)
|
||||
registerStanzaPlugin(Message, Gone)
|
||||
registerStanzaPlugin(Message, Inactive)
|
||||
registerStanzaPlugin(Message, Paused)
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates')
|
||||
|
||||
def _handleChatState(self, msg):
|
||||
state = msg['chat_state'].name
|
||||
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
|
||||
self.xmpp.event('chatstate_%s' % state, msg)
|
||||
@@ -1,21 +1,9 @@
|
||||
"""
|
||||
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 xml.etree import cElementTree as ET
|
||||
from . import base
|
||||
@@ -30,7 +18,7 @@ class xep_0092(base.base_plugin):
|
||||
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)
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version, name='Sofware Version')
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
|
||||
51
sleekxmpp/plugins/xep_0128.py
Normal file
51
sleekxmpp/plugins/xep_0128.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
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.iq import Iq
|
||||
from . xep_0030 import DiscoInfo, DiscoItems
|
||||
from . xep_0004 import Form
|
||||
|
||||
|
||||
class xep_0128(base.base_plugin):
|
||||
"""
|
||||
XEP-0128 Service Discovery Extensions
|
||||
"""
|
||||
|
||||
def plugin_init(self):
|
||||
self.xep = '0128'
|
||||
self.description = 'Service Discovery Extensions'
|
||||
|
||||
registerStanzaPlugin(DiscoInfo, Form)
|
||||
registerStanzaPlugin(DiscoItems, Form)
|
||||
|
||||
def extend_info(self, node, data=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
node = self.xmpp['xep_0030'].nodes.get(node, None)
|
||||
if node is None:
|
||||
self.xmpp['xep_0030'].add_node(node)
|
||||
|
||||
info = node.info
|
||||
info['form']['type'] = 'result'
|
||||
info['form'].setFields(data, default=None)
|
||||
|
||||
def extend_items(self, node, data=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
node = self.xmpp['xep_0030'].nodes.get(node, None)
|
||||
if node is None:
|
||||
self.xmpp['xep_0030'].add_node(node)
|
||||
|
||||
items = node.items
|
||||
items['form']['type'] = 'result'
|
||||
items['form'].setFields(data, default=None)
|
||||
@@ -1,72 +1,63 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
XEP-0199 (Ping) support
|
||||
Copyright (C) 2007 Kevin Smith
|
||||
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 xml.etree import cElementTree as ET
|
||||
from . import base
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class xep_0199(base.base_plugin):
|
||||
"""XEP-0199 XMPP Ping"""
|
||||
"""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):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['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 plugin_init(self):
|
||||
self.description = "XMPP Ping"
|
||||
self.xep = "0199"
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='urn:xmpp:ping'/></iq>" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping')
|
||||
if self.config.get('keepalive', True):
|
||||
self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
|
||||
|
||||
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)
|
||||
pingresult = iq.send()
|
||||
endTime = time.clock()
|
||||
if pingresult == False:
|
||||
#self.xmpp.disconnect(reconnect=True)
|
||||
return False
|
||||
return endTime - startTime
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:ping')
|
||||
|
||||
def handler_pingserver(self, xml):
|
||||
self.xmpp.schedule("xep-0119 ping", float(self.config.get('frequency', 300)), self.scheduled_ping, repeat=True)
|
||||
|
||||
def scheduled_ping(self):
|
||||
log.debug("pinging...")
|
||||
if self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is False:
|
||||
log.debug("Did not recieve ping back in time. Requesting Reconnect.")
|
||||
self.xmpp.reconnect()
|
||||
|
||||
def handler_ping(self, xml):
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.boundjid.domain)
|
||||
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('{urn:xmpp:ping}ping')
|
||||
iq.append(ping)
|
||||
startTime = time.clock()
|
||||
#pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout)
|
||||
pingresult = iq.send()
|
||||
endTime = time.clock()
|
||||
if pingresult == False:
|
||||
#self.xmpp.disconnect(reconnect=True)
|
||||
return False
|
||||
return endTime - startTime
|
||||
|
||||
115
sleekxmpp/plugins/xep_0202.py
Normal file
115
sleekxmpp/plugins/xep_0202.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
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_utc(self): # TODO: return a datetime.tzinfo object?
|
||||
#pass
|
||||
|
||||
def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects?
|
||||
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-%d.%fT%H:%M:%SZ')
|
||||
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
|
||||
@@ -3,6 +3,11 @@
|
||||
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.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,139 @@
|
||||
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', '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'))
|
||||
interfaces = set(('code', '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.
|
||||
"""
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.getCondition = self.get_condition
|
||||
self.setCondition = self.set_condition
|
||||
self.delCondition = self.del_condition
|
||||
self.getText = self.get_text
|
||||
self.setText = self.set_text
|
||||
self.delText = self.del_text
|
||||
|
||||
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
|
||||
|
||||
@@ -3,33 +3,95 @@
|
||||
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 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 getHtml(self):
|
||||
html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
|
||||
if html is None: return ''
|
||||
return html
|
||||
|
||||
def delHtml(self):
|
||||
if self.parent is not None:
|
||||
self.parent().xml.remove(self.xml)
|
||||
"""
|
||||
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 setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides StanzaBase.setup.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.setBody = self.set_body
|
||||
self.getBody = self.get_body
|
||||
self.delBody = self.del_body
|
||||
|
||||
return ElementBase.setup(self, xml)
|
||||
|
||||
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)
|
||||
|
||||
@@ -3,75 +3,181 @@
|
||||
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 RESPONSE_TIMEOUT, StanzaBase, ET
|
||||
from sleekxmpp.xmlstream.handler import Waiter
|
||||
from sleekxmpp.xmlstream.matcher import MatcherId
|
||||
|
||||
|
||||
class Iq(RootStanza):
|
||||
interfaces = set(('type', 'to', 'from', 'id','query'))
|
||||
types = set(('get', 'result', 'set', 'error'))
|
||||
name = 'iq'
|
||||
plugin_attrib = name
|
||||
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)
|
||||
return self
|
||||
|
||||
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)
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.setPayload = self.set_payload
|
||||
self.getQuery = self.get_query
|
||||
self.setQuery = self.set_query
|
||||
self.delQuery = self.del_query
|
||||
|
||||
if self['id'] == '':
|
||||
if self.stream is not None:
|
||||
self['id'] = self.stream.getNewId()
|
||||
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):
|
||||
"""
|
||||
Send a reply <iq> stanza.
|
||||
|
||||
Overrides StanzaBase.reply
|
||||
|
||||
Sets the 'type' to 'result' in addition to the default
|
||||
StanzaBase.reply behavior.
|
||||
"""
|
||||
self['type'] = 'result'
|
||||
StanzaBase.reply(self)
|
||||
return self
|
||||
|
||||
def send(self, block=True, timeout=RESPONSE_TIMEOUT):
|
||||
"""
|
||||
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.
|
||||
|
||||
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
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -3,61 +3,163 @@
|
||||
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'
|
||||
plugin_attrib = name
|
||||
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 setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides StanzaBase.setup.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.getType = self.get_type
|
||||
self.getMucroom = self.get_mucroom
|
||||
self.setMucroom = self.set_mucroom
|
||||
self.delMucroom = self.del_mucroom
|
||||
self.getMucnick = self.get_mucnick
|
||||
self.setMucnick = self.set_mucnick
|
||||
self.delMucnick = self.del_mucnick
|
||||
|
||||
return StanzaBase.setup(self, xml)
|
||||
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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 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
|
||||
|
||||
@@ -3,24 +3,87 @@
|
||||
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):
|
||||
if self.parent is not None:
|
||||
self.parent().xml.remove(self.xml)
|
||||
"""
|
||||
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/nick/nick'
|
||||
name = 'nick'
|
||||
plugin_attrib = name
|
||||
interfaces = set(('nick',))
|
||||
|
||||
def setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides StanzaBase.setup.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.setNick = self.set_nick
|
||||
self.getNick = self.get_nick
|
||||
self.delNick = self.del_nick
|
||||
|
||||
return ElementBase.setup(self, xml)
|
||||
|
||||
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)
|
||||
|
||||
@@ -3,61 +3,184 @@
|
||||
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'
|
||||
plugin_attrib = name
|
||||
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', text = 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 setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides ElementBase.setup.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.setShow = self.set_show
|
||||
self.getType = self.get_type
|
||||
self.setType = self.set_type
|
||||
self.delType = self.get_type
|
||||
self.getPriority = self.get_priority
|
||||
self.setPriority = self.set_priority
|
||||
|
||||
return StanzaBase.setup(self, xml)
|
||||
|
||||
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):
|
||||
"""
|
||||
Set the appropriate presence reply type.
|
||||
|
||||
Overrides StanzaBase.reply.
|
||||
"""
|
||||
if self['type'] == 'unsubscribe':
|
||||
self['type'] = 'unsubscribed'
|
||||
elif self['type'] == 'subscribe':
|
||||
self['type'] = 'subscribed'
|
||||
return StanzaBase.reply(self)
|
||||
|
||||
@@ -3,34 +3,67 @@
|
||||
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'
|
||||
if sys.version_info < (3,0):
|
||||
self['error']['text'] = "SleekXMPP got into trouble."
|
||||
else:
|
||||
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
|
||||
"""
|
||||
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'].append(extxml)
|
||||
self['error']['type'] = e.etype
|
||||
else:
|
||||
# We probably didn't raise this on purpose, so send a traceback
|
||||
self['error']['condition'] = 'undefined-condition'
|
||||
if sys.version_info < (3, 0):
|
||||
self['error']['text'] = "SleekXMPP got into trouble."
|
||||
else:
|
||||
self['error']['text'] = traceback.format_tb(e.__traceback__)
|
||||
log.exception('Error handling {%s}%s stanza' %
|
||||
(self.namespace, self.name))
|
||||
self.send()
|
||||
|
||||
|
||||
register_stanza_plugin(RootStanza, Error)
|
||||
|
||||
@@ -3,51 +3,123 @@
|
||||
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 setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides StanzaBase.setup.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.setItems = self.set_items
|
||||
self.getItems = self.get_items
|
||||
self.delItems = self.del_items
|
||||
|
||||
return ElementBase.setup(self, xml)
|
||||
|
||||
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)
|
||||
|
||||
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 *
|
||||
145
sleekxmpp/test/livesocket.py
Normal file
145
sleekxmpp/test/livesocket.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
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 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.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 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)
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
140
sleekxmpp/test/mocksocket.py
Normal file
140
sleekxmpp/test/mocksocket.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Socket Interface
|
||||
|
||||
def recv(self, *args, **kwargs):
|
||||
"""
|
||||
Read a value from the received queue.
|
||||
|
||||
Arguments:
|
||||
Placeholders. Same as for socket.Socket.recv.
|
||||
"""
|
||||
return self.read(block=True)
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Send data by placing it in the send queue.
|
||||
|
||||
Arguments:
|
||||
data -- String value to write.
|
||||
"""
|
||||
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 timeout is not None:
|
||||
block = True
|
||||
try:
|
||||
return self.recv_queue.get(block, timeout)
|
||||
except:
|
||||
return None
|
||||
628
sleekxmpp/test/sleektest.py
Normal file
628
sleekxmpp/test/sleektest.py
Normal file
@@ -0,0 +1,628 @@
|
||||
"""
|
||||
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
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import ClientXMPP, ComponentXMPP
|
||||
from sleekxmpp.stanza import Message, Iq, Presence
|
||||
from sleekxmpp.test import TestSocket, TestLiveSocket
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.tostring import tostring
|
||||
|
||||
|
||||
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 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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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(None, *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(None, *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(None, *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, xml_string,
|
||||
defaults=None, use_values=True):
|
||||
"""
|
||||
Create and compare several stanza objects to a correct XML string.
|
||||
|
||||
If use_values is False, test using getStanzaValues() and
|
||||
setStanzaValues() 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.
|
||||
xml_string -- A string version of the correct XML expected.
|
||||
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 getStanzaValues() and
|
||||
setStanzaValues() should be used. Defaults to
|
||||
True.
|
||||
"""
|
||||
stanza_class = stanza.__class__
|
||||
xml = self.parse_xml(xml_string)
|
||||
|
||||
# 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 getStanzaValues() and setStanzaValues() 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.getStanzaValues()
|
||||
stanza3 = stanza_class()
|
||||
stanza3.setStanzaValues(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_start(self, mode='client', skip=True, header=None,
|
||||
socket='mock', jid='tester@localhost',
|
||||
password='test', server='localhost',
|
||||
port=5222):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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.")
|
||||
|
||||
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
|
||||
self.xmpp.connect()
|
||||
else:
|
||||
raise ValueError("Unknown socket type.")
|
||||
|
||||
self.xmpp.register_plugins()
|
||||
self.xmpp.process(threaded=True)
|
||||
if skip:
|
||||
# Clear startup stanzas
|
||||
self.xmpp.socket.next_sent(timeout=1)
|
||||
if mode == 'component':
|
||||
self.xmpp.socket.next_sent(timeout=1)
|
||||
|
||||
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, stanza_class=StanzaBase, defaults=[],
|
||||
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 -- String stanza XML to be received and processed by
|
||||
the XMPP client or component.
|
||||
stanza_class -- The stanza object class for verifying data received
|
||||
by a live connection. Defaults to StanzaBase.
|
||||
defaults -- A list of stanza interfaces with default values that
|
||||
may interfere with comparisons.
|
||||
use_values -- Indicates if stanza comparisons should test using
|
||||
getStanzaValues() and setStanzaValues().
|
||||
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:
|
||||
return False
|
||||
stanza = stanza_class(xml=self.parse_xml(recv_data))
|
||||
return self.check(stanza_class, stanza, data,
|
||||
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, use_values=True, timeout=1):
|
||||
"""
|
||||
"""
|
||||
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:
|
||||
return False
|
||||
xml = self.parse_xml(data)
|
||||
recv_xml = self.parse_xml(recv_data)
|
||||
self.failUnless(self.compare(xml, recv_xml),
|
||||
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
|
||||
tostring(xml), tostring(recv_xml)))
|
||||
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, use_values=True, timeout=1):
|
||||
"""
|
||||
"""
|
||||
sent_data = self.xmpp.socket.next_sent(timeout)
|
||||
if sent_data is None:
|
||||
return False
|
||||
xml = self.parse_xml(data)
|
||||
sent_xml = self.parse_xml(sent_data)
|
||||
self.failUnless(self.compare(xml, sent_xml),
|
||||
"Features do not match.\nDesired:\n%s\nSent:\n%s" % (
|
||||
tostring(xml), tostring(sent_xml)))
|
||||
|
||||
def send(self, data, defaults=None,
|
||||
use_values=True, timeout=.1):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
xml = self.parse_xml(data)
|
||||
self.fix_namespaces(xml, 'jabber:client')
|
||||
data = self.xmpp._build_stanza(xml, 'jabber:client')
|
||||
sent = self.xmpp.socket.next_sent(timeout)
|
||||
self.check(data, sent,
|
||||
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
|
||||
0
sleekxmpp/thirdparty/__init__.py
vendored
Normal file
0
sleekxmpp/thirdparty/__init__.py
vendored
Normal file
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()))
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
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.jid import JID
|
||||
from sleekxmpp.xmlstream.scheduler import Scheduler
|
||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET
|
||||
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.tostring import tostring
|
||||
from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
|
||||
from sleekxmpp.xmlstream.xmlstream import RestartStream
|
||||
|
||||
__all__ = ['JID', 'Scheduler', 'StanzaBase', 'ElementBase',
|
||||
'ET', 'StateMachine', 'tostring', 'XMLStream',
|
||||
'RESPONSE_TIMEOUT', 'RestartStream']
|
||||
|
||||
@@ -3,23 +3,39 @@
|
||||
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 socket import _fileobject
|
||||
import socket
|
||||
|
||||
class filesocket(_fileobject):
|
||||
|
||||
def read(self, size=4096):
|
||||
data = self._sock.recv(size)
|
||||
if data is not None:
|
||||
return data
|
||||
class FileSocket(_fileobject):
|
||||
|
||||
"""
|
||||
Create a file object wrapper for a socket to work around
|
||||
issues present in Python 2.6 when using sockets as file objects.
|
||||
|
||||
The parser for xml.etree.cElementTree requires a file, but we will
|
||||
be reading from the XMPP connection socket instead.
|
||||
"""
|
||||
|
||||
def read(self, size=4096):
|
||||
"""Read data from the socket as if it were a file."""
|
||||
data = self._sock.recv(size)
|
||||
if data is not None:
|
||||
return data
|
||||
|
||||
|
||||
class Socket26(socket._socketobject):
|
||||
|
||||
def makefile(self, mode='r', bufsize=-1):
|
||||
"""makefile([mode[, bufsize]]) -> file object
|
||||
Return a regular file object corresponding to the socket. The mode
|
||||
and bufsize arguments are as for the built-in open() function."""
|
||||
return filesocket(self._sock, mode, bufsize)
|
||||
"""
|
||||
A custom socket implementation that uses our own FileSocket class
|
||||
to work around issues in Python 2.6 when using sockets as files.
|
||||
"""
|
||||
|
||||
def makefile(self, mode='r', bufsize=-1):
|
||||
"""makefile([mode[, bufsize]]) -> file object
|
||||
Return a regular file object corresponding to the socket. The mode
|
||||
and bufsize arguments are as for the built-in open() function."""
|
||||
return FileSocket(self._sock, mode, bufsize)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
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.handler.callback import Callback
|
||||
from sleekxmpp.xmlstream.handler.waiter import Waiter
|
||||
from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback
|
||||
from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter
|
||||
|
||||
__all__ = ['Callback', 'Waiter', 'XMLCallback', 'XMLWaiter']
|
||||
|
||||
@@ -3,26 +3,87 @@
|
||||
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 BaseHandler(object):
|
||||
|
||||
"""
|
||||
Base class for stream handlers. Stream handlers are matched with
|
||||
incoming stanzas so that the stanza may be processed in some way.
|
||||
Stanzas may be matched with multiple handlers.
|
||||
|
||||
def __init__(self, name, matcher):
|
||||
self.name = name
|
||||
self._destroy = False
|
||||
self._payload = None
|
||||
self._matcher = matcher
|
||||
|
||||
def match(self, xml):
|
||||
return self._matcher.match(xml)
|
||||
|
||||
def prerun(self, payload): # what's the point of this if the payload is called again in run??
|
||||
self._payload = payload
|
||||
Handler execution may take place in two phases. The first is during
|
||||
the stream processing itself. The second is after stream processing
|
||||
and during SleekXMPP's main event loop. The prerun method is used
|
||||
for execution during stream processing, and the run method is used
|
||||
during the main event loop.
|
||||
|
||||
def run(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def checkDelete(self):
|
||||
return self._destroy
|
||||
Attributes:
|
||||
name -- The name of the handler.
|
||||
stream -- The stream this handler is assigned to.
|
||||
|
||||
Methods:
|
||||
match -- Compare a stanza with the handler's matcher.
|
||||
prerun -- Handler execution during stream processing.
|
||||
run -- Handler execution during the main event loop.
|
||||
check_delete -- Indicate if the handler may be removed from use.
|
||||
"""
|
||||
|
||||
def __init__(self, name, matcher, stream=None):
|
||||
"""
|
||||
Create a new stream handler.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the handler.
|
||||
matcher -- A matcher object from xmlstream.matcher that will be
|
||||
used to determine if a stanza should be accepted by
|
||||
this handler.
|
||||
stream -- The XMLStream instance the handler should monitor.
|
||||
"""
|
||||
self.checkDelete = self.check_delete
|
||||
|
||||
self.name = name
|
||||
self.stream = stream
|
||||
self._destroy = False
|
||||
self._payload = None
|
||||
self._matcher = matcher
|
||||
if stream is not None:
|
||||
stream.registerHandler(self)
|
||||
|
||||
def match(self, xml):
|
||||
"""
|
||||
Compare a stanza or XML object with the handler's matcher.
|
||||
|
||||
Arguments
|
||||
xml -- An XML or stanza object.
|
||||
"""
|
||||
return self._matcher.match(xml)
|
||||
|
||||
def prerun(self, payload):
|
||||
"""
|
||||
Prepare the handler for execution while the XML stream is being
|
||||
processed.
|
||||
|
||||
Arguments:
|
||||
payload -- A stanza object.
|
||||
"""
|
||||
self._payload = payload
|
||||
|
||||
def run(self, payload):
|
||||
"""
|
||||
Execute the handler after XML stream processing and during the
|
||||
main event loop.
|
||||
|
||||
Arguments:
|
||||
payload -- A stanza object.
|
||||
"""
|
||||
self._payload = payload
|
||||
|
||||
def check_delete(self):
|
||||
"""
|
||||
Check if the handler should be removed from the list of stream
|
||||
handlers.
|
||||
"""
|
||||
return self._destroy
|
||||
|
||||
@@ -3,34 +3,82 @@
|
||||
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 . import base
|
||||
import logging
|
||||
|
||||
class Callback(base.BaseHandler):
|
||||
|
||||
def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False):
|
||||
base.BaseHandler.__init__(self, name, matcher)
|
||||
self._pointer = pointer
|
||||
self._thread = thread
|
||||
self._once = once
|
||||
self._instream = instream
|
||||
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
||||
|
||||
def prerun(self, payload): # prerun actually calls run?!? WTF! Then it gets run AGAIN!
|
||||
base.BaseHandler.prerun(self, payload)
|
||||
if self._instream:
|
||||
logging.debug('callback "%s" prerun', self.name)
|
||||
self.run(payload, True)
|
||||
|
||||
def run(self, payload, instream=False):
|
||||
if not self._instream or instream:
|
||||
logging.debug('callback "%s" run', self.name)
|
||||
base.BaseHandler.run(self, payload)
|
||||
#if self._thread:
|
||||
# x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
|
||||
# x.start()
|
||||
#else:
|
||||
self._pointer(payload)
|
||||
if self._once:
|
||||
self._destroy = True
|
||||
|
||||
class Callback(BaseHandler):
|
||||
|
||||
"""
|
||||
The Callback handler will execute a callback function with
|
||||
matched stanzas.
|
||||
|
||||
The handler may execute the callback either during stream
|
||||
processing or during the main event loop.
|
||||
|
||||
Callback functions are all executed in the same thread, so be
|
||||
aware if you are executing functions that will block for extended
|
||||
periods of time. Typically, you should signal your own events using the
|
||||
SleekXMPP object's event() method to pass the stanza off to a threaded
|
||||
event handler for further processing.
|
||||
|
||||
Methods:
|
||||
prerun -- Overrides BaseHandler.prerun
|
||||
run -- Overrides BaseHandler.run
|
||||
"""
|
||||
|
||||
def __init__(self, name, matcher, pointer, thread=False,
|
||||
once=False, instream=False, stream=None):
|
||||
"""
|
||||
Create a new callback handler.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the handler.
|
||||
matcher -- A matcher object for matching stanza objects.
|
||||
pointer -- The function to execute during callback.
|
||||
thread -- DEPRECATED. Remains only for backwards compatibility.
|
||||
once -- Indicates if the handler should be used only
|
||||
once. Defaults to False.
|
||||
instream -- Indicates if the callback should be executed
|
||||
during stream processing instead of in the
|
||||
main event loop.
|
||||
stream -- The XMLStream instance this handler should monitor.
|
||||
"""
|
||||
BaseHandler.__init__(self, name, matcher, stream)
|
||||
self._pointer = pointer
|
||||
self._once = once
|
||||
self._instream = instream
|
||||
|
||||
def prerun(self, payload):
|
||||
"""
|
||||
Execute the callback during stream processing, if
|
||||
the callback was created with instream=True.
|
||||
|
||||
Overrides BaseHandler.prerun
|
||||
|
||||
Arguments:
|
||||
payload -- The matched stanza object.
|
||||
"""
|
||||
BaseHandler.prerun(self, payload)
|
||||
if self._instream:
|
||||
self.run(payload, True)
|
||||
|
||||
def run(self, payload, instream=False):
|
||||
"""
|
||||
Execute the callback function with the matched stanza payload.
|
||||
|
||||
Overrides BaseHandler.run
|
||||
|
||||
Arguments:
|
||||
payload -- The matched stanza object.
|
||||
instream -- Force the handler to execute during
|
||||
stream processing. Used only by prerun.
|
||||
Defaults to False.
|
||||
"""
|
||||
if not self._instream or instream:
|
||||
BaseHandler.run(self, payload)
|
||||
self._pointer(payload)
|
||||
if self._once:
|
||||
self._destroy = True
|
||||
|
||||
@@ -3,34 +3,99 @@
|
||||
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 . import base
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
import logging
|
||||
from .. stanzabase import StanzaBase
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
class Waiter(base.BaseHandler):
|
||||
|
||||
def __init__(self, name, matcher):
|
||||
base.BaseHandler.__init__(self, name, matcher)
|
||||
self._payload = queue.Queue()
|
||||
|
||||
def prerun(self, payload):
|
||||
self._payload.put(payload)
|
||||
|
||||
def run(self, payload):
|
||||
pass
|
||||
from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
|
||||
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
||||
|
||||
def wait(self, timeout=60):
|
||||
try:
|
||||
return self._payload.get(True, timeout)
|
||||
except queue.Empty:
|
||||
logging.warning("Timed out waiting for %s" % self.name)
|
||||
return False
|
||||
|
||||
def checkDelete(self):
|
||||
return True
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Waiter(BaseHandler):
|
||||
|
||||
"""
|
||||
The Waiter handler allows an event handler to block
|
||||
until a particular stanza has been received. The handler
|
||||
will either be given the matched stanza, or False if the
|
||||
waiter has timed out.
|
||||
|
||||
Methods:
|
||||
check_delete -- Overrides BaseHandler.check_delete
|
||||
prerun -- Overrides BaseHandler.prerun
|
||||
run -- Overrides BaseHandler.run
|
||||
wait -- Wait for a stanza to arrive and return it to
|
||||
an event handler.
|
||||
"""
|
||||
|
||||
def __init__(self, name, matcher, stream=None):
|
||||
"""
|
||||
Create a new Waiter.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the waiter.
|
||||
matcher -- A matcher object to detect the desired stanza.
|
||||
stream -- Optional XMLStream instance to monitor.
|
||||
"""
|
||||
BaseHandler.__init__(self, name, matcher, stream=stream)
|
||||
self._payload = queue.Queue()
|
||||
|
||||
def prerun(self, payload):
|
||||
"""
|
||||
Store the matched stanza.
|
||||
|
||||
Overrides BaseHandler.prerun
|
||||
|
||||
Arguments:
|
||||
payload -- The matched stanza object.
|
||||
"""
|
||||
self._payload.put(payload)
|
||||
|
||||
def run(self, payload):
|
||||
"""
|
||||
Do not process this handler during the main event loop.
|
||||
|
||||
Overrides BaseHandler.run
|
||||
|
||||
Arguments:
|
||||
payload -- The matched stanza object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def wait(self, timeout=RESPONSE_TIMEOUT):
|
||||
"""
|
||||
Block an event handler while waiting for a stanza to arrive.
|
||||
|
||||
Be aware that this will impact performance if called from a
|
||||
non-threaded event handler.
|
||||
|
||||
Will return either the received stanza, or False if the waiter
|
||||
timed out.
|
||||
|
||||
Arguments:
|
||||
timeout -- The number of seconds to wait for the stanza to
|
||||
arrive. Defaults to the global default timeout
|
||||
value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
|
||||
"""
|
||||
try:
|
||||
stanza = self._payload.get(True, timeout)
|
||||
except queue.Empty:
|
||||
stanza = False
|
||||
log.warning("Timed out waiting for %s" % self.name)
|
||||
self.stream.removeHandler(self.name)
|
||||
return stanza
|
||||
|
||||
def check_delete(self):
|
||||
"""
|
||||
Always remove waiters after use.
|
||||
|
||||
Overrides BaseHandler.check_delete
|
||||
"""
|
||||
return True
|
||||
|
||||
@@ -3,12 +3,34 @@
|
||||
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.
|
||||
"""
|
||||
import threading
|
||||
from . callback import Callback
|
||||
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
|
||||
|
||||
class XMLCallback(Callback):
|
||||
|
||||
def run(self, payload, instream=False):
|
||||
Callback.run(self, payload.xml, instream)
|
||||
|
||||
"""
|
||||
The XMLCallback class is identical to the normal Callback class,
|
||||
except that XML contents of matched stanzas will be processed instead
|
||||
of the stanza objects themselves.
|
||||
|
||||
Methods:
|
||||
run -- Overrides Callback.run
|
||||
"""
|
||||
|
||||
def run(self, payload, instream=False):
|
||||
"""
|
||||
Execute the callback function with the matched stanza's
|
||||
XML contents, instead of the stanza itself.
|
||||
|
||||
Overrides BaseHandler.run
|
||||
|
||||
Arguments:
|
||||
payload -- The matched stanza object.
|
||||
instream -- Force the handler to execute during
|
||||
stream processing. Used only by prerun.
|
||||
Defaults to False.
|
||||
"""
|
||||
Callback.run(self, payload.xml, instream)
|
||||
|
||||
@@ -3,11 +3,31 @@
|
||||
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 . waiter import Waiter
|
||||
|
||||
from sleekxmpp.xmlstream.handler import Waiter
|
||||
|
||||
|
||||
class XMLWaiter(Waiter):
|
||||
|
||||
def prerun(self, payload):
|
||||
Waiter.prerun(self, payload.xml)
|
||||
|
||||
"""
|
||||
The XMLWaiter class is identical to the normal Waiter class
|
||||
except that it returns the XML contents of the stanza instead
|
||||
of the full stanza object itself.
|
||||
|
||||
Methods:
|
||||
prerun -- Overrides Waiter.prerun
|
||||
"""
|
||||
|
||||
def prerun(self, payload):
|
||||
"""
|
||||
Store the XML contents of the stanza to return to the
|
||||
waiting event handler.
|
||||
|
||||
Overrides Waiter.prerun
|
||||
|
||||
Arguments:
|
||||
payload -- The matched stanza object.
|
||||
"""
|
||||
Waiter.prerun(self, payload.xml)
|
||||
|
||||
123
sleekxmpp/xmlstream/jid.py
Normal file
123
sleekxmpp/xmlstream/jid.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
|
||||
class JID(object):
|
||||
"""
|
||||
A representation of a Jabber ID, or JID.
|
||||
|
||||
Each JID may have three components: a user, a domain, and an optional
|
||||
resource. For example: user@domain/resource
|
||||
|
||||
When a resource is not used, the JID is called a bare JID.
|
||||
The JID is a full JID otherwise.
|
||||
|
||||
Attributes:
|
||||
jid -- Alias for 'full'.
|
||||
full -- The value of the full JID.
|
||||
bare -- The value of the bare JID.
|
||||
user -- The username portion of the JID.
|
||||
domain -- The domain name portion of the JID.
|
||||
server -- Alias for 'domain'.
|
||||
resource -- The resource portion of the JID.
|
||||
|
||||
Methods:
|
||||
reset -- Use a new JID value.
|
||||
regenerate -- Recreate the JID from its components.
|
||||
"""
|
||||
|
||||
def __init__(self, jid):
|
||||
"""Initialize a new JID"""
|
||||
self.reset(jid)
|
||||
|
||||
def reset(self, jid):
|
||||
"""
|
||||
Start fresh from a new JID string.
|
||||
|
||||
Arguments:
|
||||
jid - The new JID value.
|
||||
"""
|
||||
self._full = self._jid = str(jid)
|
||||
self._domain = None
|
||||
self._resource = None
|
||||
self._user = None
|
||||
self._bare = None
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Handle getting the JID values, using cache if available.
|
||||
|
||||
Arguments:
|
||||
name -- One of: user, server, domain, resource,
|
||||
full, or bare.
|
||||
"""
|
||||
if name == 'resource':
|
||||
if self._resource is None and '/' in self._jid:
|
||||
self._resource = self._jid.split('/', 1)[-1]
|
||||
return self._resource or ""
|
||||
elif name == 'user':
|
||||
if self._user is None:
|
||||
if '@' in self._jid:
|
||||
self._user = self._jid.split('@', 1)[0]
|
||||
else:
|
||||
self._user = self._user
|
||||
return self._user or ""
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
if self._domain is None:
|
||||
self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
|
||||
return self._domain or ""
|
||||
elif name == 'full':
|
||||
return self._jid or ""
|
||||
elif name == 'bare':
|
||||
if self._bare is None:
|
||||
self._bare = self._jid.split('/', 1)[0]
|
||||
return self._bare or ""
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
Edit a JID by updating it's individual values, resetting the
|
||||
generated JID in the end.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the JID part. One of: user, domain,
|
||||
server, resource, full, jid, or bare.
|
||||
value -- The new value for the JID part.
|
||||
"""
|
||||
if name in ('resource', 'user', 'domain'):
|
||||
object.__setattr__(self, "_%s" % name, value)
|
||||
self.regenerate()
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
self.domain = value
|
||||
elif name in ('full', 'jid'):
|
||||
self.reset(value)
|
||||
self.regenerate()
|
||||
elif name == 'bare':
|
||||
if '@' in value:
|
||||
u, d = value.split('@', 1)
|
||||
object.__setattr__(self, "_user", u)
|
||||
object.__setattr__(self, "_domain", d)
|
||||
else:
|
||||
object.__setattr__(self, "_user", '')
|
||||
object.__setattr__(self, "_domain", value)
|
||||
self.regenerate()
|
||||
else:
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def regenerate(self):
|
||||
"""Generate a new JID based on current values, useful after editing."""
|
||||
jid = ""
|
||||
if self.user:
|
||||
jid = "%s@" % self.user
|
||||
jid += self.domain
|
||||
if self.resource:
|
||||
jid += "/%s" % self.resource
|
||||
self.reset(jid)
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return self.full
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
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.matcher.id import MatcherId
|
||||
from sleekxmpp.xmlstream.matcher.many import MatchMany
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
|
||||
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
|
||||
|
||||
__all__ = ['MatcherId', 'MatchMany', 'StanzaPath',
|
||||
'MatchXMLMask', 'MatchXPath']
|
||||
|
||||
@@ -3,12 +3,32 @@
|
||||
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 MatcherBase(object):
|
||||
|
||||
def __init__(self, criteria):
|
||||
self._criteria = criteria
|
||||
|
||||
def match(self, xml):
|
||||
return False
|
||||
"""
|
||||
Base class for stanza matchers. Stanza matchers are used to pick
|
||||
stanzas out of the XML stream and pass them to the appropriate
|
||||
stream handlers.
|
||||
"""
|
||||
|
||||
def __init__(self, criteria):
|
||||
"""
|
||||
Create a new stanza matcher.
|
||||
|
||||
Arguments:
|
||||
criteria -- Object to compare some aspect of a stanza
|
||||
against.
|
||||
"""
|
||||
self._criteria = criteria
|
||||
|
||||
def match(self, xml):
|
||||
"""
|
||||
Check if a stanza matches the stored criteria.
|
||||
|
||||
Meant to be overridden.
|
||||
"""
|
||||
return False
|
||||
|
||||
@@ -3,11 +3,30 @@
|
||||
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 . import base
|
||||
|
||||
class MatcherId(base.MatcherBase):
|
||||
|
||||
def match(self, xml):
|
||||
return xml['id'] == self._criteria
|
||||
from sleekxmpp.xmlstream.matcher.base import MatcherBase
|
||||
|
||||
|
||||
class MatcherId(MatcherBase):
|
||||
|
||||
"""
|
||||
The ID matcher selects stanzas that have the same stanza 'id'
|
||||
interface value as the desired ID.
|
||||
|
||||
Methods:
|
||||
match -- Overrides MatcherBase.match.
|
||||
"""
|
||||
|
||||
def match(self, xml):
|
||||
"""
|
||||
Compare the given stanza's 'id' attribute to the stored
|
||||
id value.
|
||||
|
||||
Overrides MatcherBase.match.
|
||||
|
||||
Arguments:
|
||||
xml -- The stanza to compare against.
|
||||
"""
|
||||
return xml['id'] == self._criteria
|
||||
|
||||
@@ -3,15 +3,38 @@
|
||||
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 . import base
|
||||
from xml.etree import cElementTree
|
||||
|
||||
class MatchMany(base.MatcherBase):
|
||||
from sleekxmpp.xmlstream.matcher.base import MatcherBase
|
||||
|
||||
def match(self, xml):
|
||||
for m in self._criteria:
|
||||
if m.match(xml):
|
||||
return True
|
||||
return False
|
||||
|
||||
class MatchMany(MatcherBase):
|
||||
|
||||
"""
|
||||
The MatchMany matcher may compare a stanza against multiple
|
||||
criteria. It is essentially an OR relation combining multiple
|
||||
matchers.
|
||||
|
||||
Each of the criteria must implement a match() method.
|
||||
|
||||
Methods:
|
||||
match -- Overrides MatcherBase.match.
|
||||
"""
|
||||
|
||||
def match(self, xml):
|
||||
"""
|
||||
Match a stanza against multiple criteria. The match is successful
|
||||
if one of the criteria matches.
|
||||
|
||||
Each of the criteria must implement a match() method.
|
||||
|
||||
Overrides MatcherBase.match.
|
||||
|
||||
Arguments:
|
||||
xml -- The stanza object to compare against.
|
||||
"""
|
||||
for m in self._criteria:
|
||||
if m.match(xml):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -3,12 +3,36 @@
|
||||
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 . import base
|
||||
from xml.etree import cElementTree
|
||||
|
||||
class StanzaPath(base.MatcherBase):
|
||||
from sleekxmpp.xmlstream.matcher.base import MatcherBase
|
||||
|
||||
def match(self, stanza):
|
||||
return stanza.match(self._criteria)
|
||||
|
||||
class StanzaPath(MatcherBase):
|
||||
|
||||
"""
|
||||
The StanzaPath matcher selects stanzas that match a given "stanza path",
|
||||
which is similar to a normal XPath except that it uses the interfaces and
|
||||
plugins of the stanza instead of the actual, underlying XML.
|
||||
|
||||
In most cases, the stanza path and XPath should be identical, but be
|
||||
aware that differences may occur.
|
||||
|
||||
Methods:
|
||||
match -- Overrides MatcherBase.match.
|
||||
"""
|
||||
|
||||
def match(self, stanza):
|
||||
"""
|
||||
Compare a stanza against a "stanza path". A stanza path is similar to
|
||||
an XPath expression, but uses the stanza's interfaces and plugins
|
||||
instead of the underlying XML. For most cases, the stanza path and
|
||||
XPath should be identical, but be aware that differences may occur.
|
||||
|
||||
Overrides MatcherBase.match.
|
||||
|
||||
Arguments:
|
||||
stanza -- The stanza object to compare against.
|
||||
"""
|
||||
return stanza.match(self._criteria)
|
||||
|
||||
@@ -3,65 +3,157 @@
|
||||
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 . import base
|
||||
from xml.etree import cElementTree
|
||||
|
||||
import logging
|
||||
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
ignore_ns = False
|
||||
from sleekxmpp.xmlstream.stanzabase import ET
|
||||
from sleekxmpp.xmlstream.matcher.base import MatcherBase
|
||||
|
||||
class MatchXMLMask(base.MatcherBase):
|
||||
|
||||
def __init__(self, criteria):
|
||||
base.MatcherBase.__init__(self, criteria)
|
||||
if type(criteria) == type(''):
|
||||
self._criteria = cElementTree.fromstring(self._criteria)
|
||||
self.default_ns = 'jabber:client'
|
||||
|
||||
def setDefaultNS(self, ns):
|
||||
self.default_ns = ns
|
||||
# Flag indicating if the builtin XPath matcher should be used, which
|
||||
# uses namespaces, or a custom matcher that ignores namespaces.
|
||||
# Changing this will affect ALL XMLMask matchers.
|
||||
IGNORE_NS = False
|
||||
|
||||
def match(self, xml):
|
||||
if hasattr(xml, 'xml'):
|
||||
xml = xml.xml
|
||||
return self.maskcmp(xml, self._criteria, True)
|
||||
|
||||
def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'):
|
||||
"""maskcmp(xmlobj, maskobj):
|
||||
Compare etree xml object to etree xml object mask"""
|
||||
use_ns = not ignore_ns
|
||||
#TODO require namespaces
|
||||
if source == None: #if element not found (happens during recursive check below)
|
||||
return False
|
||||
if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
|
||||
try:
|
||||
maskobj = cElementTree.fromstring(maskobj)
|
||||
except ExpatError:
|
||||
logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj))
|
||||
if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare
|
||||
return False
|
||||
if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ):
|
||||
return False
|
||||
if maskobj.text and source.text != maskobj.text:
|
||||
return False
|
||||
for attr_name in maskobj.attrib: #compare attributes
|
||||
if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]:
|
||||
return False
|
||||
#for subelement in maskobj.getiterator()[1:]: #recursively compare subelements
|
||||
for subelement in maskobj: #recursively compare subelements
|
||||
if use_ns:
|
||||
if not self.maskcmp(source.find(subelement.tag), subelement, use_ns):
|
||||
return False
|
||||
else:
|
||||
if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns):
|
||||
return False
|
||||
return True
|
||||
|
||||
def getChildIgnoreNS(self, xml, tag):
|
||||
tag = tag.split('}')[-1]
|
||||
try:
|
||||
idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag)
|
||||
except ValueError:
|
||||
return None
|
||||
return xml.getchildren()[idx]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MatchXMLMask(MatcherBase):
|
||||
|
||||
"""
|
||||
The XMLMask matcher selects stanzas whose XML matches a given
|
||||
XML pattern, or mask. For example, message stanzas with body elements
|
||||
could be matched using the mask:
|
||||
|
||||
<message xmlns="jabber:client"><body /></message>
|
||||
|
||||
Use of XMLMask is discouraged, and XPath or StanzaPath should be used
|
||||
instead.
|
||||
|
||||
The use of namespaces in the mask comparison is controlled by
|
||||
IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
|
||||
for ALL XMLMask matchers.
|
||||
|
||||
Methods:
|
||||
match -- Overrides MatcherBase.match.
|
||||
setDefaultNS -- Set the default namespace for the mask.
|
||||
"""
|
||||
|
||||
def __init__(self, criteria):
|
||||
"""
|
||||
Create a new XMLMask matcher.
|
||||
|
||||
Arguments:
|
||||
criteria -- Either an XML object or XML string to use as a mask.
|
||||
"""
|
||||
MatcherBase.__init__(self, criteria)
|
||||
if isinstance(criteria, str):
|
||||
self._criteria = ET.fromstring(self._criteria)
|
||||
self.default_ns = 'jabber:client'
|
||||
|
||||
def setDefaultNS(self, ns):
|
||||
"""
|
||||
Set the default namespace to use during comparisons.
|
||||
|
||||
Arguments:
|
||||
ns -- The new namespace to use as the default.
|
||||
"""
|
||||
self.default_ns = ns
|
||||
|
||||
def match(self, xml):
|
||||
"""
|
||||
Compare a stanza object or XML object against the stored XML mask.
|
||||
|
||||
Overrides MatcherBase.match.
|
||||
|
||||
Arguments:
|
||||
xml -- The stanza object or XML object to compare against.
|
||||
"""
|
||||
if hasattr(xml, 'xml'):
|
||||
xml = xml.xml
|
||||
return self._mask_cmp(xml, self._criteria, True)
|
||||
|
||||
def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
|
||||
"""
|
||||
Compare an XML object against an XML mask.
|
||||
|
||||
Arguments:
|
||||
source -- The XML object to compare against the mask.
|
||||
mask -- The XML object serving as the mask.
|
||||
use_ns -- Indicates if namespaces should be respected during
|
||||
the comparison.
|
||||
default_ns -- The default namespace to apply to elements that
|
||||
do not have a specified namespace.
|
||||
Defaults to "__no_ns__".
|
||||
"""
|
||||
use_ns = not IGNORE_NS
|
||||
|
||||
if source is None:
|
||||
# If the element was not found. May happend during recursive calls.
|
||||
return False
|
||||
|
||||
# Convert the mask to an XML object if it is a string.
|
||||
if not hasattr(mask, 'attrib'):
|
||||
try:
|
||||
mask = ET.fromstring(mask)
|
||||
except ExpatError:
|
||||
log.warning("Expat error: %s\nIn parsing: %s" % ('', mask))
|
||||
|
||||
if not use_ns:
|
||||
# Compare the element without using namespaces.
|
||||
source_tag = source.tag.split('}', 1)[-1]
|
||||
mask_tag = mask.tag.split('}', 1)[-1]
|
||||
if source_tag != mask_tag:
|
||||
return False
|
||||
else:
|
||||
# Compare the element using namespaces
|
||||
mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
|
||||
if source.tag not in [mask.tag, mask_ns_tag]:
|
||||
return False
|
||||
|
||||
# If the mask includes text, compare it.
|
||||
if mask.text and source.text != mask.text:
|
||||
return False
|
||||
|
||||
# Compare attributes. The stanza must include the attributes
|
||||
# defined by the mask, but may include others.
|
||||
for name, value in mask.attrib.items():
|
||||
if source.attrib.get(name, "__None__") != value:
|
||||
return False
|
||||
|
||||
# Recursively check subelements.
|
||||
for subelement in mask:
|
||||
if use_ns:
|
||||
if not self._mask_cmp(source.find(subelement.tag),
|
||||
subelement, use_ns):
|
||||
return False
|
||||
else:
|
||||
if not self._mask_cmp(self._get_child(source, subelement.tag),
|
||||
subelement, use_ns):
|
||||
return False
|
||||
|
||||
# Everything matches.
|
||||
return True
|
||||
|
||||
def _get_child(self, xml, tag):
|
||||
"""
|
||||
Return a child element given its tag, ignoring namespace values.
|
||||
|
||||
Returns None if the child was not found.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML object to search for the given child tag.
|
||||
tag -- The name of the subelement to find.
|
||||
"""
|
||||
tag = tag.split('}')[-1]
|
||||
try:
|
||||
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
|
||||
index = children.index(tag)
|
||||
except ValueError:
|
||||
return None
|
||||
return xml.getchildren()[index]
|
||||
|
||||
@@ -3,32 +3,77 @@
|
||||
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 . import base
|
||||
from xml.etree import cElementTree
|
||||
|
||||
ignore_ns = False
|
||||
from sleekxmpp.xmlstream.stanzabase import ET
|
||||
from sleekxmpp.xmlstream.matcher.base import MatcherBase
|
||||
|
||||
class MatchXPath(base.MatcherBase):
|
||||
|
||||
def match(self, xml):
|
||||
if hasattr(xml, 'xml'):
|
||||
xml = xml.xml
|
||||
x = cElementTree.Element('x')
|
||||
x.append(xml)
|
||||
if not ignore_ns:
|
||||
if x.find(self._criteria) is not None:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
criteria = [c.split('}')[-1] for c in self._criteria.split('/')]
|
||||
xml = x
|
||||
for tag in criteria:
|
||||
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
|
||||
try:
|
||||
idx = children.index(tag)
|
||||
except ValueError:
|
||||
return False
|
||||
xml = xml.getchildren()[idx]
|
||||
return True
|
||||
# Flag indicating if the builtin XPath matcher should be used, which
|
||||
# uses namespaces, or a custom matcher that ignores namespaces.
|
||||
# Changing this will affect ALL XPath matchers.
|
||||
IGNORE_NS = False
|
||||
|
||||
|
||||
class MatchXPath(MatcherBase):
|
||||
|
||||
"""
|
||||
The XPath matcher selects stanzas whose XML contents matches a given
|
||||
XPath expression.
|
||||
|
||||
Note that using this matcher may not produce expected behavior when using
|
||||
attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
|
||||
does not support the use of attribute selectors. If you need to support
|
||||
Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
|
||||
|
||||
If the value of IGNORE_NS is set to true, then XPath expressions will
|
||||
be matched without using namespaces.
|
||||
|
||||
Methods:
|
||||
match -- Overrides MatcherBase.match.
|
||||
"""
|
||||
|
||||
def match(self, xml):
|
||||
"""
|
||||
Compare a stanza's XML contents to an XPath expression.
|
||||
|
||||
If the value of IGNORE_NS is set to true, then XPath expressions
|
||||
will be matched without using namespaces.
|
||||
|
||||
Note that in Python 2.6 and 3.1 the ElementTree find method does
|
||||
not support attribute selectors in the XPath expression.
|
||||
|
||||
Arguments:
|
||||
xml -- The stanza object to compare against.
|
||||
"""
|
||||
if hasattr(xml, 'xml'):
|
||||
xml = xml.xml
|
||||
x = ET.Element('x')
|
||||
x.append(xml)
|
||||
|
||||
if not IGNORE_NS:
|
||||
# Use builtin, namespace respecting, XPath matcher.
|
||||
if x.find(self._criteria) is not None:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
# Remove namespaces from the XPath expression.
|
||||
criteria = []
|
||||
for ns_block in self._criteria.split('{'):
|
||||
criteria.extend(ns_block.split('}')[-1].split('/'))
|
||||
|
||||
# Walk the XPath expression.
|
||||
xml = x
|
||||
for tag in criteria:
|
||||
if not tag:
|
||||
# Skip empty tag name artifacts from the cleanup phase.
|
||||
continue
|
||||
|
||||
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
|
||||
try:
|
||||
index = children.index(tag)
|
||||
except ValueError:
|
||||
return False
|
||||
xml = xml.getchildren()[index]
|
||||
return True
|
||||
|
||||
@@ -1,87 +1,205 @@
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
"""
|
||||
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 threading
|
||||
import logging
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Task(object):
|
||||
"""Task object for the Scheduler class"""
|
||||
def __init__(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None):
|
||||
self.name = name
|
||||
self.seconds = seconds
|
||||
self.callback = callback
|
||||
self.args = args or tuple()
|
||||
self.kwargs = kwargs or {}
|
||||
self.repeat = repeat
|
||||
self.next = time.time() + self.seconds
|
||||
self.qpointer = qpointer
|
||||
|
||||
def run(self):
|
||||
if self.qpointer is not None:
|
||||
self.qpointer.put(('schedule', self.callback, self.args))
|
||||
else:
|
||||
self.callback(*self.args, **self.kwargs)
|
||||
self.reset()
|
||||
return self.repeat
|
||||
|
||||
def reset(self):
|
||||
self.next = time.time() + self.seconds
|
||||
|
||||
"""
|
||||
A scheduled task that will be executed by the scheduler
|
||||
after a given time interval has passed.
|
||||
|
||||
Attributes:
|
||||
name -- The name of the task.
|
||||
seconds -- The number of seconds to wait before executing.
|
||||
callback -- The function to execute.
|
||||
args -- The arguments to pass to the callback.
|
||||
kwargs -- The keyword arguments to pass to the callback.
|
||||
repeat -- Indicates if the task should repeat.
|
||||
Defaults to False.
|
||||
qpointer -- A pointer to an event queue for queuing callback
|
||||
execution instead of executing immediately.
|
||||
|
||||
Methods:
|
||||
run -- Either queue or execute the callback.
|
||||
reset -- Reset the task's timer.
|
||||
"""
|
||||
|
||||
def __init__(self, name, seconds, callback, args=None,
|
||||
kwargs=None, repeat=False, qpointer=None):
|
||||
"""
|
||||
Create a new task.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the task.
|
||||
seconds -- The number of seconds to wait before executing.
|
||||
callback -- The function to execute.
|
||||
args -- The arguments to pass to the callback.
|
||||
kwargs -- The keyword arguments to pass to the callback.
|
||||
repeat -- Indicates if the task should repeat.
|
||||
Defaults to False.
|
||||
qpointer -- A pointer to an event queue for queuing callback
|
||||
execution instead of executing immediately.
|
||||
"""
|
||||
self.name = name
|
||||
self.seconds = seconds
|
||||
self.callback = callback
|
||||
self.args = args or tuple()
|
||||
self.kwargs = kwargs or {}
|
||||
self.repeat = repeat
|
||||
self.next = time.time() + self.seconds
|
||||
self.qpointer = qpointer
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Execute the task's callback.
|
||||
|
||||
If an event queue was supplied, place the callback in the queue;
|
||||
otherwise, execute the callback immediately.
|
||||
"""
|
||||
if self.qpointer is not None:
|
||||
self.qpointer.put(('schedule', self.callback, self.args))
|
||||
else:
|
||||
self.callback(*self.args, **self.kwargs)
|
||||
self.reset()
|
||||
return self.repeat
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset the task's timer so that it will repeat.
|
||||
"""
|
||||
self.next = time.time() + self.seconds
|
||||
|
||||
|
||||
class Scheduler(object):
|
||||
"""Threaded scheduler that allows for updates mid-execution unlike http://docs.python.org/library/sched.html#module-sched"""
|
||||
def __init__(self, parentqueue=None):
|
||||
self.addq = queue.Queue()
|
||||
self.schedule = []
|
||||
self.thread = None
|
||||
self.run = False
|
||||
self.parentqueue = parentqueue
|
||||
|
||||
def process(self, threaded=True):
|
||||
if threaded:
|
||||
self.thread = threading.Thread(name='shedulerprocess', target=self._process)
|
||||
self.thread.start()
|
||||
else:
|
||||
self._process()
|
||||
|
||||
def _process(self):
|
||||
self.run = True
|
||||
while self.run:
|
||||
try:
|
||||
wait = 1
|
||||
updated = False
|
||||
if self.schedule:
|
||||
wait = self.schedule[0].next - time.time()
|
||||
try:
|
||||
if wait <= 0.0:
|
||||
newtask = self.addq.get(False)
|
||||
else:
|
||||
newtask = self.addq.get(True, wait)
|
||||
except queue.Empty:
|
||||
cleanup = []
|
||||
for task in self.schedule:
|
||||
if time.time() >= task.next:
|
||||
updated = True
|
||||
if not task.run():
|
||||
cleanup.append(task)
|
||||
else:
|
||||
break
|
||||
for task in cleanup:
|
||||
x = self.schedule.pop(self.schedule.index(task))
|
||||
else:
|
||||
updated = True
|
||||
self.schedule.append(newtask)
|
||||
finally:
|
||||
if updated: self.schedule = sorted(self.schedule, key=lambda task: task.next)
|
||||
except KeyboardInterrupt:
|
||||
self.run = False
|
||||
logging.debug("Quitting Scheduler thread")
|
||||
if self.parentqueue is not None:
|
||||
self.parentqueue.put(('quit', None, None))
|
||||
"""
|
||||
A threaded scheduler that allows for updates mid-execution unlike the
|
||||
scheduler in the standard library.
|
||||
|
||||
def add(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None):
|
||||
self.addq.put(Task(name, seconds, callback, args, kwargs, repeat, qpointer))
|
||||
|
||||
def quit(self):
|
||||
self.run = False
|
||||
http://docs.python.org/library/sched.html#module-sched
|
||||
|
||||
Attributes:
|
||||
addq -- A queue storing added tasks.
|
||||
schedule -- A list of tasks in order of execution times.
|
||||
thread -- If threaded, the thread processing the schedule.
|
||||
run -- Indicates if the scheduler is running.
|
||||
parentqueue -- A parent event queue in control of this scheduler.
|
||||
|
||||
Methods:
|
||||
add -- Add a new task to the schedule.
|
||||
process -- Process and schedule tasks.
|
||||
quit -- Stop the scheduler.
|
||||
"""
|
||||
|
||||
def __init__(self, parentqueue=None, parentstop=None):
|
||||
"""
|
||||
Create a new scheduler.
|
||||
|
||||
Arguments:
|
||||
parentqueue -- A separate event queue controlling this scheduler.
|
||||
"""
|
||||
self.addq = queue.Queue()
|
||||
self.schedule = []
|
||||
self.thread = None
|
||||
self.run = False
|
||||
self.parentqueue = parentqueue
|
||||
self.parentstop = parentstop
|
||||
|
||||
def process(self, threaded=True):
|
||||
"""
|
||||
Begin accepting and processing scheduled tasks.
|
||||
|
||||
Arguments:
|
||||
threaded -- Indicates if the scheduler should execute in its own
|
||||
thread. Defaults to True.
|
||||
"""
|
||||
if threaded:
|
||||
self.thread = threading.Thread(name='sheduler_process',
|
||||
target=self._process)
|
||||
self.thread.start()
|
||||
else:
|
||||
self._process()
|
||||
|
||||
def _process(self):
|
||||
"""Process scheduled tasks."""
|
||||
self.run = True
|
||||
try:
|
||||
while self.run and (self.parentstop is None or not self.parentstop.isSet()):
|
||||
wait = 1
|
||||
updated = False
|
||||
if self.schedule:
|
||||
wait = self.schedule[0].next - time.time()
|
||||
try:
|
||||
if wait <= 0.0:
|
||||
newtask = self.addq.get(False)
|
||||
else:
|
||||
newtask = self.addq.get(True, wait)
|
||||
except queue.Empty:
|
||||
cleanup = []
|
||||
for task in self.schedule:
|
||||
if time.time() >= task.next:
|
||||
updated = True
|
||||
if not task.run():
|
||||
cleanup.append(task)
|
||||
else:
|
||||
break
|
||||
for task in cleanup:
|
||||
x = self.schedule.pop(self.schedule.index(task))
|
||||
else:
|
||||
updated = True
|
||||
self.schedule.append(newtask)
|
||||
finally:
|
||||
if updated:
|
||||
self.schedule = sorted(self.schedule,
|
||||
key=lambda task: task.next)
|
||||
except KeyboardInterrupt:
|
||||
self.run = False
|
||||
if self.parentstop is not None:
|
||||
log.debug("stopping parent")
|
||||
self.parentstop.set()
|
||||
except SystemExit:
|
||||
self.run = False
|
||||
if self.parentstop is not None:
|
||||
self.parentstop.set()
|
||||
log.debug("Quitting Scheduler thread")
|
||||
if self.parentqueue is not None:
|
||||
self.parentqueue.put(('quit', None, None))
|
||||
|
||||
def add(self, name, seconds, callback, args=None,
|
||||
kwargs=None, repeat=False, qpointer=None):
|
||||
"""
|
||||
Schedule a new task.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the task.
|
||||
seconds -- The number of seconds to wait before executing.
|
||||
callback -- The function to execute.
|
||||
args -- The arguments to pass to the callback.
|
||||
kwargs -- The keyword arguments to pass to the callback.
|
||||
repeat -- Indicates if the task should repeat.
|
||||
Defaults to False.
|
||||
qpointer -- A pointer to an event queue for queuing callback
|
||||
execution instead of executing immediately.
|
||||
"""
|
||||
self.addq.put(Task(name, seconds, callback, args,
|
||||
kwargs, repeat, qpointer))
|
||||
|
||||
def quit(self):
|
||||
"""Shutdown the scheduler."""
|
||||
self.run = False
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,234 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
||||
class StateMachine(object):
|
||||
|
||||
def __init__(self, states=[]):
|
||||
self.lock = threading.Condition(threading.RLock())
|
||||
self.__states= []
|
||||
self.addStates(states)
|
||||
self.__default_state = self.__states[0]
|
||||
self.__current_state = self.__default_state
|
||||
|
||||
def addStates(self, states):
|
||||
with self.lock:
|
||||
for state in states:
|
||||
if state in self.__states:
|
||||
raise IndexError("The state '%s' is already in the StateMachine." % state)
|
||||
self.__states.append( state )
|
||||
|
||||
|
||||
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 )
|
||||
|
||||
with self.lock:
|
||||
start = time.time()
|
||||
while not self.__current_state in from_states:
|
||||
# detect timeout:
|
||||
if time.time() >= start + wait: return False
|
||||
self.lock.wait(wait)
|
||||
|
||||
if self.__current_state in from_states: # should always be True due to lock
|
||||
|
||||
return_val = True
|
||||
# Note that func might throw an exception, but that's OK, it aborts the transition
|
||||
if func is not None: return_val = func(*args,**kwargs)
|
||||
|
||||
# some 'false' value returned from func,
|
||||
# indicating that transition should not occur:
|
||||
if not return_val: return return_val
|
||||
|
||||
logging.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state)
|
||||
self.__current_state = to_state
|
||||
self.lock.notify_all()
|
||||
return return_val # some 'true' value returned by func or True if func was None
|
||||
else:
|
||||
logging.error( "StateMachine bug!! The lock should ensure this doesn't happen!" )
|
||||
return False
|
||||
|
||||
|
||||
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):
|
||||
'''
|
||||
Ensure the state machine is currently in `state`, or wait until it enters `state`.
|
||||
'''
|
||||
return self.ensure_any( (state,), wait=wait )
|
||||
|
||||
|
||||
def ensure_any(self, states, wait=0.0):
|
||||
'''
|
||||
Ensure we are currently in one of the given `states`
|
||||
'''
|
||||
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 )
|
||||
|
||||
with self.lock:
|
||||
start = time.time()
|
||||
while not self.__current_state in states:
|
||||
# detect timeout:
|
||||
if time.time() >= start + wait: return False
|
||||
self.lock.wait(wait)
|
||||
return self.__current_state in states # should always be True due to lock
|
||||
|
||||
|
||||
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._timeout = False
|
||||
|
||||
def __enter__(self):
|
||||
self.state_machine.lock.acquire()
|
||||
start = time.time()
|
||||
while not self.state_machine[ self.from_state ]:
|
||||
# detect timeout:
|
||||
if time.time() >= start + self.wait:
|
||||
logging.debug('StateMachine timeout while waiting for state: %s', self.from_state )
|
||||
self._timeout = True # to indicate we should not transition
|
||||
return False
|
||||
self.state_machine.lock.wait(self.wait)
|
||||
|
||||
logging.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:
|
||||
logging.exception( "StateMachine exception in context, remaining in state: %s\n%s:%s",
|
||||
self.state_machine.current_state(), exc_type.__name__, exc_val )
|
||||
elif not self._timeout:
|
||||
logging.debug(' ==== TRANSITION %s -> %s',
|
||||
self.state_machine.current_state(), self.to_state)
|
||||
self.state_machine._set_state( self.to_state )
|
||||
|
||||
self.state_machine.lock.notify_all()
|
||||
self.state_machine.lock.release()
|
||||
return False # re-raise any exception
|
||||
|
||||
@@ -1,60 +1,19 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
class ToString(object):
|
||||
def __str__(self, xml=None, xmlns='', stringbuffer=''):
|
||||
if xml is None:
|
||||
xml = self.xml
|
||||
newoutput = [stringbuffer]
|
||||
#TODO respect ET mapped namespaces
|
||||
itag = xml.tag.split('}', 1)[-1]
|
||||
if '}' in xml.tag:
|
||||
ixmlns = xml.tag.split('}', 1)[0][1:]
|
||||
else:
|
||||
ixmlns = ''
|
||||
nsbuffer = ''
|
||||
if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace:
|
||||
if self.stream is not None and ixmlns in self.stream.namespace_map:
|
||||
if self.stream.namespace_map[ixmlns] != '':
|
||||
itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag)
|
||||
else:
|
||||
nsbuffer = """ xmlns="%s\"""" % ixmlns
|
||||
if ixmlns not in ('', xmlns, self.namespace):
|
||||
nsbuffer = """ xmlns="%s\"""" % ixmlns
|
||||
newoutput.append("<%s" % itag)
|
||||
newoutput.append(nsbuffer)
|
||||
for attrib in xml.attrib:
|
||||
if '{' not in attrib:
|
||||
newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
|
||||
if len(xml) or xml.text or xml.tail:
|
||||
newoutput.append(">")
|
||||
if xml.text:
|
||||
newoutput.append(self.xmlesc(xml.text))
|
||||
if len(xml):
|
||||
for child in xml.getchildren():
|
||||
newoutput.append(self.__str__(child, ixmlns))
|
||||
newoutput.append("</%s>" % (itag, ))
|
||||
if xml.tail:
|
||||
newoutput.append(self.xmlesc(xml.tail))
|
||||
elif xml.text:
|
||||
newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
|
||||
else:
|
||||
newoutput.append(" />")
|
||||
return ''.join(newoutput)
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
def xmlesc(self, text):
|
||||
text = list(text)
|
||||
cc = 0
|
||||
matches = ('&', '<', '"', '>', "'")
|
||||
for c in text:
|
||||
if c in matches:
|
||||
if c == '&':
|
||||
text[cc] = '&'
|
||||
elif c == '<':
|
||||
text[cc] = '<'
|
||||
elif c == '>':
|
||||
text[cc] = '>'
|
||||
elif c == "'":
|
||||
text[cc] = '''
|
||||
else:
|
||||
text[cc] = '"'
|
||||
cc += 1
|
||||
return ''.join(text)
|
||||
import sys
|
||||
|
||||
# Import the correct tostring and xml_escape functions based on the Python
|
||||
# version in order to properly handle Unicode.
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape
|
||||
else:
|
||||
from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape
|
||||
|
||||
__all__ = ['tostring', 'xml_escape']
|
||||
|
||||
95
sleekxmpp/xmlstream/tostring/tostring.py
Normal file
95
sleekxmpp/xmlstream/tostring/tostring.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
|
||||
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
||||
"""
|
||||
Serialize an XML object to a Unicode string.
|
||||
|
||||
If namespaces are provided using xmlns or stanza_ns, then elements
|
||||
that use those namespaces will not include the xmlns attribute in
|
||||
the output.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML object to serialize. If the value is None,
|
||||
then the XML object contained in this stanza
|
||||
object will be used.
|
||||
xmlns -- Optional namespace of an element wrapping the XML
|
||||
object.
|
||||
stanza_ns -- The namespace of the stanza object that contains
|
||||
the XML object.
|
||||
stream -- The XML stream that generated the XML object.
|
||||
outbuffer -- Optional buffer for storing serializations during
|
||||
recursive calls.
|
||||
"""
|
||||
# Add previous results to the start of the output.
|
||||
output = [outbuffer]
|
||||
|
||||
# Extract the element's tag name.
|
||||
tag_name = xml.tag.split('}', 1)[-1]
|
||||
|
||||
# Extract the element's namespace if it is defined.
|
||||
if '}' in xml.tag:
|
||||
tag_xmlns = xml.tag.split('}', 1)[0][1:]
|
||||
else:
|
||||
tag_xmlns = ''
|
||||
|
||||
# Output the tag name and derived namespace of the element.
|
||||
namespace = ''
|
||||
if tag_xmlns not in ['', xmlns, stanza_ns]:
|
||||
namespace = ' xmlns="%s"' % tag_xmlns
|
||||
if stream and tag_xmlns in stream.namespace_map:
|
||||
mapped_namespace = stream.namespace_map[tag_xmlns]
|
||||
if mapped_namespace:
|
||||
tag_name = "%s:%s" % (mapped_namespace, tag_name)
|
||||
output.append("<%s" % tag_name)
|
||||
output.append(namespace)
|
||||
|
||||
# Output escaped attribute values.
|
||||
for attrib, value in xml.attrib.items():
|
||||
if '{' not in attrib:
|
||||
value = xml_escape(value)
|
||||
output.append(' %s="%s"' % (attrib, value))
|
||||
|
||||
if len(xml) or xml.text:
|
||||
# If there are additional child elements to serialize.
|
||||
output.append(">")
|
||||
if xml.text:
|
||||
output.append(xml_escape(xml.text))
|
||||
if len(xml):
|
||||
for child in xml.getchildren():
|
||||
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
|
||||
output.append("</%s>" % tag_name)
|
||||
elif xml.text:
|
||||
# If we only have text content.
|
||||
output.append(">%s</%s>" % (xml_escape(xml.text), tag_name))
|
||||
else:
|
||||
# Empty element.
|
||||
output.append(" />")
|
||||
if xml.tail:
|
||||
# If there is additional text after the element.
|
||||
output.append(xml_escape(xml.tail))
|
||||
return ''.join(output)
|
||||
|
||||
|
||||
def xml_escape(text):
|
||||
"""
|
||||
Convert special characters in XML to escape sequences.
|
||||
|
||||
Arguments:
|
||||
text -- The XML text to convert.
|
||||
"""
|
||||
text = list(text)
|
||||
escapes = {'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"'}
|
||||
for i, c in enumerate(text):
|
||||
text[i] = escapes.get(c, c)
|
||||
return ''.join(text)
|
||||
101
sleekxmpp/xmlstream/tostring/tostring26.py
Normal file
101
sleekxmpp/xmlstream/tostring/tostring26.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
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 unicode_literals
|
||||
import types
|
||||
|
||||
|
||||
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
||||
"""
|
||||
Serialize an XML object to a Unicode string.
|
||||
|
||||
If namespaces are provided using xmlns or stanza_ns, then elements
|
||||
that use those namespaces will not include the xmlns attribute in
|
||||
the output.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML object to serialize. If the value is None,
|
||||
then the XML object contained in this stanza
|
||||
object will be used.
|
||||
xmlns -- Optional namespace of an element wrapping the XML
|
||||
object.
|
||||
stanza_ns -- The namespace of the stanza object that contains
|
||||
the XML object.
|
||||
stream -- The XML stream that generated the XML object.
|
||||
outbuffer -- Optional buffer for storing serializations during
|
||||
recursive calls.
|
||||
"""
|
||||
# Add previous results to the start of the output.
|
||||
output = [outbuffer]
|
||||
|
||||
# Extract the element's tag name.
|
||||
tag_name = xml.tag.split('}', 1)[-1]
|
||||
|
||||
# Extract the element's namespace if it is defined.
|
||||
if '}' in xml.tag:
|
||||
tag_xmlns = xml.tag.split('}', 1)[0][1:]
|
||||
else:
|
||||
tag_xmlns = u''
|
||||
|
||||
# Output the tag name and derived namespace of the element.
|
||||
namespace = u''
|
||||
if tag_xmlns not in ['', xmlns, stanza_ns]:
|
||||
namespace = u' xmlns="%s"' % tag_xmlns
|
||||
if stream and tag_xmlns in stream.namespace_map:
|
||||
mapped_namespace = stream.namespace_map[tag_xmlns]
|
||||
if mapped_namespace:
|
||||
tag_name = u"%s:%s" % (mapped_namespace, tag_name)
|
||||
output.append(u"<%s" % tag_name)
|
||||
output.append(namespace)
|
||||
|
||||
# Output escaped attribute values.
|
||||
for attrib, value in xml.attrib.items():
|
||||
if '{' not in attrib:
|
||||
value = xml_escape(value)
|
||||
output.append(u' %s="%s"' % (attrib, value))
|
||||
|
||||
if len(xml) or xml.text:
|
||||
# If there are additional child elements to serialize.
|
||||
output.append(u">")
|
||||
if xml.text:
|
||||
output.append(xml_escape(xml.text))
|
||||
if len(xml):
|
||||
for child in xml.getchildren():
|
||||
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
|
||||
output.append(u"</%s>" % tag_name)
|
||||
elif xml.text:
|
||||
# If we only have text content.
|
||||
output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name))
|
||||
else:
|
||||
# Empty element.
|
||||
output.append(u" />")
|
||||
if xml.tail:
|
||||
# If there is additional text after the element.
|
||||
output.append(xml_escape(xml.tail))
|
||||
return u''.join(output)
|
||||
|
||||
|
||||
def xml_escape(text):
|
||||
"""
|
||||
Convert special characters in XML to escape sequences.
|
||||
|
||||
Arguments:
|
||||
text -- The XML text to convert.
|
||||
"""
|
||||
if type(text) != types.UnicodeType:
|
||||
text = list(unicode(text, 'utf-8', 'ignore'))
|
||||
else:
|
||||
text = list(text)
|
||||
escapes = {u'&': u'&',
|
||||
u'<': u'<',
|
||||
u'>': u'>',
|
||||
u"'": u''',
|
||||
u'"': u'"'}
|
||||
for i, c in enumerate(text):
|
||||
text[i] = escapes.get(c, c)
|
||||
return u''.join(text)
|
||||
@@ -1,65 +0,0 @@
|
||||
import types
|
||||
|
||||
class ToString(object):
|
||||
def __str__(self, xml=None, xmlns='', stringbuffer=''):
|
||||
if xml is None:
|
||||
xml = self.xml
|
||||
newoutput = [stringbuffer]
|
||||
#TODO respect ET mapped namespaces
|
||||
itag = xml.tag.split('}', 1)[-1]
|
||||
if '}' in xml.tag:
|
||||
ixmlns = xml.tag.split('}', 1)[0][1:]
|
||||
else:
|
||||
ixmlns = ''
|
||||
nsbuffer = ''
|
||||
if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace:
|
||||
if self.stream is not None and ixmlns in self.stream.namespace_map:
|
||||
if self.stream.namespace_map[ixmlns] != u'':
|
||||
itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag)
|
||||
else:
|
||||
nsbuffer = """ xmlns="%s\"""" % ixmlns
|
||||
if ixmlns not in ('', xmlns, self.namespace):
|
||||
nsbuffer = """ xmlns="%s\"""" % ixmlns
|
||||
newoutput.append("<%s" % itag)
|
||||
newoutput.append(nsbuffer)
|
||||
for attrib in xml.attrib:
|
||||
if '{' not in attrib:
|
||||
newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
|
||||
if len(xml) or xml.text or xml.tail:
|
||||
newoutput.append(u">")
|
||||
if xml.text:
|
||||
newoutput.append(self.xmlesc(xml.text))
|
||||
if len(xml):
|
||||
for child in xml.getchildren():
|
||||
newoutput.append(self.__str__(child, ixmlns))
|
||||
newoutput.append(u"</%s>" % (itag, ))
|
||||
if xml.tail:
|
||||
newoutput.append(self.xmlesc(xml.tail))
|
||||
elif xml.text:
|
||||
newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
|
||||
else:
|
||||
newoutput.append(" />")
|
||||
return u''.join(newoutput)
|
||||
|
||||
def xmlesc(self, text):
|
||||
if type(text) != types.UnicodeType:
|
||||
text = list(unicode(text, 'utf-8', 'ignore'))
|
||||
else:
|
||||
text = list(text)
|
||||
|
||||
cc = 0
|
||||
matches = (u'&', u'<', u'"', u'>', u"'")
|
||||
for c in text:
|
||||
if c in matches:
|
||||
if c == u'&':
|
||||
text[cc] = u'&'
|
||||
elif c == u'<':
|
||||
text[cc] = u'<'
|
||||
elif c == u'>':
|
||||
text[cc] = u'>'
|
||||
elif c == u"'":
|
||||
text[cc] = u'''
|
||||
else:
|
||||
text[cc] = u'"'
|
||||
cc += 1
|
||||
return ''.join(text)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python2.6
|
||||
#!/usr/bin/env python
|
||||
import unittest
|
||||
import logging
|
||||
import sys
|
||||
@@ -13,7 +13,7 @@ class testoverall(unittest.TestCase):
|
||||
if sys.version_info < (3,0):
|
||||
self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn'), quiet=True))
|
||||
else:
|
||||
self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn|.*26\Z'), quiet=True))
|
||||
self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn|.*26.*'), quiet=True))
|
||||
|
||||
def testTabNanny(self):
|
||||
"""Invoking the tabnanny"""
|
||||
@@ -21,7 +21,7 @@ class testoverall(unittest.TestCase):
|
||||
self.failIf(tabnanny.check("." + os.sep + 'sleekxmpp'))
|
||||
#raise "Help!"
|
||||
|
||||
def testMethodLength(self):
|
||||
def disabled_testMethodLength(self):
|
||||
"""Testing for excessive method lengths"""
|
||||
import re
|
||||
dirs = os.walk(sys.path[0] + os.sep + 'sleekxmpp')
|
||||
|
||||
110
tests/live_test.py
Normal file
110
tests/live_test.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0033 as xep_0033
|
||||
|
||||
|
||||
class TestLiveStream(SleekTest):
|
||||
"""
|
||||
Test that we can test a live stanza stream.
|
||||
"""
|
||||
|
||||
def tearDown(self):
|
||||
self.stream_close()
|
||||
|
||||
def testClientConnection(self):
|
||||
"""Test that we can interact with a live ClientXMPP instance."""
|
||||
self.stream_start(mode='client',
|
||||
socket='live',
|
||||
skip=False,
|
||||
jid='user@localhost/test',
|
||||
password='user')
|
||||
|
||||
# Use sid=None to ignore any id sent by the server since
|
||||
# we can't know it in advance.
|
||||
self.recv_header(sfrom='localhost', sid=None)
|
||||
self.send_header(sto='localhost')
|
||||
self.recv_feature("""
|
||||
<stream:features>
|
||||
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
|
||||
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
||||
<mechanism>DIGEST-MD5</mechanism>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
</mechanisms>
|
||||
<c xmlns="http://jabber.org/protocol/caps"
|
||||
node="http://www.process-one.net/en/ejabberd/"
|
||||
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU=" hash="sha-1" />
|
||||
<register xmlns="http://jabber.org/features/iq-register" />
|
||||
</stream:features>
|
||||
""")
|
||||
self.send_feature("""
|
||||
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
|
||||
""")
|
||||
self.recv_feature("""
|
||||
<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
|
||||
""")
|
||||
self.send_header(sto='localhost')
|
||||
self.recv_header(sfrom='localhost', sid=None)
|
||||
self.recv_feature("""
|
||||
<stream:features>
|
||||
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
||||
<mechanism>DIGEST-MD5</mechanism>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
</mechanisms>
|
||||
<c xmlns="http://jabber.org/protocol/caps"
|
||||
node="http://www.process-one.net/en/ejabberd/"
|
||||
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
|
||||
hash="sha-1" />
|
||||
<register xmlns="http://jabber.org/features/iq-register" />
|
||||
</stream:features>
|
||||
""")
|
||||
self.send_feature("""
|
||||
<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
mechanism="PLAIN">AHVzZXIAdXNlcg==</auth>
|
||||
""")
|
||||
self.recv_feature("""
|
||||
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />
|
||||
""")
|
||||
self.send_header(sto='localhost')
|
||||
self.recv_header(sfrom='localhost', sid=None)
|
||||
self.recv_feature("""
|
||||
<stream:features>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
|
||||
<session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
|
||||
<c xmlns="http://jabber.org/protocol/caps"
|
||||
node="http://www.process-one.net/en/ejabberd/"
|
||||
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
|
||||
hash="sha-1" />
|
||||
<register xmlns="http://jabber.org/features/iq-register" />
|
||||
</stream:features>
|
||||
""")
|
||||
|
||||
# Should really use send, but our Iq stanza objects
|
||||
# can't handle bind element payloads yet.
|
||||
self.send_feature("""
|
||||
<iq type="set" id="1">
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<resource>test</resource>
|
||||
</bind>
|
||||
</iq>
|
||||
""")
|
||||
self.recv_feature("""
|
||||
<iq type="result" id="1">
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<jid>user@localhost/test</jid>
|
||||
</bind>
|
||||
</iq>
|
||||
""")
|
||||
self.stream_close()
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
|
||||
|
||||
if __name__ == '__main__':
|
||||
tests = unittest.TestSuite([suite])
|
||||
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
||||
test_ns = 'http://andyet.net/protocol/tests'
|
||||
print("<tests xmlns='%s' %s %s %s %s />" % (
|
||||
test_ns,
|
||||
'ran="%s"' % result.testsRun,
|
||||
'errors="%s"' % len(result.errors),
|
||||
'fails="%s"' % len(result.failures),
|
||||
'success="%s"' % result.wasSuccessful()))
|
||||
@@ -1,155 +0,0 @@
|
||||
import unittest
|
||||
from xml.etree import cElementTree as ET
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from . import xmlcompare
|
||||
|
||||
import sleekxmpp.plugins.xep_0030 as sd
|
||||
|
||||
def stanzaPlugin(stanza, plugin):
|
||||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
|
||||
|
||||
class testdisco(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.sd = sd
|
||||
stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo)
|
||||
stanzaPlugin(self.sd.Iq, self.sd.DiscoItems)
|
||||
|
||||
def try3Methods(self, xmlstring, iq):
|
||||
iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring))
|
||||
values = iq2.getValues()
|
||||
iq3 = self.sd.Iq()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match")
|
||||
|
||||
def testCreateInfoQueryNoNode(self):
|
||||
"""Testing disco#info query with no node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = ''
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testCreateInfoQueryWithNode(self):
|
||||
"""Testing disco#info query with a node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testCreateInfoQueryNoNode(self):
|
||||
"""Testing disco#items query with no node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = ''
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testCreateItemsQueryWithNode(self):
|
||||
"""Testing disco#items query with a node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = 'foo'
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testInfoIdentities(self):
|
||||
"""Testing adding identities to disco#info."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><identity category="conference" type="text" name="Chatroom" /></query></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testInfoFeatures(self):
|
||||
"""Testing adding features to disco#info."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
iq['disco_info'].addFeature('foo')
|
||||
iq['disco_info'].addFeature('bar')
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><feature var="foo" /><feature var="bar" /></query></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testItems(self):
|
||||
"""Testing adding features to disco#info."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = 'foo'
|
||||
iq['disco_items'].addItem('user@localhost')
|
||||
iq['disco_items'].addItem('user@localhost', 'foo')
|
||||
iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo"><item jid="user@localhost" /><item node="foo" jid="user@localhost" /><item node="bar" jid="user@localhost" name="Testing" /></query></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testAddRemoveIdentities(self):
|
||||
"""Test adding and removing identities to disco#info stanza"""
|
||||
ids = [('automation', 'commands', 'AdHoc'),
|
||||
('conference', 'text', 'ChatRoom')]
|
||||
|
||||
info = self.sd.DiscoInfo()
|
||||
info.addIdentity(*ids[0])
|
||||
self.failUnless(info.getIdentities() == [ids[0]])
|
||||
|
||||
info.delIdentity('automation', 'commands')
|
||||
self.failUnless(info.getIdentities() == [])
|
||||
|
||||
info.setIdentities(ids)
|
||||
self.failUnless(info.getIdentities() == ids)
|
||||
|
||||
info.delIdentity('automation', 'commands')
|
||||
self.failUnless(info.getIdentities() == [ids[1]])
|
||||
|
||||
info.delIdentities()
|
||||
self.failUnless(info.getIdentities() == [])
|
||||
|
||||
def testAddRemoveFeatures(self):
|
||||
"""Test adding and removing features to disco#info stanza"""
|
||||
features = ['foo', 'bar', 'baz']
|
||||
|
||||
info = self.sd.DiscoInfo()
|
||||
info.addFeature(features[0])
|
||||
self.failUnless(info.getFeatures() == [features[0]])
|
||||
|
||||
info.delFeature('foo')
|
||||
self.failUnless(info.getFeatures() == [])
|
||||
|
||||
info.setFeatures(features)
|
||||
self.failUnless(info.getFeatures() == features)
|
||||
|
||||
info.delFeature('bar')
|
||||
self.failUnless(info.getFeatures() == ['foo', 'baz'])
|
||||
|
||||
info.delFeatures()
|
||||
self.failUnless(info.getFeatures() == [])
|
||||
|
||||
def testAddRemoveItems(self):
|
||||
"""Test adding and removing items to disco#items stanza"""
|
||||
items = [('user@localhost', None, None),
|
||||
('user@localhost', 'foo', None),
|
||||
('user@localhost', 'bar', 'Test')]
|
||||
|
||||
info = self.sd.DiscoItems()
|
||||
self.failUnless(True, ""+str(items[0]))
|
||||
|
||||
info.addItem(*(items[0]))
|
||||
self.failUnless(info.getItems() == [items[0]], info.getItems())
|
||||
|
||||
info.delItem('user@localhost')
|
||||
self.failUnless(info.getItems() == [])
|
||||
|
||||
info.setItems(items)
|
||||
self.failUnless(info.getItems() == items)
|
||||
|
||||
info.delItem('user@localhost', 'foo')
|
||||
self.failUnless(info.getItems() == [items[0], items[2]])
|
||||
|
||||
info.delItems()
|
||||
self.failUnless(info.getItems() == [])
|
||||
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testdisco)
|
||||
@@ -1,35 +1,72 @@
|
||||
import unittest
|
||||
import time
|
||||
from sleekxmpp.test import *
|
||||
|
||||
class testevents(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import sleekxmpp.stanza.presence as p
|
||||
self.p = p
|
||||
|
||||
def testEventHappening(self):
|
||||
"Test handler working"
|
||||
import sleekxmpp
|
||||
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
|
||||
happened = []
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
c.add_event_handler("test_event", handletestevent)
|
||||
c.event("test_event", {})
|
||||
c.event("test_event", {})
|
||||
self.failUnless(happened == [True, True], "event did not get triggered twice")
|
||||
|
||||
def testDelEvent(self):
|
||||
"Test handler working, then deleted and not triggered"
|
||||
import sleekxmpp
|
||||
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
|
||||
happened = []
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
c.add_event_handler("test_event", handletestevent)
|
||||
c.event("test_event", {})
|
||||
c.del_event_handler("test_event", handletestevent)
|
||||
c.event("test_event", {}) # should not trigger because it was deleted
|
||||
self.failUnless(happened == [True], "event did not get triggered the correct number of times")
|
||||
|
||||
class TestEvents(SleekTest):
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testevents)
|
||||
def setUp(self):
|
||||
self.stream_start()
|
||||
|
||||
def tearDown(self):
|
||||
self.stream_close()
|
||||
|
||||
def testEventHappening(self):
|
||||
"""Test handler working"""
|
||||
happened = []
|
||||
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
|
||||
self.xmpp.add_event_handler("test_event", handletestevent)
|
||||
self.xmpp.event("test_event")
|
||||
self.xmpp.event("test_event")
|
||||
|
||||
# Give the event queue time to process.
|
||||
time.sleep(0.1)
|
||||
|
||||
msg = "Event was not triggered the correct number of times: %s"
|
||||
self.failUnless(happened == [True, True], msg)
|
||||
|
||||
def testDelEvent(self):
|
||||
"""Test handler working, then deleted and not triggered"""
|
||||
happened = []
|
||||
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
|
||||
self.xmpp.add_event_handler("test_event", handletestevent)
|
||||
self.xmpp.event("test_event", {})
|
||||
|
||||
self.xmpp.del_event_handler("test_event", handletestevent)
|
||||
|
||||
# Should not trigger because it was deleted
|
||||
self.xmpp.event("test_event", {})
|
||||
|
||||
# Give the event queue time to process.
|
||||
time.sleep(0.1)
|
||||
|
||||
msg = "Event was not triggered the correct number of times: %s"
|
||||
self.failUnless(happened == [True], msg % happened)
|
||||
|
||||
def testDisposableEvent(self):
|
||||
"""Test disposable handler working, then not being triggered again."""
|
||||
happened = []
|
||||
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
|
||||
self.xmpp.add_event_handler("test_event", handletestevent,
|
||||
disposable=True)
|
||||
self.xmpp.event("test_event", {})
|
||||
|
||||
# Should not trigger because it was deleted
|
||||
self.xmpp.event("test_event", {})
|
||||
|
||||
# Give the event queue time to process.
|
||||
time.sleep(0.1)
|
||||
|
||||
msg = "Event was not triggered the correct number of times: %s"
|
||||
self.failUnless(happened == [True], msg % happened)
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents)
|
||||
|
||||
128
tests/test_jid.py
Normal file
128
tests/test_jid.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.xmlstream.jid import JID
|
||||
|
||||
|
||||
class TestJIDClass(SleekTest):
|
||||
|
||||
"""Verify that the JID class can parse and manipulate JIDs."""
|
||||
|
||||
def testJIDFromFull(self):
|
||||
"""Test using JID of the form 'user@server/resource/with/slashes'."""
|
||||
self.check_jid(JID('user@someserver/some/resource'),
|
||||
'user',
|
||||
'someserver',
|
||||
'some/resource',
|
||||
'user@someserver',
|
||||
'user@someserver/some/resource',
|
||||
'user@someserver/some/resource')
|
||||
|
||||
def testJIDchange(self):
|
||||
"""Test changing JID of the form 'user@server/resource/with/slashes'"""
|
||||
j = JID('user1@someserver1/some1/resource1')
|
||||
j.user = 'user'
|
||||
j.domain = 'someserver'
|
||||
j.resource = 'some/resource'
|
||||
self.check_jid(j,
|
||||
'user',
|
||||
'someserver',
|
||||
'some/resource',
|
||||
'user@someserver',
|
||||
'user@someserver/some/resource',
|
||||
'user@someserver/some/resource')
|
||||
|
||||
def testJIDaliases(self):
|
||||
"""Test changing JID using aliases for domain."""
|
||||
j = JID('user@someserver/resource')
|
||||
j.server = 'anotherserver'
|
||||
self.check_jid(j, domain='anotherserver')
|
||||
j.host = 'yetanother'
|
||||
self.check_jid(j, domain='yetanother')
|
||||
|
||||
def testJIDSetFullWithUser(self):
|
||||
"""Test setting the full JID with a user portion."""
|
||||
j = JID('user@domain/resource')
|
||||
j.full = 'otheruser@otherdomain/otherresource'
|
||||
self.check_jid(j,
|
||||
'otheruser',
|
||||
'otherdomain',
|
||||
'otherresource',
|
||||
'otheruser@otherdomain',
|
||||
'otheruser@otherdomain/otherresource',
|
||||
'otheruser@otherdomain/otherresource')
|
||||
|
||||
def testJIDFullNoUserWithResource(self):
|
||||
"""
|
||||
Test setting the full JID without a user
|
||||
portion and with a resource.
|
||||
"""
|
||||
j = JID('user@domain/resource')
|
||||
j.full = 'otherdomain/otherresource'
|
||||
self.check_jid(j,
|
||||
'',
|
||||
'otherdomain',
|
||||
'otherresource',
|
||||
'otherdomain',
|
||||
'otherdomain/otherresource',
|
||||
'otherdomain/otherresource')
|
||||
|
||||
def testJIDFullNoUserNoResource(self):
|
||||
"""
|
||||
Test setting the full JID without a user
|
||||
portion and without a resource.
|
||||
"""
|
||||
j = JID('user@domain/resource')
|
||||
j.full = 'otherdomain'
|
||||
self.check_jid(j,
|
||||
'',
|
||||
'otherdomain',
|
||||
'',
|
||||
'otherdomain',
|
||||
'otherdomain',
|
||||
'otherdomain')
|
||||
|
||||
def testJIDBareUser(self):
|
||||
"""Test setting the bare JID with a user."""
|
||||
j = JID('user@domain/resource')
|
||||
j.bare = 'otheruser@otherdomain'
|
||||
self.check_jid(j,
|
||||
'otheruser',
|
||||
'otherdomain',
|
||||
'resource',
|
||||
'otheruser@otherdomain',
|
||||
'otheruser@otherdomain/resource',
|
||||
'otheruser@otherdomain/resource')
|
||||
|
||||
def testJIDBareNoUser(self):
|
||||
"""Test setting the bare JID without a user."""
|
||||
j = JID('user@domain/resource')
|
||||
j.bare = 'otherdomain'
|
||||
self.check_jid(j,
|
||||
'',
|
||||
'otherdomain',
|
||||
'resource',
|
||||
'otherdomain',
|
||||
'otherdomain/resource',
|
||||
'otherdomain/resource')
|
||||
|
||||
def testJIDNoResource(self):
|
||||
"""Test using JID of the form 'user@domain'."""
|
||||
self.check_jid(JID('user@someserver'),
|
||||
'user',
|
||||
'someserver',
|
||||
'',
|
||||
'user@someserver',
|
||||
'user@someserver',
|
||||
'user@someserver')
|
||||
|
||||
def testJIDNoUser(self):
|
||||
"""Test JID of the form 'component.domain.tld'."""
|
||||
self.check_jid(JID('component.someserver'),
|
||||
'',
|
||||
'component.someserver',
|
||||
'',
|
||||
'component.someserver',
|
||||
'component.someserver',
|
||||
'component.someserver')
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)
|
||||
@@ -1,44 +0,0 @@
|
||||
import unittest
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
class testmessagestanzas(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import sleekxmpp.stanza.message as m
|
||||
from sleekxmpp.basexmpp import stanzaPlugin
|
||||
from sleekxmpp.stanza.htmlim import HTMLIM
|
||||
stanzaPlugin(m.Message, HTMLIM)
|
||||
self.m = m
|
||||
|
||||
def testGroupchatReplyRegression(self):
|
||||
"Regression groupchat reply should be to barejid"
|
||||
msg = self.m.Message()
|
||||
msg['to'] = 'me@myserver.tld'
|
||||
msg['from'] = 'room@someservice.someserver.tld/somenick'
|
||||
msg['type'] = 'groupchat'
|
||||
msg['body'] = "this is a message"
|
||||
msg.reply()
|
||||
self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld')
|
||||
|
||||
def testAttribProperty(self):
|
||||
"Test attrib property returning self"
|
||||
msg = self.m.Message()
|
||||
msg.attrib.attrib.attrib['to'] = 'usr@server.tld'
|
||||
self.failUnless(str(msg['to']) == 'usr@server.tld')
|
||||
|
||||
def testHTMLPlugin(self):
|
||||
"Test message/html/html stanza"
|
||||
msgtxt = """<message to="fritzy@netflint.net/sleekxmpp" type="chat"><body>this is the plaintext message</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>This is the htmlim message</p></body></html></message>"""
|
||||
msg = self.m.Message()
|
||||
msg['to'] = "fritzy@netflint.net/sleekxmpp"
|
||||
msg['body'] = "this is the plaintext message"
|
||||
msg['type'] = 'chat'
|
||||
p = ET.Element('{http://www.w3.org/1999/xhtml}p')
|
||||
p.text = "This is the htmlim message"
|
||||
msg['html']['html'] = p
|
||||
msg2 = self.m.Message()
|
||||
values = msg.getValues()
|
||||
msg2.setValues(values)
|
||||
self.failUnless(msgtxt == str(msg) == str(msg2))
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testmessagestanzas)
|
||||
@@ -1,31 +0,0 @@
|
||||
import unittest
|
||||
|
||||
class testpresencestanzas(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import sleekxmpp.stanza.presence as p
|
||||
self.p = p
|
||||
|
||||
def testPresenceShowRegression(self):
|
||||
"Regression check presence['type'] = 'dnd' show value working"
|
||||
p = self.p.Presence()
|
||||
p['type'] = 'dnd'
|
||||
self.failUnless(str(p) == "<presence><show>dnd</show></presence>")
|
||||
|
||||
def testPresenceUnsolicitedOffline(self):
|
||||
"Unsolicted offline presence does not spawn changed_status or update roster"
|
||||
p = self.p.Presence()
|
||||
p['type'] = 'unavailable'
|
||||
p['from'] = 'bill@chadmore.com/gmail15af'
|
||||
import sleekxmpp
|
||||
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
|
||||
happened = []
|
||||
def handlechangedpresence(event):
|
||||
happened.append(True)
|
||||
c.add_event_handler("changed_status", handlechangedpresence)
|
||||
c._handlePresence(p)
|
||||
self.failUnless(happened == [], "changed_status event triggered for superfulous unavailable presence")
|
||||
self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testpresencestanzas)
|
||||
@@ -1,314 +0,0 @@
|
||||
import unittest
|
||||
from xml.etree import cElementTree as ET
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from . import xmlcompare
|
||||
|
||||
class testpubsubstanzas(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import sleekxmpp.plugins.stanza_pubsub as ps
|
||||
self.ps = ps
|
||||
|
||||
def testAffiliations(self):
|
||||
"Testing iq/pubsub/affiliations/affiliation stanzas"
|
||||
iq = self.ps.Iq()
|
||||
aff1 = self.ps.Affiliation()
|
||||
aff1['node'] = 'testnode'
|
||||
aff1['affiliation'] = 'owner'
|
||||
aff2 = self.ps.Affiliation()
|
||||
aff2['node'] = 'testnode2'
|
||||
aff2['affiliation'] = 'publisher'
|
||||
iq['pubsub']['affiliations'].append(aff1)
|
||||
iq['pubsub']['affiliations'].append(aff2)
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><affiliations><affiliation node="testnode" affiliation="owner" /><affiliation node="testnode2" affiliation="publisher" /></affiliations></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), "3 methods for creating stanza don't match")
|
||||
self.failUnless(iq.match('iq@id=0/pubsub/affiliations/affiliation@node=testnode2@affiliation=publisher'), 'Match path failed')
|
||||
|
||||
def testSubscriptions(self):
|
||||
"Testing iq/pubsub/subscriptions/subscription stanzas"
|
||||
iq = self.ps.Iq()
|
||||
sub1 = self.ps.Subscription()
|
||||
sub1['node'] = 'testnode'
|
||||
sub1['jid'] = 'steve@myserver.tld/someresource'
|
||||
sub2 = self.ps.Subscription()
|
||||
sub2['node'] = 'testnode2'
|
||||
sub2['jid'] = 'boogers@bork.top/bill'
|
||||
sub2['subscription'] = 'subscribed'
|
||||
iq['pubsub']['subscriptions'].append(sub1)
|
||||
iq['pubsub']['subscriptions'].append(sub2)
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscriptions><subscription node="testnode" jid="steve@myserver.tld/someresource" /><subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" /></subscriptions></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testOptionalSettings(self):
|
||||
"Testing iq/pubsub/subscription/subscribe-options stanzas"
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub']['subscription']['suboptions']['required'] = True
|
||||
iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas'
|
||||
iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp"
|
||||
iq['pubsub']['subscription']['subscription'] = 'unconfigured'
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured"><subscribe-options><required /></subscribe-options></subscription></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testItems(self):
|
||||
"Testing iq/pubsub/items stanzas"
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub']['items']
|
||||
payload = ET.fromstring("""<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'><child1 /><child2 normandy='cheese' foo='bar' /></thinger>""")
|
||||
payload2 = ET.fromstring("""<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'><child12 /><child22 normandy='cheese2' foo='bar2' /></thinger2>""")
|
||||
item = self.ps.Item()
|
||||
item['id'] = 'asdf'
|
||||
item['payload'] = payload
|
||||
item2 = self.ps.Item()
|
||||
item2['id'] = 'asdf2'
|
||||
item2['payload'] = payload2
|
||||
iq['pubsub']['items'].append(item)
|
||||
iq['pubsub']['items'].append(item2)
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><items><item id="asdf"><thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1"><child1 /><child2 foo="bar" normandy="cheese" /></thinger></item><item id="asdf2"><thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12"><child12 /><child22 foo="bar2" normandy="cheese2" /></thinger2></item></items></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testCreate(self):
|
||||
"Testing iq/pubsub/create&configure stanzas"
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub']['create']['node'] = 'mynode'
|
||||
form = xep_0004.Form()
|
||||
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
|
||||
iq['pubsub']['configure']['config'] = form
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="mynode" /><configure><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></configure></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testState(self):
|
||||
"Testing iq/psstate stanzas"
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
iq = self.ps.Iq()
|
||||
iq['psstate']['node']= 'mynode'
|
||||
iq['psstate']['item']= 'myitem'
|
||||
pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
|
||||
iq['psstate']['payload'] = pl
|
||||
xmlstring = """<iq id="0"><state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem"><claimed xmlns="http://andyet.net/protocol/pubsubqueue" /></state></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testDefault(self):
|
||||
"Testing iq/pubsub_owner/default stanzas"
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub_owner']['default']
|
||||
iq['pubsub_owner']['default']['node'] = 'mynode'
|
||||
form = xep_0004.Form()
|
||||
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
|
||||
iq['pubsub_owner']['default']['config'] = form
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub#owner"><default node="mynode"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></default></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testSubscribe(self):
|
||||
"Testing iq/pubsub/subscribe stanzas"
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub']['subscribe']['options']
|
||||
iq['pubsub']['subscribe']['node'] = 'cheese'
|
||||
iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp'
|
||||
iq['pubsub']['subscribe']['options']['node'] = 'cheese'
|
||||
iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp'
|
||||
form = xep_0004.Form()
|
||||
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
|
||||
iq['pubsub']['subscribe']['options']['options'] = form
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp"><options node="cheese" jid="fritzy@netflint.net/sleekxmpp"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></options></subscribe></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testPublish(self):
|
||||
"Testing iq/pubsub/publish stanzas"
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub']['publish']['node'] = 'thingers'
|
||||
payload = ET.fromstring("""<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'><child1 /><child2 normandy='cheese' foo='bar' /></thinger>""")
|
||||
payload2 = ET.fromstring("""<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'><child12 /><child22 normandy='cheese2' foo='bar2' /></thinger2>""")
|
||||
item = self.ps.Item()
|
||||
item['id'] = 'asdf'
|
||||
item['payload'] = payload
|
||||
item2 = self.ps.Item()
|
||||
item2['id'] = 'asdf2'
|
||||
item2['payload'] = payload2
|
||||
iq['pubsub']['publish'].append(item)
|
||||
iq['pubsub']['publish'].append(item2)
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><publish node="thingers"><item id="asdf"><thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1"><child1 /><child2 foo="bar" normandy="cheese" /></thinger></item><item id="asdf2"><thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12"><child12 /><child22 foo="bar2" normandy="cheese2" /></thinger2></item></publish></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testDelete(self):
|
||||
"Testing iq/pubsub_owner/delete stanzas"
|
||||
iq = self.ps.Iq()
|
||||
iq['pubsub_owner']['delete']['node'] = 'thingers'
|
||||
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub#owner"><delete node="thingers" /></pubsub></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
iq3.setValues(iq2.getValues())
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testCreateConfigGet(self):
|
||||
"""Testing getting config from full create"""
|
||||
xml = """<iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7"><pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="testnode2" /><configure><x xmlns="jabber:x:data" type="submit"><field var="FORM_TYPE" type="hidden"><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var="pubsub#node_type" type="list-single" label="Select the node type"><value>leaf</value></field><field var="pubsub#title" type="text-single" label="A friendly name for the node" /><field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications"><value>1</value></field><field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications"><value>1</value></field><field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" /><field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" /><field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node"><value>1</value></field><field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" /><field var="pubsub#persist_items" type="boolean" label="Persist items in storage" /><field var="pubsub#max_items" type="text-single" label="Max # of items to persist"><value>10</value></field><field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions"><value>1</value></field><field var="pubsub#access_model" type="list-single" label="Specify the subscriber model"><value>open</value></field><field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"><value>publishers</value></field><field var="pubsub#send_last_published_item" type="list-single" label="Send last published item"><value>never</value></field><field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" /></x></configure></pubsub></iq>"""
|
||||
iq = self.ps.Iq(None, self.ps.ET.fromstring(xml))
|
||||
config = iq['pubsub']['configure']['config']
|
||||
self.failUnless(config.getValues() != {})
|
||||
|
||||
def testItemEvent(self):
|
||||
"""Testing message/pubsub_event/items/item"""
|
||||
msg = self.ps.Message()
|
||||
item = self.ps.EventItem()
|
||||
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
|
||||
item['payload'] = pl
|
||||
item['id'] = 'abc123'
|
||||
msg['pubsub_event']['items'].append(item)
|
||||
msg['pubsub_event']['items']['node'] = 'cheese'
|
||||
msg['type'] = 'normal'
|
||||
xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item></items></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testItemsEvent(self):
|
||||
"""Testing multiple message/pubsub_event/items/item"""
|
||||
msg = self.ps.Message()
|
||||
item = self.ps.EventItem()
|
||||
item2 = self.ps.EventItem()
|
||||
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
|
||||
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
|
||||
item2['payload'] = pl2
|
||||
item['payload'] = pl
|
||||
item['id'] = 'abc123'
|
||||
item2['id'] = '123abc'
|
||||
msg['pubsub_event']['items'].append(item)
|
||||
msg['pubsub_event']['items'].append(item2)
|
||||
msg['pubsub_event']['items']['node'] = 'cheese'
|
||||
msg['type'] = 'normal'
|
||||
xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item><item id="123abc"><test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" /></item></items></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testItemsEvent(self):
|
||||
"""Testing message/pubsub_event/items/item & retract mix"""
|
||||
msg = self.ps.Message()
|
||||
item = self.ps.EventItem()
|
||||
item2 = self.ps.EventItem()
|
||||
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
|
||||
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
|
||||
item2['payload'] = pl2
|
||||
retract = self.ps.EventRetract()
|
||||
retract['id'] = 'aabbcc'
|
||||
item['payload'] = pl
|
||||
item['id'] = 'abc123'
|
||||
item2['id'] = '123abc'
|
||||
msg['pubsub_event']['items'].append(item)
|
||||
msg['pubsub_event']['items'].append(retract)
|
||||
msg['pubsub_event']['items'].append(item2)
|
||||
msg['pubsub_event']['items']['node'] = 'cheese'
|
||||
msg['type'] = 'normal'
|
||||
xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item><retract id="aabbcc" /><item id="123abc"><test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" /></item></items></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testCollectionAssociate(self):
|
||||
"""Testing message/pubsub_event/collection/associate"""
|
||||
msg = self.ps.Message()
|
||||
msg['pubsub_event']['collection']['associate']['node'] = 'cheese'
|
||||
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
|
||||
msg['type'] = 'headline'
|
||||
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><collection node="cheeseburger"><associate node="cheese" /></collection></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testCollectionDisassociate(self):
|
||||
"""Testing message/pubsub_event/collection/disassociate"""
|
||||
msg = self.ps.Message()
|
||||
msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese'
|
||||
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
|
||||
msg['type'] = 'headline'
|
||||
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><collection node="cheeseburger"><disassociate node="cheese" /></collection></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testEventConfiguration(self):
|
||||
"""Testing message/pubsub_event/configuration/config"""
|
||||
msg = self.ps.Message()
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
form = xep_0004.Form()
|
||||
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
|
||||
msg['pubsub_event']['configuration']['node'] = 'cheese'
|
||||
msg['pubsub_event']['configuration']['config'] = form
|
||||
msg['type'] = 'headline'
|
||||
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><configuration node="cheese"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></configuration></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testEventPurge(self):
|
||||
"""Testing message/pubsub_event/purge"""
|
||||
msg = self.ps.Message()
|
||||
msg['pubsub_event']['purge']['node'] = 'pickles'
|
||||
msg['type'] = 'headline'
|
||||
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><purge node="pickles" /></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
|
||||
|
||||
def testEventSubscription(self):
|
||||
"""Testing message/pubsub_event/subscription"""
|
||||
msg = self.ps.Message()
|
||||
msg['pubsub_event']['subscription']['node'] = 'pickles'
|
||||
msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test'
|
||||
msg['pubsub_event']['subscription']['subid'] = 'aabb1122'
|
||||
msg['pubsub_event']['subscription']['subscription'] = 'subscribed'
|
||||
msg['pubsub_event']['subscription']['expiry'] = 'presence'
|
||||
msg['type'] = 'headline'
|
||||
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" /></event></message>"""
|
||||
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
|
||||
msg3 = self.ps.Message()
|
||||
msg3.setValues(msg2.getValues())
|
||||
self.failUnless(xmlcompare.comparemany([xmlstring, str(msg), str(msg2), str(msg3)]))
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testpubsubstanzas)
|
||||
79
tests/test_stanza_base.py
Normal file
79
tests/test_stanza_base.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase
|
||||
|
||||
|
||||
class TestStanzaBase(SleekTest):
|
||||
|
||||
def testTo(self):
|
||||
"""Test the 'to' interface of StanzaBase."""
|
||||
stanza = StanzaBase()
|
||||
stanza['to'] = 'user@example.com'
|
||||
self.failUnless(str(stanza['to']) == 'user@example.com',
|
||||
"Setting and retrieving stanza 'to' attribute did not work.")
|
||||
|
||||
def testFrom(self):
|
||||
"""Test the 'from' interface of StanzaBase."""
|
||||
stanza = StanzaBase()
|
||||
stanza['from'] = 'user@example.com'
|
||||
self.failUnless(str(stanza['from']) == 'user@example.com',
|
||||
"Setting and retrieving stanza 'from' attribute did not work.")
|
||||
|
||||
def testPayload(self):
|
||||
"""Test the 'payload' interface of StanzaBase."""
|
||||
stanza = StanzaBase()
|
||||
self.failUnless(stanza['payload'] == [],
|
||||
"Empty stanza does not have an empty payload.")
|
||||
|
||||
stanza['payload'] = ET.Element("{foo}foo")
|
||||
self.failUnless(len(stanza['payload']) == 1,
|
||||
"Stanza contents and payload do not match.")
|
||||
|
||||
stanza['payload'] = ET.Element('{bar}bar')
|
||||
self.failUnless(len(stanza['payload']) == 2,
|
||||
"Stanza payload was not appended.")
|
||||
|
||||
del stanza['payload']
|
||||
self.failUnless(stanza['payload'] == [],
|
||||
"Stanza payload not cleared after deletion.")
|
||||
|
||||
stanza['payload'] = [ET.Element('{foo}foo'),
|
||||
ET.Element('{bar}bar')]
|
||||
self.failUnless(len(stanza['payload']) == 2,
|
||||
"Adding multiple elements to stanza's payload did not work.")
|
||||
|
||||
def testClear(self):
|
||||
"""Test clearing a stanza."""
|
||||
stanza = StanzaBase()
|
||||
stanza['to'] = 'user@example.com'
|
||||
stanza['payload'] = ET.Element("{foo}foo")
|
||||
stanza.clear()
|
||||
|
||||
self.failUnless(stanza['payload'] == [],
|
||||
"Stanza payload was not cleared after calling .clear()")
|
||||
self.failUnless(str(stanza['to']) == "user@example.com",
|
||||
"Stanza attributes were not preserved after calling .clear()")
|
||||
|
||||
def testReply(self):
|
||||
"""Test creating a reply stanza."""
|
||||
stanza = StanzaBase()
|
||||
stanza['to'] = "recipient@example.com"
|
||||
stanza['from'] = "sender@example.com"
|
||||
stanza['payload'] = ET.Element("{foo}foo")
|
||||
|
||||
stanza.reply()
|
||||
|
||||
self.failUnless(str(stanza['to'] == "sender@example.com"),
|
||||
"Stanza reply did not change 'to' attribute.")
|
||||
self.failUnless(stanza['payload'] == [],
|
||||
"Stanza reply did not empty stanza payload.")
|
||||
|
||||
def testError(self):
|
||||
"""Test marking a stanza as an error."""
|
||||
stanza = StanzaBase()
|
||||
stanza['type'] = 'get'
|
||||
stanza.error()
|
||||
self.failUnless(stanza['type'] == 'error',
|
||||
"Stanza type is not 'error' after calling error()")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)
|
||||
660
tests/test_stanza_element.py
Normal file
660
tests/test_stanza_element.py
Normal file
@@ -0,0 +1,660 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.xmlstream.stanzabase import ElementBase
|
||||
|
||||
|
||||
class TestElementBase(SleekTest):
|
||||
|
||||
def testFixNs(self):
|
||||
"""Test fixing namespaces in an XPath expression."""
|
||||
|
||||
e = ElementBase()
|
||||
ns = "http://jabber.org/protocol/disco#items"
|
||||
result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns))
|
||||
|
||||
expected = "/".join(["{%s}foo" % ns,
|
||||
"{%s}bar" % ns,
|
||||
"{abc}baz",
|
||||
"{%s}more" % ns])
|
||||
self.failUnless(expected == result,
|
||||
"Incorrect namespace fixing result: %s" % str(result))
|
||||
|
||||
|
||||
def testExtendedName(self):
|
||||
"""Test element names of the form tag1/tag2/tag3."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo/bar/baz"
|
||||
namespace = "test"
|
||||
|
||||
stanza = TestStanza()
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<bar>
|
||||
<baz />
|
||||
</bar>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testGetStanzaValues(self):
|
||||
"""Test getStanzaValues using plugins and substanzas."""
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "foo2"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
plugin_attrib = "foo2"
|
||||
|
||||
class TestSubStanza(ElementBase):
|
||||
name = "subfoo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
subitem = set((TestSubStanza,))
|
||||
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['bar'] = 'a'
|
||||
stanza['foo2']['baz'] = 'b'
|
||||
substanza = TestSubStanza()
|
||||
substanza['bar'] = 'c'
|
||||
stanza.append(substanza)
|
||||
|
||||
values = stanza.getStanzaValues()
|
||||
expected = {'bar': 'a',
|
||||
'baz': '',
|
||||
'foo2': {'bar': '',
|
||||
'baz': 'b'},
|
||||
'substanzas': [{'__childtag__': '{foo}subfoo',
|
||||
'bar': 'c',
|
||||
'baz': ''}]}
|
||||
self.failUnless(values == expected,
|
||||
"Unexpected stanza values:\n%s\n%s" % (str(expected), str(values)))
|
||||
|
||||
|
||||
def testSetStanzaValues(self):
|
||||
"""Test using setStanzaValues with substanzas and plugins."""
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "pluginfoo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
plugin_attrib = "plugin_foo"
|
||||
|
||||
class TestStanzaPlugin2(ElementBase):
|
||||
name = "pluginfoo2"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
plugin_attrib = "plugin_foo2"
|
||||
|
||||
class TestSubStanza(ElementBase):
|
||||
name = "subfoo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
subitem = set((TestSubStanza,))
|
||||
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin2)
|
||||
|
||||
stanza = TestStanza()
|
||||
values = {'bar': 'a',
|
||||
'baz': '',
|
||||
'plugin_foo': {'bar': '',
|
||||
'baz': 'b'},
|
||||
'plugin_foo2': {'bar': 'd',
|
||||
'baz': 'e'},
|
||||
'substanzas': [{'__childtag__': '{foo}subfoo',
|
||||
'bar': 'c',
|
||||
'baz': ''}]}
|
||||
stanza.setStanzaValues(values)
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" bar="a">
|
||||
<pluginfoo baz="b" />
|
||||
<pluginfoo2 bar="d" baz="e" />
|
||||
<subfoo bar="c" />
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testGetItem(self):
|
||||
"""Test accessing stanza interfaces."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz', 'qux'))
|
||||
sub_interfaces = set(('baz',))
|
||||
|
||||
def getQux(self):
|
||||
return 'qux'
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "foobar"
|
||||
namespace = "foo"
|
||||
plugin_attrib = "foobar"
|
||||
interfaces = set(('fizz',))
|
||||
|
||||
TestStanza.subitem = (TestStanza,)
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||
|
||||
stanza = TestStanza()
|
||||
substanza = TestStanza()
|
||||
stanza.append(substanza)
|
||||
stanza.setStanzaValues({'bar': 'a',
|
||||
'baz': 'b',
|
||||
'qux': 42,
|
||||
'foobar': {'fizz': 'c'}})
|
||||
|
||||
# Test non-plugin interfaces
|
||||
expected = {'substanzas': [substanza],
|
||||
'bar': 'a',
|
||||
'baz': 'b',
|
||||
'qux': 'qux',
|
||||
'meh': ''}
|
||||
for interface, value in expected.items():
|
||||
result = stanza[interface]
|
||||
self.failUnless(result == value,
|
||||
"Incorrect stanza interface access result: %s" % result)
|
||||
|
||||
# Test plugin interfaces
|
||||
self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin),
|
||||
"Incorrect plugin object result.")
|
||||
self.failUnless(stanza['foobar']['fizz'] == 'c',
|
||||
"Incorrect plugin subvalue result.")
|
||||
|
||||
def testSetItem(self):
|
||||
"""Test assigning to stanza interfaces."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz', 'qux'))
|
||||
sub_interfaces = set(('baz',))
|
||||
|
||||
def setQux(self, value):
|
||||
pass
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "foobar"
|
||||
namespace = "foo"
|
||||
plugin_attrib = "foobar"
|
||||
interfaces = set(('foobar',))
|
||||
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||
|
||||
stanza = TestStanza()
|
||||
|
||||
stanza['bar'] = 'attribute!'
|
||||
stanza['baz'] = 'element!'
|
||||
stanza['qux'] = 'overridden'
|
||||
stanza['foobar'] = 'plugin'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" bar="attribute!">
|
||||
<baz>element!</baz>
|
||||
<foobar foobar="plugin" />
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testDelItem(self):
|
||||
"""Test deleting stanza interface values."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz', 'qux'))
|
||||
sub_interfaces = set(('bar',))
|
||||
|
||||
def delQux(self):
|
||||
pass
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "foobar"
|
||||
namespace = "foo"
|
||||
plugin_attrib = "foobar"
|
||||
interfaces = set(('foobar',))
|
||||
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['bar'] = 'a'
|
||||
stanza['baz'] = 'b'
|
||||
stanza['qux'] = 'c'
|
||||
stanza['foobar']['foobar'] = 'd'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" baz="b" qux="c">
|
||||
<bar>a</bar>
|
||||
<foobar foobar="d" />
|
||||
</foo>
|
||||
""")
|
||||
|
||||
del stanza['bar']
|
||||
del stanza['baz']
|
||||
del stanza['qux']
|
||||
del stanza['foobar']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" qux="c" />
|
||||
""")
|
||||
|
||||
def testModifyingAttributes(self):
|
||||
"""Test modifying top level attributes of a stanza's XML object."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
stanza = TestStanza()
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" />
|
||||
""")
|
||||
|
||||
self.failUnless(stanza._get_attr('bar') == '',
|
||||
"Incorrect value returned for an unset XML attribute.")
|
||||
|
||||
stanza._set_attr('bar', 'a')
|
||||
stanza._set_attr('baz', 'b')
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" bar="a" baz="b" />
|
||||
""")
|
||||
|
||||
self.failUnless(stanza._get_attr('bar') == 'a',
|
||||
"Retrieved XML attribute value is incorrect.")
|
||||
|
||||
stanza._set_attr('bar', None)
|
||||
stanza._del_attr('baz')
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo" />
|
||||
""")
|
||||
|
||||
self.failUnless(stanza._get_attr('bar', 'c') == 'c',
|
||||
"Incorrect default value returned for an unset XML attribute.")
|
||||
|
||||
def testGetSubText(self):
|
||||
"""Test retrieving the contents of a sub element."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar',))
|
||||
|
||||
def setBar(self, value):
|
||||
wrapper = ET.Element("{foo}wrapper")
|
||||
bar = ET.Element("{foo}bar")
|
||||
bar.text = value
|
||||
wrapper.append(bar)
|
||||
self.xml.append(wrapper)
|
||||
|
||||
def getBar(self):
|
||||
return self._get_sub_text("wrapper/bar", default="not found")
|
||||
|
||||
stanza = TestStanza()
|
||||
self.failUnless(stanza['bar'] == 'not found',
|
||||
"Default _get_sub_text value incorrect.")
|
||||
|
||||
stanza['bar'] = 'found'
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<wrapper>
|
||||
<bar>found</bar>
|
||||
</wrapper>
|
||||
</foo>
|
||||
""")
|
||||
self.failUnless(stanza['bar'] == 'found',
|
||||
"_get_sub_text value incorrect: %s." % stanza['bar'])
|
||||
|
||||
def testSubElement(self):
|
||||
"""Test setting the contents of a sub element."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
def setBaz(self, value):
|
||||
self._set_sub_text("wrapper/baz", text=value)
|
||||
|
||||
def getBaz(self):
|
||||
return self._get_sub_text("wrapper/baz")
|
||||
|
||||
def setBar(self, value):
|
||||
self._set_sub_text("wrapper/bar", text=value)
|
||||
|
||||
def getBar(self):
|
||||
return self._get_sub_text("wrapper/bar")
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['bar'] = 'a'
|
||||
stanza['baz'] = 'b'
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<wrapper>
|
||||
<bar>a</bar>
|
||||
<baz>b</baz>
|
||||
</wrapper>
|
||||
</foo>
|
||||
""")
|
||||
stanza._set_sub_text('wrapper/bar', text='', keep=True)
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<wrapper>
|
||||
<bar />
|
||||
<baz>b</baz>
|
||||
</wrapper>
|
||||
</foo>
|
||||
""", use_values=False)
|
||||
|
||||
stanza['bar'] = 'a'
|
||||
stanza._set_sub_text('wrapper/bar', text='')
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<wrapper>
|
||||
<baz>b</baz>
|
||||
</wrapper>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testDelSub(self):
|
||||
"""Test removing sub elements."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
def setBar(self, value):
|
||||
self._set_sub_text("path/to/only/bar", value);
|
||||
|
||||
def getBar(self):
|
||||
return self._get_sub_text("path/to/only/bar")
|
||||
|
||||
def delBar(self):
|
||||
self._del_sub("path/to/only/bar")
|
||||
|
||||
def setBaz(self, value):
|
||||
self._set_sub_text("path/to/just/baz", value);
|
||||
|
||||
def getBaz(self):
|
||||
return self._get_sub_text("path/to/just/baz")
|
||||
|
||||
def delBaz(self):
|
||||
self._del_sub("path/to/just/baz")
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['bar'] = 'a'
|
||||
stanza['baz'] = 'b'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<path>
|
||||
<to>
|
||||
<only>
|
||||
<bar>a</bar>
|
||||
</only>
|
||||
<just>
|
||||
<baz>b</baz>
|
||||
</just>
|
||||
</to>
|
||||
</path>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
del stanza['bar']
|
||||
del stanza['baz']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<path>
|
||||
<to>
|
||||
<only />
|
||||
<just />
|
||||
</to>
|
||||
</path>
|
||||
</foo>
|
||||
""", use_values=False)
|
||||
|
||||
stanza['bar'] = 'a'
|
||||
stanza['baz'] = 'b'
|
||||
|
||||
stanza._del_sub('path/to/only/bar', all=True)
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<path>
|
||||
<to>
|
||||
<just>
|
||||
<baz>b</baz>
|
||||
</just>
|
||||
</to>
|
||||
</path>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testMatch(self):
|
||||
"""Test matching a stanza against an XPath expression."""
|
||||
|
||||
class TestSubStanza(ElementBase):
|
||||
name = "sub"
|
||||
namespace = "baz"
|
||||
interfaces = set(('attrib',))
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar','baz', 'qux'))
|
||||
sub_interfaces = set(('qux',))
|
||||
subitem = (TestSubStanza,)
|
||||
|
||||
def setQux(self, value):
|
||||
self._set_sub_text('qux', text=value)
|
||||
|
||||
def getQux(self):
|
||||
return self._get_sub_text('qux')
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "plugin"
|
||||
namespace = "http://test/slash/bar"
|
||||
interfaces = set(('attrib',))
|
||||
|
||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||
|
||||
stanza = TestStanza()
|
||||
self.failUnless(stanza.match("foo"),
|
||||
"Stanza did not match its own tag name.")
|
||||
|
||||
self.failUnless(stanza.match("{foo}foo"),
|
||||
"Stanza did not match its own namespaced name.")
|
||||
|
||||
stanza['bar'] = 'a'
|
||||
self.failUnless(stanza.match("foo@bar=a"),
|
||||
"Stanza did not match its own name with attribute value check.")
|
||||
|
||||
stanza['baz'] = 'b'
|
||||
self.failUnless(stanza.match("foo@bar=a@baz=b"),
|
||||
"Stanza did not match its own name with multiple attributes.")
|
||||
|
||||
stanza['qux'] = 'c'
|
||||
self.failUnless(stanza.match("foo/qux"),
|
||||
"Stanza did not match with subelements.")
|
||||
|
||||
stanza['qux'] = ''
|
||||
self.failUnless(stanza.match("foo/qux") == False,
|
||||
"Stanza matched missing subinterface element.")
|
||||
|
||||
self.failUnless(stanza.match("foo/bar") == False,
|
||||
"Stanza matched nonexistent element.")
|
||||
|
||||
stanza['plugin']['attrib'] = 'c'
|
||||
self.failUnless(stanza.match("foo/plugin@attrib=c"),
|
||||
"Stanza did not match with plugin and attribute.")
|
||||
|
||||
self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"),
|
||||
"Stanza did not match with namespaced plugin.")
|
||||
|
||||
substanza = TestSubStanza()
|
||||
substanza['attrib'] = 'd'
|
||||
stanza.append(substanza)
|
||||
self.failUnless(stanza.match("foo/sub@attrib=d"),
|
||||
"Stanza did not match with substanzas and attribute.")
|
||||
|
||||
self.failUnless(stanza.match("foo/{baz}sub"),
|
||||
"Stanza did not match with namespaced substanza.")
|
||||
|
||||
def testComparisons(self):
|
||||
"""Test comparing ElementBase objects."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
stanza1 = TestStanza()
|
||||
stanza1['bar'] = 'a'
|
||||
|
||||
self.failUnless(stanza1,
|
||||
"Stanza object does not evaluate to True")
|
||||
|
||||
stanza2 = TestStanza()
|
||||
stanza2['baz'] = 'b'
|
||||
|
||||
self.failUnless(stanza1 != stanza2,
|
||||
"Different stanza objects incorrectly compared equal.")
|
||||
|
||||
stanza1['baz'] = 'b'
|
||||
stanza2['bar'] = 'a'
|
||||
|
||||
self.failUnless(stanza1 == stanza2,
|
||||
"Equal stanzas incorrectly compared inequal.")
|
||||
|
||||
def testKeys(self):
|
||||
"""Test extracting interface names from a stanza object."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
plugin_attrib = 'qux'
|
||||
|
||||
register_stanza_plugin(TestStanza, TestStanza)
|
||||
|
||||
stanza = TestStanza()
|
||||
|
||||
self.failUnless(set(stanza.keys()) == set(('bar', 'baz')),
|
||||
"Returned set of interface keys does not match expected.")
|
||||
|
||||
stanza.enable('qux')
|
||||
|
||||
self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')),
|
||||
"Incorrect set of interface and plugin keys.")
|
||||
|
||||
def testGet(self):
|
||||
"""Test accessing stanza interfaces using get()."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['bar'] = 'a'
|
||||
|
||||
self.failUnless(stanza.get('bar') == 'a',
|
||||
"Incorrect value returned by stanza.get")
|
||||
|
||||
self.failUnless(stanza.get('baz', 'b') == 'b',
|
||||
"Incorrect default value returned by stanza.get")
|
||||
|
||||
def testSubStanzas(self):
|
||||
"""Test manipulating substanzas of a stanza object."""
|
||||
|
||||
class TestSubStanza(ElementBase):
|
||||
name = "foobar"
|
||||
namespace = "foo"
|
||||
interfaces = set(('qux',))
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
subitem = (TestSubStanza,)
|
||||
|
||||
stanza = TestStanza()
|
||||
substanza1 = TestSubStanza()
|
||||
substanza2 = TestSubStanza()
|
||||
substanza1['qux'] = 'a'
|
||||
substanza2['qux'] = 'b'
|
||||
|
||||
# Test appending substanzas
|
||||
self.failUnless(len(stanza) == 0,
|
||||
"Incorrect empty stanza size.")
|
||||
|
||||
stanza.append(substanza1)
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<foobar qux="a" />
|
||||
</foo>
|
||||
""", use_values=False)
|
||||
self.failUnless(len(stanza) == 1,
|
||||
"Incorrect stanza size with 1 substanza.")
|
||||
|
||||
stanza.append(substanza2)
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<foobar qux="a" />
|
||||
<foobar qux="b" />
|
||||
</foo>
|
||||
""", use_values=False)
|
||||
self.failUnless(len(stanza) == 2,
|
||||
"Incorrect stanza size with 2 substanzas.")
|
||||
|
||||
# Test popping substanzas
|
||||
stanza.pop(0)
|
||||
self.check(stanza, """
|
||||
<foo xmlns="foo">
|
||||
<foobar qux="b" />
|
||||
</foo>
|
||||
""", use_values=False)
|
||||
|
||||
# Test iterating over substanzas
|
||||
stanza.append(substanza1)
|
||||
results = []
|
||||
for substanza in stanza:
|
||||
results.append(substanza['qux'])
|
||||
self.failUnless(results == ['b', 'a'],
|
||||
"Iteration over substanzas failed: %s." % str(results))
|
||||
|
||||
def testCopy(self):
|
||||
"""Test copying stanza objects."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar', 'baz'))
|
||||
|
||||
stanza1 = TestStanza()
|
||||
stanza1['bar'] = 'a'
|
||||
|
||||
stanza2 = stanza1.__copy__()
|
||||
|
||||
self.failUnless(stanza1 == stanza2,
|
||||
"Copied stanzas are not equal to each other.")
|
||||
|
||||
stanza1['baz'] = 'b'
|
||||
self.failUnless(stanza1 != stanza2,
|
||||
"Divergent stanza copies incorrectly compared equal.")
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)
|
||||
76
tests/test_stanza_error.py
Normal file
76
tests/test_stanza_error.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from sleekxmpp.test import *
|
||||
|
||||
|
||||
class TestErrorStanzas(SleekTest):
|
||||
|
||||
def testSetup(self):
|
||||
"""Test setting initial values in error stanza."""
|
||||
msg = self.Message()
|
||||
msg.enable('error')
|
||||
self.check(msg, """
|
||||
<message type="error">
|
||||
<error type="cancel">
|
||||
<feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
</error>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testCondition(self):
|
||||
"""Test modifying the error condition."""
|
||||
msg = self.Message()
|
||||
msg['error']['condition'] = 'item-not-found'
|
||||
|
||||
self.check(msg, """
|
||||
<message type="error">
|
||||
<error type="cancel">
|
||||
<item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
</error>
|
||||
</message>
|
||||
""")
|
||||
|
||||
self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.")
|
||||
|
||||
msg['error']['condition'] = 'resource-constraint'
|
||||
|
||||
self.check(msg, """
|
||||
<message type="error">
|
||||
<error type="cancel">
|
||||
<resource-constraint xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
</error>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testDelCondition(self):
|
||||
"""Test that deleting error conditions doesn't remove extra elements."""
|
||||
msg = self.Message()
|
||||
msg['error']['text'] = 'Error!'
|
||||
msg['error']['condition'] = 'internal-server-error'
|
||||
|
||||
del msg['error']['condition']
|
||||
|
||||
self.check(msg, """
|
||||
<message type="error">
|
||||
<error type="cancel">
|
||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Error!</text>
|
||||
</error>
|
||||
</message>
|
||||
""", use_values=False)
|
||||
|
||||
def testDelText(self):
|
||||
"""Test deleting the text of an error."""
|
||||
msg = self.Message()
|
||||
msg['error']['test'] = 'Error!'
|
||||
msg['error']['condition'] = 'internal-server-error'
|
||||
|
||||
del msg['error']['text']
|
||||
|
||||
self.check(msg, """
|
||||
<message type="error">
|
||||
<error type="cancel">
|
||||
<internal-server-error xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
</error>
|
||||
</message>
|
||||
""")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas)
|
||||
88
tests/test_stanza_gmail.py
Normal file
88
tests/test_stanza_gmail.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.gmail_notify as gmail
|
||||
|
||||
|
||||
class TestGmail(SleekTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Iq, gmail.GmailQuery)
|
||||
register_stanza_plugin(Iq, gmail.MailBox)
|
||||
register_stanza_plugin(Iq, gmail.NewMail)
|
||||
|
||||
def testCreateQuery(self):
|
||||
"""Testing querying Gmail for emails."""
|
||||
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['gmail']['search'] = 'is:starred'
|
||||
iq['gmail']['newer-than-time'] = '1140638252542'
|
||||
iq['gmail']['newer-than-tid'] = '11134623426430234'
|
||||
|
||||
self.check(iq, """
|
||||
<iq type="get">
|
||||
<query xmlns="google:mail:notify"
|
||||
newer-than-time="1140638252542"
|
||||
newer-than-tid="11134623426430234"
|
||||
q="is:starred" />
|
||||
</iq>
|
||||
""", use_values=False)
|
||||
|
||||
def testMailBox(self):
|
||||
"""Testing reading from Gmail mailbox result"""
|
||||
|
||||
# Use the example from Google's documentation at
|
||||
# http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications
|
||||
xml = ET.fromstring("""
|
||||
<iq type="result">
|
||||
<mailbox xmlns="google:mail:notify"
|
||||
result-time='1118012394209'
|
||||
url='http://mail.google.com/mail'
|
||||
total-matched='95'
|
||||
total-estimate='0'>
|
||||
<mail-thread-info tid='1172320964060972012'
|
||||
participation='1'
|
||||
messages='28'
|
||||
date='1118012394209'
|
||||
url='http://mail.google.com/mail?view=cv'>
|
||||
<senders>
|
||||
<sender name='Me' address='romeo@gmail.com' originator='1' />
|
||||
<sender name='Benvolio' address='benvolio@gmail.com' />
|
||||
<sender name='Mercutio' address='mercutio@gmail.com' unread='1'/>
|
||||
</senders>
|
||||
<labels>act1scene3</labels>
|
||||
<subject>Put thy rapier up.</subject>
|
||||
<snippet>Ay, ay, a scratch, a scratch; marry, 'tis enough.</snippet>
|
||||
</mail-thread-info>
|
||||
</mailbox>
|
||||
</iq>
|
||||
""")
|
||||
|
||||
iq = self.Iq(xml=xml)
|
||||
mailbox = iq['mailbox']
|
||||
self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match")
|
||||
self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match")
|
||||
self.failUnless(mailbox['matched'] == '95', "total-matched incorrect")
|
||||
self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect")
|
||||
self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads")
|
||||
|
||||
thread = mailbox['threads'][0]
|
||||
self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match")
|
||||
self.failUnless(thread['participation'] == '1', "thread participation incorrect")
|
||||
self.failUnless(thread['messages'] == '28', "thread message count incorrect")
|
||||
self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match")
|
||||
self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match")
|
||||
self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect")
|
||||
self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match")
|
||||
self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match")
|
||||
self.failUnless(len(thread['senders']) == 3, "could not extract senders")
|
||||
|
||||
sender1 = thread['senders'][0]
|
||||
self.failUnless(sender1['name'] == 'Me', "sender name doesn't match")
|
||||
self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match")
|
||||
self.failUnless(sender1['originator'] == True, "sender originator incorrect")
|
||||
self.failUnless(sender1['unread'] == False, "sender unread incorrectly True")
|
||||
|
||||
sender2 = thread['senders'][2]
|
||||
self.failUnless(sender2['unread'] == True, "sender unread incorrectly False")
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail)
|
||||
90
tests/test_stanza_iq.py
Normal file
90
tests/test_stanza_iq.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.xmlstream.stanzabase import ET
|
||||
|
||||
|
||||
class TestIqStanzas(SleekTest):
|
||||
|
||||
def tearDown(self):
|
||||
"""Shutdown the XML stream after testing."""
|
||||
self.stream_close()
|
||||
|
||||
def testSetup(self):
|
||||
"""Test initializing default Iq values."""
|
||||
iq = self.Iq()
|
||||
self.check(iq, """
|
||||
<iq id="0" />
|
||||
""")
|
||||
|
||||
def testPayload(self):
|
||||
"""Test setting Iq stanza payload."""
|
||||
iq = self.Iq()
|
||||
iq.setPayload(ET.Element('{test}tester'))
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<tester xmlns="test" />
|
||||
</iq>
|
||||
""", use_values=False)
|
||||
|
||||
|
||||
def testUnhandled(self):
|
||||
"""Test behavior for Iq.unhandled."""
|
||||
self.stream_start()
|
||||
self.recv("""
|
||||
<iq id="test" type="get">
|
||||
<query xmlns="test" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
iq = self.Iq()
|
||||
iq['id'] = 'test'
|
||||
iq['error']['condition'] = 'feature-not-implemented'
|
||||
iq['error']['text'] = 'No handlers registered for this request.'
|
||||
|
||||
self.send(iq, """
|
||||
<iq id="test" type="error">
|
||||
<error type="cancel">
|
||||
<feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
|
||||
No handlers registered for this request.
|
||||
</text>
|
||||
</error>
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testQuery(self):
|
||||
"""Test modifying query element of Iq stanzas."""
|
||||
iq = self.Iq()
|
||||
|
||||
iq['query'] = 'query_ns'
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="query_ns" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
iq['query'] = 'query_ns2'
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="query_ns2" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match")
|
||||
|
||||
del iq['query']
|
||||
self.check(iq, """
|
||||
<iq id="0" />
|
||||
""")
|
||||
|
||||
def testReply(self):
|
||||
"""Test setting proper result type in Iq replies."""
|
||||
iq = self.Iq()
|
||||
iq['to'] = 'user@localhost'
|
||||
iq['type'] = 'get'
|
||||
iq.reply()
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0" type="result" />
|
||||
""")
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas)
|
||||
57
tests/test_stanza_message.py
Normal file
57
tests/test_stanza_message.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.stanza.message import Message
|
||||
from sleekxmpp.stanza.htmlim import HTMLIM
|
||||
|
||||
|
||||
class TestMessageStanzas(SleekTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Message, HTMLIM)
|
||||
|
||||
def testGroupchatReplyRegression(self):
|
||||
"Regression groupchat reply should be to barejid"
|
||||
msg = self.Message()
|
||||
msg['to'] = 'me@myserver.tld'
|
||||
msg['from'] = 'room@someservice.someserver.tld/somenick'
|
||||
msg['type'] = 'groupchat'
|
||||
msg['body'] = "this is a message"
|
||||
msg.reply()
|
||||
self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld')
|
||||
|
||||
def testAttribProperty(self):
|
||||
"Test attrib property returning self"
|
||||
msg = self.Message()
|
||||
msg.attrib.attrib.attrib['to'] = 'usr@server.tld'
|
||||
self.failUnless(str(msg['to']) == 'usr@server.tld')
|
||||
|
||||
def testHTMLPlugin(self):
|
||||
"Test message/html/body stanza"
|
||||
msg = self.Message()
|
||||
msg['to'] = "fritzy@netflint.net/sleekxmpp"
|
||||
msg['body'] = "this is the plaintext message"
|
||||
msg['type'] = 'chat'
|
||||
p = ET.Element('{http://www.w3.org/1999/xhtml}p')
|
||||
p.text = "This is the htmlim message"
|
||||
msg['html']['body'] = p
|
||||
self.check(msg, """
|
||||
<message to="fritzy@netflint.net/sleekxmpp" type="chat">
|
||||
<body>this is the plaintext message</body>
|
||||
<html xmlns="http://jabber.org/protocol/xhtml-im">
|
||||
<body xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>This is the htmlim message</p>
|
||||
</body>
|
||||
</html>
|
||||
</message>""")
|
||||
|
||||
def testNickPlugin(self):
|
||||
"Test message/nick/nick stanza."
|
||||
msg = self.Message()
|
||||
msg['nick']['nick'] = 'A nickname!'
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<nick xmlns="http://jabber.org/nick/nick">A nickname!</nick>
|
||||
</message>
|
||||
""")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas)
|
||||
66
tests/test_stanza_presence.py
Normal file
66
tests/test_stanza_presence.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.stanza.presence import Presence
|
||||
|
||||
|
||||
class TestPresenceStanzas(SleekTest):
|
||||
|
||||
def testPresenceShowRegression(self):
|
||||
"""Regression check presence['type'] = 'dnd' show value working"""
|
||||
p = self.Presence()
|
||||
p['type'] = 'dnd'
|
||||
self.check(p, "<presence><show>dnd</show></presence>")
|
||||
|
||||
def testPresenceType(self):
|
||||
"""Test manipulating presence['type']"""
|
||||
p = self.Presence()
|
||||
p['type'] = 'available'
|
||||
self.check(p, "<presence />")
|
||||
self.failUnless(p['type'] == 'available',
|
||||
"Incorrect presence['type'] for type 'available': %s" % p['type'])
|
||||
|
||||
for showtype in ['away', 'chat', 'dnd', 'xa']:
|
||||
p['type'] = showtype
|
||||
self.check(p, """
|
||||
<presence><show>%s</show></presence>
|
||||
""" % showtype)
|
||||
self.failUnless(p['type'] == showtype,
|
||||
"Incorrect presence['type'] for type '%s'" % showtype)
|
||||
|
||||
p['type'] = None
|
||||
self.check(p, "<presence />")
|
||||
|
||||
def testPresenceUnsolicitedOffline(self):
|
||||
"""
|
||||
Unsolicted offline presence does not spawn changed_status
|
||||
or update the roster.
|
||||
"""
|
||||
p = self.Presence()
|
||||
p['type'] = 'unavailable'
|
||||
p['from'] = 'bill@chadmore.com/gmail15af'
|
||||
|
||||
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
|
||||
happened = []
|
||||
|
||||
def handlechangedpresence(event):
|
||||
happened.append(True)
|
||||
|
||||
c.add_event_handler("changed_status", handlechangedpresence)
|
||||
c._handle_presence(p)
|
||||
|
||||
self.failUnless(happened == [],
|
||||
"changed_status event triggered for extra unavailable presence")
|
||||
self.failUnless(c.roster == {},
|
||||
"Roster updated for superfulous unavailable presence")
|
||||
|
||||
def testNickPlugin(self):
|
||||
"""Test presence/nick/nick stanza."""
|
||||
p = self.Presence()
|
||||
p['nick']['nick'] = 'A nickname!'
|
||||
self.check(p, """
|
||||
<presence>
|
||||
<nick xmlns="http://jabber.org/nick/nick">A nickname!</nick>
|
||||
</presence>
|
||||
""")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas)
|
||||
84
tests/test_stanza_roster.py
Normal file
84
tests/test_stanza_roster.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.stanza.roster import Roster
|
||||
|
||||
|
||||
class TestRosterStanzas(SleekTest):
|
||||
|
||||
def testAddItems(self):
|
||||
"""Test adding items to a roster stanza."""
|
||||
iq = self.Iq()
|
||||
iq['roster'].setItems({
|
||||
'user@example.com': {
|
||||
'name': 'User',
|
||||
'subscription': 'both',
|
||||
'groups': ['Friends', 'Coworkers']},
|
||||
'otheruser@example.com': {
|
||||
'name': 'Other User',
|
||||
'subscription': 'both',
|
||||
'groups': []}})
|
||||
self.check(iq, """
|
||||
<iq>
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" name="User" subscription="both">
|
||||
<group>Friends</group>
|
||||
<group>Coworkers</group>
|
||||
</item>
|
||||
<item jid="otheruser@example.com" name="Other User"
|
||||
subscription="both" />
|
||||
</query>
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testGetItems(self):
|
||||
"""Test retrieving items from a roster stanza."""
|
||||
xml_string = """
|
||||
<iq>
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" name="User" subscription="both">
|
||||
<group>Friends</group>
|
||||
<group>Coworkers</group>
|
||||
</item>
|
||||
<item jid="otheruser@example.com" name="Other User"
|
||||
subscription="both" />
|
||||
</query>
|
||||
</iq>
|
||||
"""
|
||||
iq = self.Iq(ET.fromstring(xml_string))
|
||||
expected = {
|
||||
'user@example.com': {
|
||||
'name': 'User',
|
||||
'subscription': 'both',
|
||||
'groups': ['Friends', 'Coworkers']},
|
||||
'otheruser@example.com': {
|
||||
'name': 'Other User',
|
||||
'subscription': 'both',
|
||||
'groups': []}}
|
||||
debug = "Roster items don't match after retrieval."
|
||||
debug += "\nReturned: %s" % str(iq['roster']['items'])
|
||||
debug += "\nExpected: %s" % str(expected)
|
||||
self.failUnless(iq['roster']['items'] == expected, debug)
|
||||
|
||||
def testDelItems(self):
|
||||
"""Test clearing items from a roster stanza."""
|
||||
xml_string = """
|
||||
<iq>
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" name="User" subscription="both">
|
||||
<group>Friends</group>
|
||||
<group>Coworkers</group>
|
||||
</item>
|
||||
<item jid="otheruser@example.com" name="Other User"
|
||||
subscription="both" />
|
||||
</query>
|
||||
</iq>
|
||||
"""
|
||||
iq = self.Iq(ET.fromstring(xml_string))
|
||||
del iq['roster']['items']
|
||||
self.check(iq, """
|
||||
<iq>
|
||||
<query xmlns="jabber:iq:roster" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas)
|
||||
115
tests/test_stanza_xep_0004.py
Normal file
115
tests/test_stanza_xep_0004.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0004 as xep_0004
|
||||
|
||||
|
||||
class TestDataForms(SleekTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Message, xep_0004.Form)
|
||||
register_stanza_plugin(xep_0004.Form, xep_0004.FormField)
|
||||
register_stanza_plugin(xep_0004.FormField, xep_0004.FieldOption)
|
||||
|
||||
def testMultipleInstructions(self):
|
||||
"""Testing using multiple instructions elements in a data form."""
|
||||
msg = self.Message()
|
||||
msg['form']['instructions'] = "Instructions\nSecond batch"
|
||||
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<instructions>Instructions</instructions>
|
||||
<instructions>Second batch</instructions>
|
||||
</x>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testAddField(self):
|
||||
"""Testing adding fields to a data form."""
|
||||
|
||||
msg = self.Message()
|
||||
form = msg['form']
|
||||
form.addField(var='f1',
|
||||
ftype='text-single',
|
||||
label='Text',
|
||||
desc='A text field',
|
||||
required=True,
|
||||
value='Some text!')
|
||||
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="f1" type="text-single" label="Text">
|
||||
<desc>A text field</desc>
|
||||
<required />
|
||||
<value>Some text!</value>
|
||||
</field>
|
||||
</x>
|
||||
</message>
|
||||
""")
|
||||
|
||||
form['fields'] = [('f1', {'type': 'text-single',
|
||||
'label': 'Username',
|
||||
'required': True}),
|
||||
('f2', {'type': 'text-private',
|
||||
'label': 'Password',
|
||||
'required': True}),
|
||||
('f3', {'type': 'text-multi',
|
||||
'label': 'Message',
|
||||
'value': 'Enter message.\nA long one even.'}),
|
||||
('f4', {'type': 'list-single',
|
||||
'label': 'Message Type',
|
||||
'options': [{'label': 'Cool!',
|
||||
'value': 'cool'},
|
||||
{'label': 'Urgh!',
|
||||
'value': 'urgh'}]})]
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="f1" type="text-single" label="Username">
|
||||
<required />
|
||||
</field>
|
||||
<field var="f2" type="text-private" label="Password">
|
||||
<required />
|
||||
</field>
|
||||
<field var="f3" type="text-multi" label="Message">
|
||||
<value>Enter message.</value>
|
||||
<value>A long one even.</value>
|
||||
</field>
|
||||
<field var="f4" type="list-single" label="Message Type">
|
||||
<option label="Cool!">
|
||||
<value>cool</value>
|
||||
</option>
|
||||
<option label="Urgh!">
|
||||
<value>urgh</value>
|
||||
</option>
|
||||
</field>
|
||||
</x>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testSetValues(self):
|
||||
"""Testing setting form values"""
|
||||
|
||||
msg = self.Message()
|
||||
form = msg['form']
|
||||
form.setFields([
|
||||
('foo', {'type': 'text-single'}),
|
||||
('bar', {'type': 'list-multi'})])
|
||||
|
||||
form.setValues({'foo': 'Foo!',
|
||||
'bar': ['a', 'b']})
|
||||
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="foo" type="text-single">
|
||||
<value>Foo!</value>
|
||||
</field>
|
||||
<field var="bar" type="list-multi">
|
||||
<value>a</value>
|
||||
<value>b</value>
|
||||
</field>
|
||||
</x>
|
||||
</message>""")
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms)
|
||||
176
tests/test_stanza_xep_0030.py
Normal file
176
tests/test_stanza_xep_0030.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0030 as xep_0030
|
||||
|
||||
|
||||
class TestDisco(SleekTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Iq, xep_0030.DiscoInfo)
|
||||
register_stanza_plugin(Iq, xep_0030.DiscoItems)
|
||||
|
||||
def testCreateInfoQueryNoNode(self):
|
||||
"""Testing disco#info query with no node."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = ''
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#info" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testCreateInfoQueryWithNode(self):
|
||||
"""Testing disco#info query with a node."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#info" node="foo" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testCreateInfoQueryNoNode(self):
|
||||
"""Testing disco#items query with no node."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = ''
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testCreateItemsQueryWithNode(self):
|
||||
"""Testing disco#items query with a node."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = 'foo'
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" node="foo" />
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testInfoIdentities(self):
|
||||
"""Testing adding identities to disco#info."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#info" node="foo">
|
||||
<identity category="conference" type="text" name="Chatroom" />
|
||||
</query>
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testInfoFeatures(self):
|
||||
"""Testing adding features to disco#info."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
iq['disco_info'].addFeature('foo')
|
||||
iq['disco_info'].addFeature('bar')
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#info" node="foo">
|
||||
<feature var="foo" />
|
||||
<feature var="bar" />
|
||||
</query>
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testItems(self):
|
||||
"""Testing adding features to disco#info."""
|
||||
iq = self.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = 'foo'
|
||||
iq['disco_items'].addItem('user@localhost')
|
||||
iq['disco_items'].addItem('user@localhost', 'foo')
|
||||
iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<query xmlns="http://jabber.org/protocol/disco#items" node="foo">
|
||||
<item jid="user@localhost" />
|
||||
<item node="foo" jid="user@localhost" />
|
||||
<item node="bar" jid="user@localhost" name="Testing" />
|
||||
</query>
|
||||
</iq>
|
||||
""")
|
||||
|
||||
def testAddRemoveIdentities(self):
|
||||
"""Test adding and removing identities to disco#info stanza"""
|
||||
ids = [('automation', 'commands', 'AdHoc'),
|
||||
('conference', 'text', 'ChatRoom')]
|
||||
|
||||
info = xep_0030.DiscoInfo()
|
||||
info.addIdentity(*ids[0])
|
||||
self.failUnless(info.getIdentities() == [ids[0]])
|
||||
|
||||
info.delIdentity('automation', 'commands')
|
||||
self.failUnless(info.getIdentities() == [])
|
||||
|
||||
info.setIdentities(ids)
|
||||
self.failUnless(info.getIdentities() == ids)
|
||||
|
||||
info.delIdentity('automation', 'commands')
|
||||
self.failUnless(info.getIdentities() == [ids[1]])
|
||||
|
||||
info.delIdentities()
|
||||
self.failUnless(info.getIdentities() == [])
|
||||
|
||||
def testAddRemoveFeatures(self):
|
||||
"""Test adding and removing features to disco#info stanza"""
|
||||
features = ['foo', 'bar', 'baz']
|
||||
|
||||
info = xep_0030.DiscoInfo()
|
||||
info.addFeature(features[0])
|
||||
self.failUnless(info.getFeatures() == [features[0]])
|
||||
|
||||
info.delFeature('foo')
|
||||
self.failUnless(info.getFeatures() == [])
|
||||
|
||||
info.setFeatures(features)
|
||||
self.failUnless(info.getFeatures() == features)
|
||||
|
||||
info.delFeature('bar')
|
||||
self.failUnless(info.getFeatures() == ['foo', 'baz'])
|
||||
|
||||
info.delFeatures()
|
||||
self.failUnless(info.getFeatures() == [])
|
||||
|
||||
def testAddRemoveItems(self):
|
||||
"""Test adding and removing items to disco#items stanza"""
|
||||
items = [('user@localhost', None, None),
|
||||
('user@localhost', 'foo', None),
|
||||
('user@localhost', 'bar', 'Test')]
|
||||
|
||||
info = xep_0030.DiscoItems()
|
||||
self.failUnless(True, ""+str(items[0]))
|
||||
|
||||
info.addItem(*(items[0]))
|
||||
self.failUnless(info.getItems() == [items[0]], info.getItems())
|
||||
|
||||
info.delItem('user@localhost')
|
||||
self.failUnless(info.getItems() == [])
|
||||
|
||||
info.setItems(items)
|
||||
self.failUnless(info.getItems() == items)
|
||||
|
||||
info.delItem('user@localhost', 'foo')
|
||||
self.failUnless(info.getItems() == [items[0], items[2]])
|
||||
|
||||
info.delItems()
|
||||
self.failUnless(info.getItems() == [])
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco)
|
||||
111
tests/test_stanza_xep_0033.py
Normal file
111
tests/test_stanza_xep_0033.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0033 as xep_0033
|
||||
|
||||
|
||||
class TestAddresses(SleekTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Message, xep_0033.Addresses)
|
||||
|
||||
def testAddAddress(self):
|
||||
"""Testing adding extended stanza address."""
|
||||
msg = self.Message()
|
||||
msg['addresses'].addAddress(atype='to', jid='to@header1.org')
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<addresses xmlns="http://jabber.org/protocol/address">
|
||||
<address jid="to@header1.org" type="to" />
|
||||
</addresses>
|
||||
</message>
|
||||
""")
|
||||
|
||||
msg = self.Message()
|
||||
msg['addresses'].addAddress(atype='replyto',
|
||||
jid='replyto@header1.org',
|
||||
desc='Reply address')
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<addresses xmlns="http://jabber.org/protocol/address">
|
||||
<address jid="replyto@header1.org" type="replyto" desc="Reply address" />
|
||||
</addresses>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testAddAddresses(self):
|
||||
"""Testing adding multiple extended stanza addresses."""
|
||||
|
||||
xmlstring = """
|
||||
<message>
|
||||
<addresses xmlns="http://jabber.org/protocol/address">
|
||||
<address jid="replyto@header1.org" type="replyto" desc="Reply address" />
|
||||
<address jid="cc@header2.org" type="cc" />
|
||||
<address jid="bcc@header2.org" type="bcc" />
|
||||
</addresses>
|
||||
</message>
|
||||
"""
|
||||
|
||||
msg = self.Message()
|
||||
msg['addresses'].setAddresses([
|
||||
{'type':'replyto',
|
||||
'jid':'replyto@header1.org',
|
||||
'desc':'Reply address'},
|
||||
{'type':'cc',
|
||||
'jid':'cc@header2.org'},
|
||||
{'type':'bcc',
|
||||
'jid':'bcc@header2.org'}])
|
||||
self.check(msg, xmlstring)
|
||||
|
||||
msg = self.Message()
|
||||
msg['addresses']['replyto'] = [{'jid':'replyto@header1.org',
|
||||
'desc':'Reply address'}]
|
||||
msg['addresses']['cc'] = [{'jid':'cc@header2.org'}]
|
||||
msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}]
|
||||
self.check(msg, xmlstring)
|
||||
|
||||
def testAddURI(self):
|
||||
"""Testing adding URI attribute to extended stanza address."""
|
||||
|
||||
msg = self.Message()
|
||||
addr = msg['addresses'].addAddress(atype='to',
|
||||
jid='to@header1.org',
|
||||
node='foo')
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<addresses xmlns="http://jabber.org/protocol/address">
|
||||
<address node="foo" jid="to@header1.org" type="to" />
|
||||
</addresses>
|
||||
</message>
|
||||
""")
|
||||
|
||||
addr['uri'] = 'mailto:to@header2.org'
|
||||
self.check(msg, """
|
||||
<message>
|
||||
<addresses xmlns="http://jabber.org/protocol/address">
|
||||
<address type="to" uri="mailto:to@header2.org" />
|
||||
</addresses>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testDelivered(self):
|
||||
"""Testing delivered attribute of extended stanza addresses."""
|
||||
|
||||
xmlstring = """
|
||||
<message>
|
||||
<addresses xmlns="http://jabber.org/protocol/address">
|
||||
<address %s jid="to@header1.org" type="to" />
|
||||
</addresses>
|
||||
</message>
|
||||
"""
|
||||
|
||||
msg = self.Message()
|
||||
addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to')
|
||||
self.check(msg, xmlstring % '')
|
||||
|
||||
addr['delivered'] = True
|
||||
self.check(msg, xmlstring % 'delivered="true"')
|
||||
|
||||
addr['delivered'] = False
|
||||
self.check(msg, xmlstring % '')
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses)
|
||||
511
tests/test_stanza_xep_0060.py
Normal file
511
tests/test_stanza_xep_0060.py
Normal file
@@ -0,0 +1,511 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0004 as xep_0004
|
||||
import sleekxmpp.plugins.stanza_pubsub as pubsub
|
||||
|
||||
|
||||
class TestPubsubStanzas(SleekTest):
|
||||
|
||||
def testAffiliations(self):
|
||||
"Testing iq/pubsub/affiliations/affiliation stanzas"
|
||||
iq = self.Iq()
|
||||
aff1 = pubsub.Affiliation()
|
||||
aff1['node'] = 'testnode'
|
||||
aff1['affiliation'] = 'owner'
|
||||
aff2 = pubsub.Affiliation()
|
||||
aff2['node'] = 'testnode2'
|
||||
aff2['affiliation'] = 'publisher'
|
||||
iq['pubsub']['affiliations'].append(aff1)
|
||||
iq['pubsub']['affiliations'].append(aff2)
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<affiliations>
|
||||
<affiliation node="testnode" affiliation="owner" />
|
||||
<affiliation node="testnode2" affiliation="publisher" />
|
||||
</affiliations>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testSubscriptions(self):
|
||||
"Testing iq/pubsub/subscriptions/subscription stanzas"
|
||||
iq = self.Iq()
|
||||
sub1 = pubsub.Subscription()
|
||||
sub1['node'] = 'testnode'
|
||||
sub1['jid'] = 'steve@myserver.tld/someresource'
|
||||
sub2 = pubsub.Subscription()
|
||||
sub2['node'] = 'testnode2'
|
||||
sub2['jid'] = 'boogers@bork.top/bill'
|
||||
sub2['subscription'] = 'subscribed'
|
||||
iq['pubsub']['subscriptions'].append(sub1)
|
||||
iq['pubsub']['subscriptions'].append(sub2)
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscriptions>
|
||||
<subscription node="testnode" jid="steve@myserver.tld/someresource" />
|
||||
<subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" />
|
||||
</subscriptions>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testOptionalSettings(self):
|
||||
"Testing iq/pubsub/subscription/subscribe-options stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub']['subscription']['suboptions']['required'] = True
|
||||
iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas'
|
||||
iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp"
|
||||
iq['pubsub']['subscription']['subscription'] = 'unconfigured'
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured">
|
||||
<subscribe-options>
|
||||
<required />
|
||||
</subscribe-options>
|
||||
</subscription>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testItems(self):
|
||||
"Testing iq/pubsub/items stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub']['items']['node'] = 'crap'
|
||||
payload = ET.fromstring("""
|
||||
<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
|
||||
<child1 />
|
||||
<child2 normandy='cheese' foo='bar' />
|
||||
</thinger>""")
|
||||
payload2 = ET.fromstring("""
|
||||
<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
|
||||
<child12 />
|
||||
<child22 normandy='cheese2' foo='bar2' />
|
||||
</thinger2>""")
|
||||
item = pubsub.Item()
|
||||
item['id'] = 'asdf'
|
||||
item['payload'] = payload
|
||||
item2 = pubsub.Item()
|
||||
item2['id'] = 'asdf2'
|
||||
item2['payload'] = payload2
|
||||
iq['pubsub']['items'].append(item)
|
||||
iq['pubsub']['items'].append(item2)
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<items node="crap">
|
||||
<item id="asdf">
|
||||
<thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
|
||||
<child1 />
|
||||
<child2 foo="bar" normandy="cheese" />
|
||||
</thinger>
|
||||
</item>
|
||||
<item id="asdf2">
|
||||
<thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
|
||||
<child12 />
|
||||
<child22 foo="bar2" normandy="cheese2" />
|
||||
</thinger2>
|
||||
</item>
|
||||
</items>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testCreate(self):
|
||||
"Testing iq/pubsub/create&configure stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub']['create']['node'] = 'mynode'
|
||||
iq['pubsub']['configure']['form'].addField('pubsub#title',
|
||||
ftype='text-single',
|
||||
value='This thing is awesome')
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<create node="mynode" />
|
||||
<configure>
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="pubsub#title" type="text-single">
|
||||
<value>This thing is awesome</value>
|
||||
</field>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testState(self):
|
||||
"Testing iq/psstate stanzas"
|
||||
iq = self.Iq()
|
||||
iq['psstate']['node']= 'mynode'
|
||||
iq['psstate']['item']= 'myitem'
|
||||
pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
|
||||
iq['psstate']['payload'] = pl
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem">
|
||||
<claimed xmlns="http://andyet.net/protocol/pubsubqueue" />
|
||||
</state>
|
||||
</iq>""")
|
||||
|
||||
def testDefault(self):
|
||||
"Testing iq/pubsub_owner/default stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub_owner']['default']
|
||||
iq['pubsub_owner']['default']['node'] = 'mynode'
|
||||
iq['pubsub_owner']['default']['type'] = 'leaf'
|
||||
iq['pubsub_owner']['default']['form'].addField('pubsub#title',
|
||||
ftype='text-single',
|
||||
value='This thing is awesome')
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<default node="mynode" type="leaf">
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="pubsub#title" type="text-single">
|
||||
<value>This thing is awesome</value>
|
||||
</field>
|
||||
</x>
|
||||
</default>
|
||||
</pubsub>
|
||||
</iq>""", use_values=False)
|
||||
|
||||
def testSubscribe(self):
|
||||
"testing iq/pubsub/subscribe stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub']['subscribe']['options']
|
||||
iq['pubsub']['subscribe']['node'] = 'cheese'
|
||||
iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp'
|
||||
iq['pubsub']['subscribe']['options']['node'] = 'cheese'
|
||||
iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp'
|
||||
form = xep_0004.Form()
|
||||
form.addField('pubsub#title', ftype='text-single', value='this thing is awesome')
|
||||
iq['pubsub']['subscribe']['options']['options'] = form
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
|
||||
<options node="cheese" jid="fritzy@netflint.net/sleekxmpp">
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="pubsub#title" type="text-single">
|
||||
<value>this thing is awesome</value>
|
||||
</field>
|
||||
</x>
|
||||
</options>
|
||||
</subscribe>
|
||||
</pubsub>
|
||||
</iq>""", use_values=False)
|
||||
|
||||
def testPublish(self):
|
||||
"Testing iq/pubsub/publish stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub']['publish']['node'] = 'thingers'
|
||||
payload = ET.fromstring("""
|
||||
<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
|
||||
<child1 />
|
||||
<child2 normandy='cheese' foo='bar' />
|
||||
</thinger>""")
|
||||
payload2 = ET.fromstring("""
|
||||
<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
|
||||
<child12 />
|
||||
<child22 normandy='cheese2' foo='bar2' />
|
||||
</thinger2>""")
|
||||
item = pubsub.Item()
|
||||
item['id'] = 'asdf'
|
||||
item['payload'] = payload
|
||||
item2 = pubsub.Item()
|
||||
item2['id'] = 'asdf2'
|
||||
item2['payload'] = payload2
|
||||
iq['pubsub']['publish'].append(item)
|
||||
iq['pubsub']['publish'].append(item2)
|
||||
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<publish node="thingers">
|
||||
<item id="asdf">
|
||||
<thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
|
||||
<child1 />
|
||||
<child2 foo="bar" normandy="cheese" />
|
||||
</thinger>
|
||||
</item>
|
||||
<item id="asdf2">
|
||||
<thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
|
||||
<child12 />
|
||||
<child22 foo="bar2" normandy="cheese2" />
|
||||
</thinger2>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testDelete(self):
|
||||
"Testing iq/pubsub_owner/delete stanzas"
|
||||
iq = self.Iq()
|
||||
iq['pubsub_owner']['delete']['node'] = 'thingers'
|
||||
self.check(iq, """
|
||||
<iq id="0">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<delete node="thingers" />
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testCreateConfigGet(self):
|
||||
"""Testing getting config from full create"""
|
||||
iq = self.Iq()
|
||||
iq['to'] = 'pubsub.asdf'
|
||||
iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7'
|
||||
iq['type'] = 'set'
|
||||
iq['id'] = 'E'
|
||||
|
||||
pub = iq['pubsub']
|
||||
pub['create']['node'] = 'testnode2'
|
||||
pub['configure']['form']['type'] = 'submit'
|
||||
pub['configure']['form'].setFields([
|
||||
('FORM_TYPE', {'type': 'hidden',
|
||||
'value': 'http://jabber.org/protocol/pubsub#node_config'}),
|
||||
('pubsub#node_type', {'type': 'list-single',
|
||||
'label': 'Select the node type',
|
||||
'value': 'leaf'}),
|
||||
('pubsub#title', {'type': 'text-single',
|
||||
'label': 'A friendly name for the node'}),
|
||||
('pubsub#deliver_notifications', {'type': 'boolean',
|
||||
'label': 'Deliver event notifications',
|
||||
'value': True}),
|
||||
('pubsub#deliver_payloads', {'type': 'boolean',
|
||||
'label': 'Deliver payloads with event notifications',
|
||||
'value': True}),
|
||||
('pubsub#notify_config', {'type': 'boolean',
|
||||
'label': 'Notify subscribers when the node configuration changes'}),
|
||||
('pubsub#notify_delete', {'type': 'boolean',
|
||||
'label': 'Notify subscribers when the node is deleted'}),
|
||||
('pubsub#notify_retract', {'type': 'boolean',
|
||||
'label': 'Notify subscribers when items are removed from the node',
|
||||
'value': True}),
|
||||
('pubsub#notify_sub', {'type': 'boolean',
|
||||
'label': 'Notify owners about new subscribers and unsubscribes'}),
|
||||
('pubsub#persist_items', {'type': 'boolean',
|
||||
'label': 'Persist items in storage'}),
|
||||
('pubsub#max_items', {'type': 'text-single',
|
||||
'label': 'Max # of items to persist',
|
||||
'value': '10'}),
|
||||
('pubsub#subscribe', {'type': 'boolean',
|
||||
'label': 'Whether to allow subscriptions',
|
||||
'value': True}),
|
||||
('pubsub#access_model', {'type': 'list-single',
|
||||
'label': 'Specify the subscriber model',
|
||||
'value': 'open'}),
|
||||
('pubsub#publish_model', {'type': 'list-single',
|
||||
'label': 'Specify the publisher model',
|
||||
'value': 'publishers'}),
|
||||
('pubsub#send_last_published_item', {'type': 'list-single',
|
||||
'label': 'Send last published item',
|
||||
'value': 'never'}),
|
||||
('pubsub#presence_based_delivery', {'type': 'boolean',
|
||||
'label': 'Deliver notification only to available users'}),
|
||||
])
|
||||
|
||||
self.check(iq, """
|
||||
<iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<create node="testnode2" />
|
||||
<configure>
|
||||
<x xmlns="jabber:x:data" type="submit">
|
||||
<field var="FORM_TYPE" type="hidden">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field var="pubsub#node_type" type="list-single" label="Select the node type">
|
||||
<value>leaf</value>
|
||||
</field>
|
||||
<field var="pubsub#title" type="text-single" label="A friendly name for the node" />
|
||||
<field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" />
|
||||
<field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" />
|
||||
<field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" />
|
||||
<field var="pubsub#persist_items" type="boolean" label="Persist items in storage" />
|
||||
<field var="pubsub#max_items" type="text-single" label="Max # of items to persist">
|
||||
<value>10</value>
|
||||
</field>
|
||||
<field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#access_model" type="list-single" label="Specify the subscriber model">
|
||||
<value>open</value>
|
||||
</field>
|
||||
<field var="pubsub#publish_model" type="list-single" label="Specify the publisher model">
|
||||
<value>publishers</value>
|
||||
</field>
|
||||
<field var="pubsub#send_last_published_item" type="list-single" label="Send last published item">
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" />
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>""")
|
||||
|
||||
def testItemEvent(self):
|
||||
"""Testing message/pubsub_event/items/item"""
|
||||
msg = self.Message()
|
||||
item = pubsub.EventItem()
|
||||
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
|
||||
item['payload'] = pl
|
||||
item['id'] = 'abc123'
|
||||
msg['pubsub_event']['items'].append(item)
|
||||
msg['pubsub_event']['items']['node'] = 'cheese'
|
||||
msg['type'] = 'normal'
|
||||
self.check(msg, """
|
||||
<message type="normal">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<items node="cheese">
|
||||
<item id="abc123">
|
||||
<test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
|
||||
</item>
|
||||
</items>
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testItemsEvent(self):
|
||||
"""Testing multiple message/pubsub_event/items/item"""
|
||||
msg = self.Message()
|
||||
item = pubsub.EventItem()
|
||||
item2 = pubsub.EventItem()
|
||||
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
|
||||
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
|
||||
item2['payload'] = pl2
|
||||
item['payload'] = pl
|
||||
item['id'] = 'abc123'
|
||||
item2['id'] = '123abc'
|
||||
msg['pubsub_event']['items'].append(item)
|
||||
msg['pubsub_event']['items'].append(item2)
|
||||
msg['pubsub_event']['items']['node'] = 'cheese'
|
||||
msg['type'] = 'normal'
|
||||
self.check(msg, """
|
||||
<message type="normal">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<items node="cheese">
|
||||
<item id="abc123">
|
||||
<test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
|
||||
</item>
|
||||
<item id="123abc">
|
||||
<test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
|
||||
</item>
|
||||
</items>
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testItemsEvent(self):
|
||||
"""Testing message/pubsub_event/items/item & retract mix"""
|
||||
msg = self.Message()
|
||||
item = pubsub.EventItem()
|
||||
item2 = pubsub.EventItem()
|
||||
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
|
||||
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
|
||||
item2['payload'] = pl2
|
||||
retract = pubsub.EventRetract()
|
||||
retract['id'] = 'aabbcc'
|
||||
item['payload'] = pl
|
||||
item['id'] = 'abc123'
|
||||
item2['id'] = '123abc'
|
||||
msg['pubsub_event']['items'].append(item)
|
||||
msg['pubsub_event']['items'].append(retract)
|
||||
msg['pubsub_event']['items'].append(item2)
|
||||
msg['pubsub_event']['items']['node'] = 'cheese'
|
||||
msg['type'] = 'normal'
|
||||
self.check(msg, """
|
||||
<message type="normal">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<items node="cheese">
|
||||
<item id="abc123">
|
||||
<test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
|
||||
</item><retract id="aabbcc" />
|
||||
<item id="123abc">
|
||||
<test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
|
||||
</item>
|
||||
</items>
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testCollectionAssociate(self):
|
||||
"""Testing message/pubsub_event/collection/associate"""
|
||||
msg = self.Message()
|
||||
msg['pubsub_event']['collection']['associate']['node'] = 'cheese'
|
||||
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
|
||||
msg['type'] = 'headline'
|
||||
self.check(msg, """
|
||||
<message type="headline">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<collection node="cheeseburger">
|
||||
<associate node="cheese" />
|
||||
</collection>
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testCollectionDisassociate(self):
|
||||
"""Testing message/pubsub_event/collection/disassociate"""
|
||||
msg = self.Message()
|
||||
msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese'
|
||||
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
|
||||
msg['type'] = 'headline'
|
||||
self.check(msg, """
|
||||
<message type="headline">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<collection node="cheeseburger">
|
||||
<disassociate node="cheese" />
|
||||
</collection>
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testEventConfiguration(self):
|
||||
"""Testing message/pubsub_event/configuration/config"""
|
||||
msg = self.Message()
|
||||
msg['pubsub_event']['configuration']['node'] = 'cheese'
|
||||
msg['pubsub_event']['configuration']['form'].addField('pubsub#title',
|
||||
ftype='text-single',
|
||||
value='This thing is awesome')
|
||||
msg['type'] = 'headline'
|
||||
self.check(msg, """
|
||||
<message type="headline">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<configuration node="cheese">
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="pubsub#title" type="text-single">
|
||||
<value>This thing is awesome</value>
|
||||
</field>
|
||||
</x>
|
||||
</configuration>
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testEventPurge(self):
|
||||
"""Testing message/pubsub_event/purge"""
|
||||
msg = self.Message()
|
||||
msg['pubsub_event']['purge']['node'] = 'pickles'
|
||||
msg['type'] = 'headline'
|
||||
self.check(msg, """
|
||||
<message type="headline">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<purge node="pickles" />
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
def testEventSubscription(self):
|
||||
"""Testing message/pubsub_event/subscription"""
|
||||
msg = self.Message()
|
||||
msg['pubsub_event']['subscription']['node'] = 'pickles'
|
||||
msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test'
|
||||
msg['pubsub_event']['subscription']['subid'] = 'aabb1122'
|
||||
msg['pubsub_event']['subscription']['subscription'] = 'subscribed'
|
||||
msg['pubsub_event']['subscription']['expiry'] = 'presence'
|
||||
msg['type'] = 'headline'
|
||||
self.check(msg, """
|
||||
<message type="headline">
|
||||
<event xmlns="http://jabber.org/protocol/pubsub#event">
|
||||
<subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" />
|
||||
</event>
|
||||
</message>""")
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas)
|
||||
44
tests/test_stanza_xep_0085.py
Normal file
44
tests/test_stanza_xep_0085.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0085 as xep_0085
|
||||
|
||||
class TestChatStates(SleekTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Message, xep_0085.Active)
|
||||
register_stanza_plugin(Message, xep_0085.Composing)
|
||||
register_stanza_plugin(Message, xep_0085.Gone)
|
||||
register_stanza_plugin(Message, xep_0085.Inactive)
|
||||
register_stanza_plugin(Message, xep_0085.Paused)
|
||||
|
||||
def testCreateChatState(self):
|
||||
"""Testing creating chat states."""
|
||||
|
||||
xmlstring = """
|
||||
<message>
|
||||
<%s xmlns="http://jabber.org/protocol/chatstates" />
|
||||
</message>
|
||||
"""
|
||||
|
||||
msg = self.Message()
|
||||
msg['chat_state'].active()
|
||||
self.check(msg, xmlstring % 'active',
|
||||
use_values=False)
|
||||
|
||||
msg['chat_state'].composing()
|
||||
self.check(msg, xmlstring % 'composing',
|
||||
use_values=False)
|
||||
|
||||
|
||||
msg['chat_state'].gone()
|
||||
self.check(msg, xmlstring % 'gone',
|
||||
use_values=False)
|
||||
|
||||
msg['chat_state'].inactive()
|
||||
self.check(msg, xmlstring % 'inactive',
|
||||
use_values=False)
|
||||
|
||||
msg['chat_state'].paused()
|
||||
self.check(msg, xmlstring % 'paused',
|
||||
use_values=False)
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates)
|
||||
@@ -1,261 +0,0 @@
|
||||
import unittest
|
||||
import time, threading, random, functools
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys, os
|
||||
sys.path.insert(0, os.getcwd())
|
||||
import sleekxmpp.xmlstream.statemachine as sm
|
||||
|
||||
|
||||
class testStateMachine(unittest.TestCase):
|
||||
|
||||
def setUp(self): pass
|
||||
|
||||
|
||||
def testDefaults(self):
|
||||
"Test ensure transitions occur correctly in a single thread"
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
self.assertTrue(s['one'])
|
||||
self.failIf(s['two'])
|
||||
try:
|
||||
s['booga']
|
||||
self.fail('s.booga is an invalid state and should throw an exception!')
|
||||
except: pass #expected exception
|
||||
|
||||
# just make sure __str__ works, no reason to test its exact value:
|
||||
print str(s)
|
||||
|
||||
|
||||
def testTransitions(self):
|
||||
"Test ensure transitions occur correctly in a single thread"
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
|
||||
self.assertTrue( s.transition('one', 'two') )
|
||||
self.assertTrue( s['two'] )
|
||||
self.failIf( s['one'] )
|
||||
|
||||
self.assertTrue( s.transition('two', 'three') )
|
||||
self.assertTrue( s['three'] )
|
||||
self.failIf( s['two'] )
|
||||
|
||||
self.assertTrue( s.transition('three', 'one') )
|
||||
self.assertTrue( s['one'] )
|
||||
self.failIf( s['three'] )
|
||||
|
||||
# should return False immediately w/ no wait:
|
||||
self.failIf( s.transition('three', 'one') )
|
||||
self.assertTrue( s['one'] )
|
||||
self.failIf( s['three'] )
|
||||
|
||||
# test fail condition w/ a short delay:
|
||||
self.failIf( s.transition('two', 'three') )
|
||||
|
||||
# Ensure bad states are weeded out:
|
||||
try:
|
||||
s.transition('blah', 'three')
|
||||
s.fail('Exception expected')
|
||||
except: pass
|
||||
|
||||
try:
|
||||
s.transition('one', 'blahblah')
|
||||
s.fail('Exception expected')
|
||||
except: pass
|
||||
|
||||
|
||||
def testTransitionsBlocking(self):
|
||||
"Test that transitions block from more than one thread"
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
self.assertTrue(s['one'])
|
||||
|
||||
now = time.time()
|
||||
self.failIf( s.transition('two', 'one', wait=5.0) )
|
||||
self.assertTrue( time.time() > now + 4 )
|
||||
self.assertTrue( time.time() < now + 7 )
|
||||
|
||||
def testThreadedTransitions(self):
|
||||
"Test that transitions are atomic in > one thread"
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
self.assertTrue(s['one'])
|
||||
|
||||
thread_state = {'ready': False, 'transitioned': False}
|
||||
def t1():
|
||||
if s['two']:
|
||||
print 'thread has already transitioned!'
|
||||
self.fail()
|
||||
thread_state['ready'] = True
|
||||
print 'Thread is ready'
|
||||
# this will block until the main thread transitions to 'two'
|
||||
self.assertTrue( s.transition('two','three', wait=20) )
|
||||
print 'transitioned to three!'
|
||||
thread_state['transitioned'] = True
|
||||
|
||||
thread = threading.Thread(target=t1)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
start = time.time()
|
||||
while not thread_state['ready']:
|
||||
print 'not ready'
|
||||
if time.time() > start+10: self.fail('Timeout waiting for thread to init!')
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.2) # the thread should be blocking on the 'transition' call at this point.
|
||||
self.failIf( thread_state['transitioned'] ) # ensure it didn't 'go' yet.
|
||||
print 'transitioning to two!'
|
||||
self.assertTrue( s.transition('one','two') )
|
||||
time.sleep(0.2) # second thread should have transitioned now:
|
||||
self.assertTrue( thread_state['transitioned'] )
|
||||
|
||||
|
||||
def testForRaceCondition(self):
|
||||
"""Attempt to allow two threads to perform the same transition;
|
||||
only one should ever make it."""
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
|
||||
def t1(num):
|
||||
while True:
|
||||
if not trigger['go'] or thread_state[num] in (True,False):
|
||||
time.sleep( random.random()/100 ) # < .01s
|
||||
if thread_state[num] == 'quit': break
|
||||
continue
|
||||
|
||||
thread_state[num] = s.transition('one','two' )
|
||||
# print '-',
|
||||
|
||||
thread_count = 20
|
||||
threads = []
|
||||
thread_state = {}
|
||||
def reset():
|
||||
for c in range(thread_count): thread_state[c] = "reset"
|
||||
trigger = {'go':False} # use of a plain boolean seems to be non-volatile between threads.
|
||||
|
||||
for c in range(thread_count):
|
||||
thread_state[c] = "reset"
|
||||
thread = threading.Thread( target= functools.partial(t1,c) )
|
||||
threads.append( thread )
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
for x in range(100): # this will take 10s to execute
|
||||
# print "+",
|
||||
trigger['go'] = True
|
||||
time.sleep(.1)
|
||||
trigger['go'] = False
|
||||
winners = 0
|
||||
for (num, state) in thread_state.items():
|
||||
if state == True: winners = winners +1
|
||||
elif state != False: raise Exception( "!%d!%s!" % (num,state) )
|
||||
|
||||
self.assertEqual( 1, winners, "Expected one winner! %d" % winners )
|
||||
self.assertTrue( s.ensure('two') )
|
||||
self.assertTrue( s.transition('two','one') ) # return to the first state.
|
||||
reset()
|
||||
|
||||
# now let the threads quit gracefully:
|
||||
for c in range(thread_count): thread_state[c] = 'quit'
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def testTransitionFunctions(self):
|
||||
"test that a `func` argument allows or blocks the transition correctly."
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
|
||||
def alwaysFalse(): return False
|
||||
def alwaysTrue(): return True
|
||||
|
||||
self.failIf( s.transition('one','two', func=alwaysFalse) )
|
||||
self.assertTrue(s['one'])
|
||||
self.failIf(s['two'])
|
||||
|
||||
self.assertTrue( s.transition('one','two', func=alwaysTrue) )
|
||||
self.failIf(s['one'])
|
||||
self.assertTrue(s['two'])
|
||||
|
||||
|
||||
def testTransitionFuncException(self):
|
||||
"if a transition function throws an exeption, ensure we're in a sane state"
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
|
||||
def alwaysException(): raise Exception('whups!')
|
||||
|
||||
try:
|
||||
self.failIf( s.transition('one','two', func=alwaysException) )
|
||||
self.fail("exception should have been thrown")
|
||||
except: pass #expected exception
|
||||
|
||||
self.assertTrue(s['one'])
|
||||
self.failIf(s['two'])
|
||||
|
||||
# ensure a subsequent attempt completes normally:
|
||||
self.assertTrue( s.transition('one','two') )
|
||||
self.failIf(s['one'])
|
||||
self.assertTrue(s['two'])
|
||||
|
||||
|
||||
def testContextManager(self):
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
|
||||
with s.transition_ctx('one','two'):
|
||||
self.assertTrue( s['one'] )
|
||||
self.failIf( s['two'] )
|
||||
|
||||
#successful transition b/c no exception was thrown
|
||||
self.assertTrue( s['two'] )
|
||||
self.failIf( s['one'] )
|
||||
|
||||
# failed transition because exception is thrown:
|
||||
try:
|
||||
with s.transition_ctx('two','three'):
|
||||
raise Exception("boom!")
|
||||
self.fail('exception expected')
|
||||
except: pass
|
||||
|
||||
self.failIf( s.current_state() in ('one','three') )
|
||||
self.assertTrue( s['two'] )
|
||||
|
||||
def testCtxManagerTransitionFailure(self):
|
||||
|
||||
s = sm.StateMachine(('one','two','three'))
|
||||
|
||||
with s.transition_ctx('two','three') as result:
|
||||
self.failIf( result )
|
||||
self.assertTrue( s['one'] )
|
||||
self.failIf( s.current_state in ('two','three') )
|
||||
|
||||
self.assertTrue( s['one'] )
|
||||
|
||||
def r1():
|
||||
print 'thread 1 started'
|
||||
self.assertTrue( s.transition('one','two') )
|
||||
print 'thread 1 transitioned'
|
||||
|
||||
def r2():
|
||||
print 'thread 2 started'
|
||||
self.failIf( s['two'] )
|
||||
with s.transition_ctx('two','three', 10) as result:
|
||||
self.assertTrue( result )
|
||||
self.assertTrue( s['two'] )
|
||||
print 'thread 2 will transition on exit from the context manager...'
|
||||
self.assertTrue( s['three'] )
|
||||
print 'transitioned to %s' % s.current_state()
|
||||
|
||||
t1 = threading.Thread(target=r1)
|
||||
t2 = threading.Thread(target=r2)
|
||||
|
||||
t2.start() # this should block until r1 goes
|
||||
time.sleep(1)
|
||||
t1.start()
|
||||
|
||||
t1.join()
|
||||
t2.join()
|
||||
|
||||
self.assertTrue( s['three'] )
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testStateMachine)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
60
tests/test_stream.py
Normal file
60
tests/test_stream.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0033 as xep_0033
|
||||
|
||||
|
||||
class TestStreamTester(SleekTest):
|
||||
"""
|
||||
Test that we can simulate and test a stanza stream.
|
||||
"""
|
||||
|
||||
def tearDown(self):
|
||||
self.stream_close()
|
||||
|
||||
def testClientEcho(self):
|
||||
"""Test that we can interact with a ClientXMPP instance."""
|
||||
self.stream_start(mode='client')
|
||||
|
||||
def echo(msg):
|
||||
msg.reply('Thanks for sending: %(body)s' % msg).send()
|
||||
|
||||
self.xmpp.add_event_handler('message', echo)
|
||||
|
||||
self.recv("""
|
||||
<message to="tester@localhost" from="user@localhost">
|
||||
<body>Hi!</body>
|
||||
</message>
|
||||
""")
|
||||
|
||||
self.send("""
|
||||
<message to="user@localhost">
|
||||
<body>Thanks for sending: Hi!</body>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testComponentEcho(self):
|
||||
"""Test that we can interact with a ComponentXMPP instance."""
|
||||
self.stream_start(mode='component')
|
||||
|
||||
def echo(msg):
|
||||
msg.reply('Thanks for sending: %(body)s' % msg).send()
|
||||
|
||||
self.xmpp.add_event_handler('message', echo)
|
||||
|
||||
self.recv("""
|
||||
<message to="tester.localhost" from="user@localhost">
|
||||
<body>Hi!</body>
|
||||
</message>
|
||||
""")
|
||||
|
||||
self.send("""
|
||||
<message to="user@localhost" from="tester.localhost">
|
||||
<body>Thanks for sending: Hi!</body>
|
||||
</message>
|
||||
""")
|
||||
|
||||
def testSendStreamHeader(self):
|
||||
"""Test that we can check a sent stream header."""
|
||||
self.stream_start(mode='client', skip=False)
|
||||
self.send_header(sto='localhost')
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user