diff --git a/conf/jcl.conf b/conf/jcl.conf index bc8db2a..d30c8d8 100644 --- a/conf/jcl.conf +++ b/conf/jcl.conf @@ -24,3 +24,6 @@ pid_file: /var/run/jabber/jcl.pid welcome_message: "Welcome to JCL" admins: admin1@domain.com, admin2@domain.com log_file: /var/log/jabber/jcl.log + +[vcard] +url: http://people.happycoders.org/dax/projects/jcl diff --git a/src/jcl/jabber/component.py b/src/jcl/jabber/component.py index d06bcaa..061aebe 100644 --- a/src/jcl/jabber/component.py +++ b/src/jcl/jabber/component.py @@ -44,6 +44,7 @@ from pyxmpp.jabberd.component import Component from pyxmpp.message import Message from pyxmpp.presence import Presence from pyxmpp.jabber.dataforms import Form +import pyxmpp.jabber.vcard as vcard from jcl.error import FieldError from jcl.jabber.disco import AccountDiscoGetInfoHandler, \ @@ -61,6 +62,8 @@ from jcl.jabber.presence import AccountPresenceAvailableHandler, \ RootPresenceUnsubscribeHandler from jcl.jabber.register import RootSetRegisterHandler, \ AccountSetRegisterHandler, AccountTypeSetRegisterHandler +from jcl.jabber.vcard import DefaultVCardHandler + import jcl.model as model from jcl.model import account from jcl.model.account import Account, User @@ -639,6 +642,7 @@ class JCLComponent(Component, object): self.set_register_handlers = [[RootSetRegisterHandler(self), AccountSetRegisterHandler(self), AccountTypeSetRegisterHandler(self)]] + self.vcard_handlers = [[DefaultVCardHandler(self)]] self.__logger = logging.getLogger("jcl.jabber.JCLComponent") self.lang = lang @@ -729,10 +733,15 @@ class JCLComponent(Component, object): self.handle_get_gateway) self.stream.set_iq_set_handler("query", "jabber:iq:gateway", self.handle_set_gateway) + self.stream.set_iq_get_handler("query", "jabber:iq:last", + self.handle_get_last) self.stream.set_iq_set_handler("command", command.COMMAND_NS, self.handle_command) + self.stream.set_iq_get_handler("vCard", vcard.VCARD_NS, + self.handle_vcard) + self.stream.set_presence_handler("available", self.handle_presence_available) @@ -851,6 +860,13 @@ class JCLComponent(Component, object): self.send_stanzas(result) return result + def handle_get_last(self, info_query): + """ + Handle IQ-get "jabber:iq:last" requests. + """ + # TODO + return 1 + def handle_get_gateway(self, info_query): """Handle IQ-get "jabber:iq:gateway" requests. Return prompt and description. @@ -1056,6 +1072,14 @@ class JCLComponent(Component, object): exc_info=True) return 1 + def handle_vcard(self, info_query): + """ + Handle VCard request + """ + self.__logger.debug("VCard request") + self.apply_registered_behavior(self.vcard_handlers, info_query) + return 1 + ########################################################################### # Utils ########################################################################### diff --git a/src/jcl/jabber/disco.py b/src/jcl/jabber/disco.py index 71c0d1e..2ed9287 100644 --- a/src/jcl/jabber/disco.py +++ b/src/jcl/jabber/disco.py @@ -24,6 +24,7 @@ import logging from pyxmpp.jid import JID from pyxmpp.jabber.disco import DiscoInfo, DiscoItems, DiscoItem, DiscoIdentity +import pyxmpp.jabber.vcard as vcard import jcl.jabber as jabber @@ -56,6 +57,8 @@ class RootDiscoGetInfoHandler(DiscoHandler): disco_info.add_feature("jabber:iq:version") disco_info.add_feature("http://jabber.org/protocol/disco#info") disco_info.add_feature("http://jabber.org/protocol/disco#items") + disco_info.add_feature(vcard.VCARD_NS) + disco_info.add_feature("jabber:iq:last") if not self.component.account_manager.has_multiple_account_type: disco_info.add_feature("jabber:iq:register") DiscoIdentity(disco_info, self.component.name, @@ -75,6 +78,8 @@ class AccountDiscoGetInfoHandler(DiscoHandler): """Implement discovery get_info on an account node""" self.__logger.debug("account_disco_get_info") disco_info = DiscoInfo(node) + disco_info.add_feature(vcard.VCARD_NS) + disco_info.add_feature("jabber:iq:last") disco_info.add_feature("jabber:iq:register") return [disco_info] diff --git a/src/jcl/jabber/message.py b/src/jcl/jabber/message.py index 6587b7d..cc96234 100644 --- a/src/jcl/jabber/message.py +++ b/src/jcl/jabber/message.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ## ## message.py ## Login : David Rousselie diff --git a/src/jcl/jabber/tests/__init__.py b/src/jcl/jabber/tests/__init__.py index c05cd8a..b2d0eee 100644 --- a/src/jcl/jabber/tests/__init__.py +++ b/src/jcl/jabber/tests/__init__.py @@ -6,7 +6,7 @@ import unittest import jcl.jabber as jabber from jcl.jabber.tests import component, feeder, command, message, presence, \ - disco + disco, vcard class HandlerType1: pass @@ -38,6 +38,7 @@ def suite(): test_suite.addTest(message.suite()) test_suite.addTest(presence.suite()) test_suite.addTest(disco.suite()) + test_suite.addTest(vcard.suite()) return test_suite if __name__ == '__main__': diff --git a/src/jcl/jabber/tests/command.py b/src/jcl/jabber/tests/command.py index 6da72de..7fb898c 100644 --- a/src/jcl/jabber/tests/command.py +++ b/src/jcl/jabber/tests/command.py @@ -3217,7 +3217,7 @@ class JCLCommandManagerShutdownCommand_TestCase(JCLCommandManagerTestCase): self.assertTrue(self.comp.running) threads = threading.enumerate() self.assertEquals(len(threads), 2) - threading.Event().wait(1) + threading.Event().wait(2) threads = threading.enumerate() self.assertEquals(len(threads), 1) self.assertFalse(self.comp.restart) diff --git a/src/jcl/jabber/tests/component.py b/src/jcl/jabber/tests/component.py index 7b6f366..6628994 100644 --- a/src/jcl/jabber/tests/component.py +++ b/src/jcl/jabber/tests/component.py @@ -35,6 +35,7 @@ from pyxmpp.iq import Iq from pyxmpp.presence import Presence from pyxmpp.message import Message from pyxmpp.jabber.dataforms import Form +import pyxmpp.jabber.vcard as vcard import jcl.tests from jcl.jabber import Handler @@ -67,11 +68,13 @@ class MockStream(object): self.sent.append(iq) def set_iq_set_handler(self, iq_type, ns, handler): - if not iq_type in ["query", "command"]: + if not iq_type in ["query", "command", "vCard"]: raise Exception("IQ type unknown: " + iq_type) if not ns in ["jabber:iq:version", "jabber:iq:register", "jabber:iq:gateway", + "jabber:iq:last", + vcard.VCARD_NS, "http://jabber.org/protocol/disco#items", "http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/commands"]: @@ -503,6 +506,8 @@ class JCLComponent_TestCase(JCLTestCase): self.assertEquals(len(self.comp.stream.sent), 0) self.assertEquals(disco_info.get_identities()[0].get_name(), self.comp.name) self.assertTrue(disco_info.has_feature("jabber:iq:version")) + self.assertTrue(disco_info.has_feature(vcard.VCARD_NS)) + self.assertTrue(disco_info.has_feature("jabber:iq:last")) self.assertTrue(disco_info.has_feature("jabber:iq:register")) def test_disco_get_info_multiple_account_type(self): @@ -518,6 +523,8 @@ class JCLComponent_TestCase(JCLTestCase): self.assertEquals(disco_info.get_identities()[0].get_name(), self.comp.name) self.assertTrue(disco_info.has_feature("jabber:iq:version")) + self.assertTrue(disco_info.has_feature(vcard.VCARD_NS)) + self.assertTrue(disco_info.has_feature("jabber:iq:last")) self.assertFalse(disco_info.has_feature("jabber:iq:register")) def test_disco_get_info_node(self): @@ -529,6 +536,8 @@ class JCLComponent_TestCase(JCLTestCase): disco_info = self.comp.disco_get_info("node_test", info_query) self.assertEquals(disco_info.get_node(), "node_test") self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature(vcard.VCARD_NS)) + self.assertTrue(disco_info.has_feature("jabber:iq:last")) self.assertTrue(disco_info.has_feature("jabber:iq:register")) def test_disco_get_info_long_node(self): @@ -542,6 +551,8 @@ class JCLComponent_TestCase(JCLTestCase): info_query) self.assertEquals(disco_info.get_node(), "node_type/node_test") self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature(vcard.VCARD_NS)) + self.assertTrue(disco_info.has_feature("jabber:iq:last")) self.assertTrue(disco_info.has_feature("jabber:iq:register")) def test_disco_get_info_root_unknown_node(self): diff --git a/src/jcl/jabber/tests/disco.py b/src/jcl/jabber/tests/disco.py new file mode 100644 index 0000000..f210960 --- /dev/null +++ b/src/jcl/jabber/tests/disco.py @@ -0,0 +1,79 @@ +## +## disco.py +## Login : David Rousselie +## Started on Fri Jul 6 21:40:55 2007 David Rousselie +## $Id$ +## +## Copyright (C) 2007 David Rousselie +## This program 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. +## +## This program 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 this program; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import unittest + +from pyxmpp.message import Message + +from jcl.tests import JCLTestCase +from jcl.model.tests.account import ExampleAccount + +from jcl.model.account import User, Account +from jcl.jabber.component import JCLComponent +from jcl.jabber.disco import DiscoHandler, AccountTypeDiscoGetItemsHandler + +class DiscoHandler_TestCase(JCLTestCase): + def setUp(self): + JCLTestCase.setUp(self, tables=[User, Account, ExampleAccount]) + self.comp = JCLComponent("jcl.test.com", + "password", + "localhost", + "5347", + self.db_url) + self.handler = DiscoHandler(self.comp) + + def test_default_filter(self): + """Test default filter behavior""" + self.assertFalse(self.handler.filter(None, None, None)) + + def test_default_handler(self): + """Test default handler: do nothing""" + self.assertEquals(self.handler.handle(None, None, None, None, None), + None) + +class AccountTypeDiscoGetItemsHandler_TestCase (JCLTestCase): + """Test AccountTypeDiscoGetItemsHandler class""" + def setUp(self): + JCLTestCase.setUp(self, tables=[User, Account, ExampleAccount]) + self.comp = JCLComponent("jcl.test.com", + "password", + "localhost", + "5347", + self.db_url) + self.comp.account_manager.account_classes = (ExampleAccount,) + self.handler = AccountTypeDiscoGetItemsHandler(self.comp) + + def test_handler_unknown_account_type(self): + """Test handler with an unknown account type""" + self.assertEquals(self.handler.handle(Message(from_jid="from@test.com"), + None, None, None, + "Unknown"), []) + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(unittest.makeSuite(DiscoHandler_TestCase, 'test')) + test_suite.addTest(unittest.makeSuite(AccountTypeDiscoGetItemsHandler_TestCase, + 'test')) + return test_suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/src/jcl/jabber/tests/message.py b/src/jcl/jabber/tests/message.py index c8fd8c8..b63349c 100644 --- a/src/jcl/jabber/tests/message.py +++ b/src/jcl/jabber/tests/message.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ## ## message.py ## Login : David Rousselie diff --git a/src/jcl/jabber/tests/vcard.py b/src/jcl/jabber/tests/vcard.py new file mode 100644 index 0000000..ebb5c67 --- /dev/null +++ b/src/jcl/jabber/tests/vcard.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +## +## vcard.py +## Login : David Rousselie +## Started on Fri Jul 6 21:40:55 2007 David Rousselie +## $Id$ +## +## Copyright (C) 2007 David Rousselie +## This program 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. +## +## This program 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 this program; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import unittest +import logging +import sys +from ConfigParser import ConfigParser + +from pyxmpp.iq import Iq + +import jcl.tests +from jcl.lang import Lang +from jcl.jabber.component import JCLComponent +from jcl.model.account import Account, User +from jcl.jabber.vcard import DefaultVCardHandler + +from jcl.model.tests.account import ExampleAccount +from jcl.tests import JCLTestCase + +class DefaultVCardHandler_TestCase(JCLTestCase): + def setUp(self): + JCLTestCase.setUp(self, tables=[User, Account, ExampleAccount]) + self.comp = JCLComponent("jcl.test.com", + "password", + "localhost", + "5347", + self.db_url) + self.handler = DefaultVCardHandler(self.comp) + self.comp.config = ConfigParser() + self.comp.config.read("src/jcl/tests/jcl.conf") + + def test_filter(self): + """Test DefaultVCardHandler filter. Accept any stanza""" + self.assertEquals(self.handler.filter(None, None), True) + + def test_handle(self): + """Test default VCard returned""" + result = self.handler.handle(Iq(from_jid="jcl.test.com", + to_jid="user@test.com", + stanza_type="get"), + Lang.en, True) + self.assertEquals(len(result), 1) + result[0].xmlnode.setNs(None) + self.assertTrue(jcl.tests.is_xml_equal(\ + u"" + + "" + + "" + self.comp.config.get("vcard", "url") + "" + + "" + self.comp.name + "" + + "" + + "" + + "" + + "" + + "" + self.comp.name + "" + + "", + result[0].xmlnode, True)) + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(unittest.makeSuite(DefaultVCardHandler_TestCase, 'test')) + return test_suite + +if __name__ == '__main__': + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + if '-v' in sys.argv: + logger.setLevel(logging.INFO) + unittest.main(defaultTest='suite') diff --git a/src/jcl/jabber/vcard.py b/src/jcl/jabber/vcard.py new file mode 100644 index 0000000..4ed14b4 --- /dev/null +++ b/src/jcl/jabber/vcard.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +## +## vcard.py +## Login : David Rousselie +## Started on Wed Jun 20 08:19:57 2007 David Rousselie +## $Id$ +## +## Copyright (C) 2007 David Rousselie +## This program 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. +## +## This program 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 this program; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +from pyxmpp.jabber.vcard import VCard, VCardName, VCardString + +from jcl.jabber import Handler + +class DefaultVCardHandler(Handler): + """Default VCard Handler. Return a VCard for all requests""" + + def filter(self, stanza, lang_class): + """ + Do not filter any stanza + """ + return True + + def handle(self, stanza, lang_class, data): + """ + Return default VCard + """ + print "Handling VCard" + vcard_response = stanza.make_result_response() + vcard_rfc_tab = [u"BEGIN:VCARD", + VCardName("N", self.component.name).rfc2426()] + if self.component.config.has_section("vcard"): + config = self.component.config + vcard_rfc_tab += [VCardString(field.upper(), + unicode(config.get("vcard", + field))).rfc2426() + for field in config.options("vcard")] + vcard_rfc_tab += ["END:VCARD"] + vcard = VCard("\n".join(vcard_rfc_tab)) + ## Correct PyXMPP bug + vcard.xml_element_name = "vCard" + vcard.as_xml(vcard_response.xmlnode) + return [vcard_response] diff --git a/src/jcl/tests/__init__.py b/src/jcl/tests/__init__.py index 090792d..a224a5b 100644 --- a/src/jcl/tests/__init__.py +++ b/src/jcl/tests/__init__.py @@ -38,19 +38,30 @@ def is_xml_equal(xml_ref, xml_test, strict=False, __logger.info("Testing xml node equality:\n--\n" + str(xml_ref) + "\n--\n" + str(xml_test) + "\n--\n") if (xml_ref is None) ^ (xml_test is None): - if strict or xml_test is None: + if xml_test is None: __logger.error("xml_test (" + str(xml_test) + ") or xml_ref (" + str(xml_ref) + ") is None") return False + elif strict: + # libxml2 parser set content to None for empty node but + # pyxmpp parser set content to an empty string + if xml_test.type == "text" and xml_test.content == "": + return True + else: + __logger.error("xml_test (" + str(xml_test) + ") or xml_ref (" + + str(xml_ref) + ") is None") + return False else: return True if (xml_ref is None) and (xml_test is None): return True if isinstance(xml_ref, types.StringType) \ or isinstance(xml_ref, types.UnicodeType): + libxml2.pedanticParserDefault(True) xml_ref = libxml2.parseDoc(xml_ref).children if isinstance(xml_test, types.StringType) \ or isinstance(xml_test, types.UnicodeType): + libxml2.pedanticParserDefault(True) xml_test = libxml2.parseDoc(xml_test).children def check_equality(test_func, ref, test, strict): @@ -100,14 +111,14 @@ def is_xml_equal(xml_ref, xml_test, strict=False, return False for attr in ref.properties: if ref.prop(attr.name) != test.prop(attr.name): - __logger.error("XML node attributs are different: " + __logger.error("XML node attributs are different: " + str(attr) + " != " + str(test.prop(attr.name))) return False if strict_attribut: for attr in test.properties: if ref.prop(attr.name) != test.prop(attr.name): - __logger.error("XML node attributs are different: " + __logger.error("XML node attributs are different: " + str(attr) + " != " + str(ref.prop(attr.name))) return False diff --git a/src/jcl/tests/jcl.conf b/src/jcl/tests/jcl.conf index 5b49ad8..11e9282 100644 --- a/src/jcl/tests/jcl.conf +++ b/src/jcl/tests/jcl.conf @@ -19,3 +19,5 @@ db_url: %(type)s://%(host)s%(name)s pid_file: /var/run/jabber/test_jcl.pid log_file: /tmp/jcl.log +[vcard] +url: http://people.happycoders.org/dax/projects/jcl