Compare commits

...

16 Commits

Author SHA1 Message Date
mathieui
04244ecf82 XEP-0446: complete support and tests 2025-02-02 12:16:07 +01:00
mathieui
8c8bb5da8b doap: add missing xep 264 and 469 support 2025-01-31 12:23:11 +01:00
mathieui
bd638f1b39 doap: add hats support since 1.8.6 2025-01-31 11:12:37 +01:00
mathieui
0ff9e3661d XMLStream: allow custom sslcontext provisioning (fixes #3582)
For some applications that have strict requirements on blocking calls,
it might be beneficial to create the SSLContext in advance and
provide it to the client/componentxmpp instance that will be going
through kwargs until XMLStream.

The context will be reconfigured later on based on user parameters, but
it is highly recommended to set it up in a secure way.
2025-01-30 11:08:41 +00:00
mathieui
5ec378cccd xep-0055: fix stanza test 2025-01-30 09:35:41 +01:00
nicoco
a9fc955eda fix: add default error type for "payment-required" condition 2025-01-27 16:36:07 +01:00
nicoco
05860f71ac ci: follow linter recommendations 2025-01-27 16:36:07 +01:00
mathieui
1482bcc395 basexmpp: make_iq no longer defaults to id="0"
having a hardcoded default ID in make_iq is a bad idea, particularly
since it will overwrite the (good) id produced byt Iq() when a stream is
available.

This is arguably a breaking change, but I certainly hope it is not
breaking anything in the real world.
2025-01-26 18:03:15 +01:00
nicoco
2e736bc715 feat: support XEP-0492 (Chat Notification Settings) 2025-01-26 16:24:31 +00:00
nicoco
8d984cd8a1 XEP-0004: fix: prevent multiple <values> for 'text-single' field
According to XEP-0004:
- if there is no "type" attribute on a <field />, we should assume it is
  "text-single";
- "text-single" MUST NOT contain morethan one <value />.

Before this patch, not specifying a field type and passing a multi-line
string would result in an illegal stanza.

While it would be cleaner to log a warning or even raise an exception if
set_value() is called with an incompatible type, this breaks a lot of
tests and backward-compatibility, so we introduce some heuristic in
FormField.set_value() to infer the field type based on the provided
value instead.

I also changed FormField.get_value() so that it returns a list by
default for 'text-multi' fields. This is a breaking change, but I have
not found the justification for the previous behaviour.
2025-01-24 14:33:16 +00:00
nicoco
100014651c cq: remove unused files 2025-01-24 15:30:24 +01:00
nicoco
f9a9a0dcb7 fix: add default error type for "policy-violation" condition 2025-01-24 09:53:42 +01:00
nicoco
c585ec5983 fix: add missing "policy-violation" to error conditions 2025-01-22 21:32:57 +01:00
nicoco
27bbb1ef95 xep_0425: add missing 'id' attribute to 'Moderate' stanza plugin 2024-12-30 14:46:25 +01:00
mathieui
5dfc622539 itests: fix XEP-0424 test
broken due to the new XEP version
2024-12-29 01:08:05 +01:00
mathieui
2ab9b5a05c ci: add setuptools, remove 3.7/3.8 and add 3.13 2024-12-29 00:50:53 +01:00
27 changed files with 626 additions and 122 deletions

View File

@@ -1,81 +0,0 @@
stages:
- lint
- test
- trigger
mypy:
stage: lint
tags:
- docker
image: python:3
script:
- pip3 install mypy
- mypy slixmpp
test-3.7:
stage: test
tags:
- docker
image: python:3.7
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.10:
stage: test
tags:
- docker
image: python:3.10
script:
- apt update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.11:
stage: test
tags:
- docker
image: python:3.11
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.12:
stage: test
tags:
- docker
image: python:3.12-rc
allow_failure: true
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test_integration:
stage: test
tags:
- docker
image: python:3
only:
variables:
- $CI_ACCOUNT1
- $CI_ACCOUNT2
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp aiodns
- ./run_integration_tests.py
trigger_poezio:
stage: trigger
tags:
- docker
image: curlimages/curl:7.79.1
script:
- curl --request POST -F token="$SLIXMPP_TRIGGER_TOKEN" -F ref=master https://lab.louiz.org/api/v4/projects/18/trigger/pipeline

View File

@@ -1,7 +0,0 @@
language: python
python:
- "3.7"
- "3.8-dev"
install:
- "pip install ."
script: testall.py

View File

@@ -1,3 +1,6 @@
when:
event: [ push, pull_request ]
steps:
mypy:
image: python:3

View File

@@ -1,7 +1,20 @@
when:
event: [ push, pull_request ]
steps:
test_integration:
image: "python:3.11"
secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password, ci_muc_server]
environment:
CI_ACCOUNT1:
from_secret: ci_account1
CI_ACCOUNT1_PASSWORD:
from_secret: ci_account1_password
CI_ACCOUNT2:
from_secret: ci_account2
CI_ACCOUNT2_PASSWORD:
from_secret: ci_account2_password
CI_MUC_SERVER:
from_secret: ci_muc_server
commands:
- apt-get update
- apt-get install -y python3-pip cython3 gpg idn libidn-dev

View File

@@ -1,17 +1,19 @@
when:
event: [ push, pull_request ]
steps:
unit_tests:
image: "python:${TAG}"
commands:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- pip3 install emoji aiohttp cryptography setuptools
- ./run_tests.py
matrix:
TAG:
- "3.7"
- "3.9"
- "3.8"
- "3.10"
- "3.11"
- "3.12"
- "3.13"

View File

@@ -616,6 +616,14 @@
<xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0264.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.2</xmpp:version>
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0270.html"/>
@@ -682,6 +690,14 @@
<xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0317.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.8.6</xmpp:version>
<xmpp:since>0.2</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
@@ -900,13 +916,29 @@
<xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>1.8.7</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.1</xmpp:since>
<xmpp:note>no thumbnail support</xmpp:note>
<xmpp:note>no thumbnail support</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0469.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
@@ -917,6 +949,14 @@
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0492.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.7</xmpp:since>
</xmpp:SupportedXep>
</implements>
<release>
<Version>

View File

@@ -94,3 +94,4 @@ Plugin index
xep_0439
xep_0441
xep_0444
xep_0492

View File

@@ -0,0 +1,18 @@
XEP-0492: Chat Notification Settings
===========================
.. module:: slixmpp.plugins.xep_0492
.. autoclass:: XEP_0492
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0492.stanza
:members:
:undoc-members:

View File

@@ -23,7 +23,6 @@ class TestRetract(SlixIntegration):
fallback_text='Twas a mistake',
)
msg = await self.clients[1].wait_until('message_retract')
self.assertEqual(msg['apply_to']['id'], 'toto')
self.assertTrue(msg['apply_to']['retract'])
self.assertEqual(msg['retract']['id'], 'toto')
suite = unittest.TestLoader().loadTestsFromTestCase(TestRetract)

View File

@@ -315,13 +315,12 @@ class BaseXMPP(XMLStream):
pres['lang'] = self.default_lang
return pres
def make_iq(self, id: str = "0", ifrom: OptJidStr = None,
def make_iq(self, id: Optional[str] = None, ifrom: OptJidStr = None,
ito: OptJidStr = None, itype: Optional[IqTypes] = None,
iquery: Optional[str] = None) -> stanza.Iq:
"""Create a new :class:`~.Iq` stanza with a given Id and from JID.
:param id: An ideally unique ID value for this stanza thread.
Defaults to 0.
:param ifrom: The from :class:`~.JID`
to use for this stanza.
:param ito: The destination :class:`~.JID`
@@ -332,7 +331,8 @@ class BaseXMPP(XMLStream):
:param iquery: Optional namespace for adding a query element.
"""
iq = self.Iq()
iq['id'] = str(id)
if id is not None:
iq['id'] = str(id)
iq['to'] = ito
iq['from'] = ifrom
iq['type'] = itype

View File

@@ -135,6 +135,7 @@ _DEFAULT_ERROR_TYPES: Dict[ErrorConditions, ErrorTypes] = {
"not-allowed": "cancel",
"not-authorized": "auth",
"payment-required": "auth",
"policy-violation": "modify",
"recipient-unavailable": "wait",
"redirect": "modify",
"registration-required": "auth",

View File

@@ -118,10 +118,12 @@ PLUGINS = [
'xep_0439', # Quick Response
'xep_0441', # Message Archive Management Preferences
'xep_0444', # Message Reactions
'xep_0446', # File metadata element
'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
'xep_0490', # Message Displayed Synchronization
'xep_0492', # Chat Notification Settings
# Meant to be imported by plugins
]

View File

@@ -1,8 +1,9 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import logging
from slixmpp.xmlstream import ElementBase, ET
@@ -78,7 +79,14 @@ class FormField(ElementBase):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
def get_value(self, convert=True):
def get_value(self, convert=True, convert_list=False):
"""
Gets the value for this field
:param convert: Convert truthy values to boolean
:param convert_list: Convert text-multi fields to a string with
\n as separator for values
"""
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
@@ -92,7 +100,7 @@ class FormField(ElementBase):
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
if self._type == 'text-multi' and convert:
if self._type == 'text-multi' and convert_list:
values = "\n".join(values)
return values
else:
@@ -127,6 +135,17 @@ class FormField(ElementBase):
del self['value']
valXMLName = '{%s}value' % self.namespace
if not self._type:
if isinstance(value, bool):
log.debug("Passed a 'boolean' as value of an untyped field, assuming it is a 'boolean'")
self._type = "boolean"
elif isinstance(value, str):
log.debug("Passed a 'str' as value of an untyped field, assuming it is a 'text-single'")
self._type = "text-single"
elif isinstance(value, (list, tuple)):
log.debug("Passed a %s as value of an untyped field, assuming it is a 'text-multi'")
self._type = "text-multi"
if self._type == 'boolean':
if value in self.true_values:
valXML = ET.Element(valXMLName)
@@ -180,3 +199,6 @@ FormField.setOptions = FormField.set_options
FormField.setRequired = FormField.set_required
FormField.setTrue = FormField.set_true
FormField.setValue = FormField.set_value
log = logging.getLogger(__name__)

View File

@@ -19,7 +19,7 @@ class Moderate(ElementBase):
namespace = NS
name = 'moderate'
plugin_attrib = 'moderate'
interfaces = {'reason'}
interfaces = {'id', 'reason'}
sub_interfaces = {'reason'}

View File

@@ -18,3 +18,7 @@ class XEP_0446(BasePlugin):
name = "xep_0446"
description = "XEP-0446: File metadata element"
stanza = stanza
dependencies = {'xep_0300', 'xep_0264'}
def plugin_init(self):
stanza.register_plugins()

View File

@@ -1,7 +1,10 @@
from datetime import datetime
from typing import Optional
from slixmpp.plugins.xep_0082 import format_datetime, parse
from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0300 import Hash
from slixmpp.plugins.xep_0264.stanza import Thumbnail
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = "urn:xmpp:file:metadata:0"
@@ -10,15 +13,42 @@ class File(ElementBase):
name = "file"
namespace = NS
plugin_attrib = "file"
interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"}
interfaces = sub_interfaces = {
"media-type",
"name",
"date",
"size",
"desc",
"width",
"height",
"length"
}
def set_width(self, width: int):
self.__set_if_positive("width", width)
def get_width(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("width"))
def set_height(self, height: int):
self.__set_if_positive("height", height)
def get_height(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("height"))
def set_length(self, length: int):
self.__set_if_positive("length", length)
def get_length(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("length"))
def set_size(self, size: int):
self._set_sub_text("size", str(size))
self.__set_if_positive("size", size)
def get_size(self):
return _int_or_none(self._get_sub_text("size"))
def get_size(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("size"))
def get_date(self):
def get_date(self) -> Optional[datetime]:
try:
return parse(self._get_sub_text("date"))
except ValueError:
@@ -30,9 +60,18 @@ class File(ElementBase):
except ValueError:
pass
def __set_if_positive(self, key: str, value: int):
if value <= 0:
raise ValueError(f"Invalid value for element {key}: {value}")
self._set_sub_text(key, str(value))
def _int_or_none(v):
def _positive_int_or_none(v: str) -> Optional[int]:
try:
return int(v)
except ValueError:
return None
def register_plugins():
register_stanza_plugin(File, Hash)
register_stanza_plugin(File, Thumbnail)

View File

@@ -0,0 +1,13 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins.base import register_plugin
from . import stanza
from .notify import XEP_0492
register_plugin(XEP_0492)
__all__ = ["stanza", "XEP_0492"]

View File

@@ -0,0 +1,21 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0492(BasePlugin):
"""
XEP-0492: Chat notification settings
"""
name = "xep_0492"
description = "XEP-0492: Chat notification settings"
dependencies = {"xep_0402"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,106 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from typing import Literal, Optional, cast
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0402.stanza import Extensions
from slixmpp.types import ClientTypes
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:notification-settings:0"
WhenLiteral = Literal["never", "always", "on-mention"]
class Notify(ElementBase):
"""
Chat notification settings element
To enable it on a Conference element, use configure() like this:
.. code-block::python
# C being a Conference element
C['extensions']["notify"].configure("always", client_type="pc")
Which will add the <notify> element to the <extensions> element.
"""
namespace = NS
name = "notify"
plugin_attrib = "notify"
interfaces = {"notify"}
def configure(self, when: WhenLiteral, client_type: Optional[ClientTypes] = None) -> None:
"""
Configure the chat notification settings for this bookmark.
This method ensures that there are no conflicting settings, e.g.,
both a <never /> and a <always /> element.
"""
cls = _CLASS_MAP[when]
element = cls()
if client_type is not None:
element["client-type"] = client_type
match = client_type if client_type is not None else ""
for child in self:
if isinstance(child, _Base) and child["client-type"] == match:
self.xml.remove(child.xml)
self.append(element)
def get_config(
self, client_type: Optional[ClientTypes] = None
) -> Optional[WhenLiteral]:
"""
Get the chat notification settings for this bookmark.
:param client_type: Optionally, get the notification for a specific client type.
If unset, returns the global notification setting.
:return: The chat notification setting as a string, or None if unset.
"""
match = client_type if client_type is not None else ""
for child in self:
if isinstance(child, _Base) and child["client-type"] == match:
return cast(WhenLiteral, child.name)
return None
class _Base(ElementBase):
namespace = NS
interfaces = {"client-type"}
class Never(_Base):
name = "never"
class Always(_Base):
name = "always"
class OnMention(_Base):
name = "on-mention"
class Advanced(ElementBase):
namespace = NS
name = plugin_attrib = "advanced"
_CLASS_MAP = {
"never": Never,
"always": Always,
"on-mention": OnMention,
}
def register_plugin():
register_stanza_plugin(Extensions, Notify)
register_stanza_plugin(Notify, Advanced)

View File

@@ -97,6 +97,7 @@ ErrorConditions = Literal[
"not-allowed",
"not-authorized",
"payment-required",
"policy-violation",
"recipient-unavailable",
"redirect",
"registration-required",
@@ -109,8 +110,21 @@ ErrorConditions = Literal[
"unexpected-request",
]
# https://xmpp.org/registrar/disco-categories.html#client
ClientTypes = Literal[
"bot",
"console",
"game",
"handheld",
"pc",
"phone",
"sms",
"tablet",
"web",
]
__all__ = [
'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault',
'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole',
'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes'
'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes', 'ClientTypes'
]

View File

@@ -281,7 +281,8 @@ class XMLStream(asyncio.BaseProtocol):
__slow_tasks: List[Task]
__queued_stanzas: List[Tuple[Union[StanzaBase, str], bool]]
def __init__(self, host: str = '', port: int = 0):
def __init__(self, host: str = '', port: int = 0,
ssl_context: Optional[ssl.SSLContext] = None):
self.transport = None
self.socket = None
self._connect_loop_wait = 0
@@ -298,9 +299,12 @@ class XMLStream(asyncio.BaseProtocol):
# A dict of {name: handle}
self.scheduled_events = {}
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = True
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
if ssl_context is None:
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = True
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
else:
self.ssl_context = ssl_context
self.event_when_connected = "connected"

View File

@@ -95,6 +95,21 @@ class TestDataForms(SlixTest):
</message>
""")
def testMultiLineField(self):
msg = self.Message()
form = msg['form']
form.addField(var='f1',
value='Some text\non several\n\nlines')
self.check(msg, """
<message>
<x xmlns="jabber:x:data" type="form">
<field var="f1">
<value>Some text\non several\n\nlines</value>
</field>
</x>
</message>
""")
def testSetValues(self):
"""Testing setting form values"""
@@ -117,7 +132,7 @@ class TestDataForms(SlixTest):
<value>b</value>
</field>
</x>
</message>""")
</message>""", use_values=False)
def testSubmitType(self):
"""Test that setting type to 'submit' clears extra details"""

View File

@@ -34,6 +34,7 @@ class TestJabberSearch(SlixTest):
ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit"
)
iq["search"]["form"].add_field(var="x-gender", value="male")
iq["id"] = "0"
self.check(
iq,
"""

View File

@@ -21,12 +21,12 @@ class TestModeration(SlixTest):
self.check(iq, """
<iq type='set' id='a'>
<moderate xmlns='urn:xmpp:message-moderate:1'>
<moderate xmlns='urn:xmpp:message-moderate:1' id='some-id'>
<retract xmlns='urn:xmpp:message-retract:1'/>
<reason>R</reason>
</moderate>
</iq>
""", use_values=False)
""")
def testModerated(self):
message = Message()

View File

@@ -0,0 +1,101 @@
import unittest
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0446 import stanza
class TestFileMeta(SlixTest):
def setUp(self):
stanza.register_plugins()
def test_simple(self):
file = stanza.File()
file["desc"] = "a description"
file["name"] = "toto.jpg"
file["media-type"] = "image/jpeg"
file["height"] = 1024
file["width"] = 768
file["size"] = 2048
self.check(
file,
"""
<file xmlns='urn:xmpp:file:metadata:0'>
<desc>a description</desc>
<name>toto.jpg</name>
<media-type>image/jpeg</media-type>
<height>1024</height>
<width>768</width>
<size>2048</size>
</file>
""",
)
def test_bad_value(self):
file = stanza.File()
file["desc"] = "My great video"
file["name"] = "toto.mp4"
file["media-type"] = "video/3gpp"
file["height"] = 1024
file["width"] = 768
with self.assertRaises(ValueError):
file["length"] = -100
def test_hash_element(self):
file = stanza.File()
file["desc"] = "My great video"
file["name"] = "toto.3gp"
file["media-type"] = "video/3gpp"
file["height"] = 1024
file["width"] = 768
file["length"] = 2000
file["hash"]["algo"] = "sha3-256"
file["hash"]["value"] = "abcdef="
self.check(
file,
"""
<file xmlns='urn:xmpp:file:metadata:0'>
<desc>My great video</desc>
<name>toto.3gp</name>
<media-type>video/3gpp</media-type>
<height>1024</height>
<width>768</width>
<length>2000</length>
<hash xmlns='urn:xmpp:hashes:2' algo="sha3-256">abcdef=</hash>
</file>
""",
)
def test_thumbnail_element(self):
file = stanza.File()
file["desc"] = "a description"
file["name"] = "toto.jpg"
file["media-type"] = "image/jpeg"
file["height"] = 1024
file["width"] = 768
file["size"] = 2048
file["thumbnail"]["media-type"] = "image/png"
file["thumbnail"]["uri"] = "cid:sha1+deadbeef@bob.xmpp.org"
file["thumbnail"]["width"] = 128
file["thumbnail"]["height"] = 96
self.check(
file,
"""
<file xmlns='urn:xmpp:file:metadata:0'>
<desc>a description</desc>
<name>toto.jpg</name>
<media-type>image/jpeg</media-type>
<height>1024</height>
<width>768</width>
<size>2048</size>
<thumbnail xmlns='urn:xmpp:thumbs:1'
uri='cid:sha1+deadbeef@bob.xmpp.org'
media-type='image/png'
width='128'
height='96'/>
</file>
""",
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestFileMeta)

View File

@@ -0,0 +1,178 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import unittest
from slixmpp import register_stanza_plugin, ElementBase
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0492 import stanza
from slixmpp.plugins.xep_0402 import stanza as b_stanza
class TestNotificationSetting(SlixTest):
def setUp(self):
b_stanza.register_plugin()
stanza.register_plugin()
def test_never(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<never />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_always(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("always")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<always />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_on_mention(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("on-mention")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<on-mention />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_advanced(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never", client_type="pc")
bookmark["extensions"]["notify"].configure("on-mention", client_type="mobile")
register_stanza_plugin(stanza.Advanced, AdvancedExtension)
bookmark["extensions"]["notify"]["advanced"].enable("cool")
bookmark["extensions"]["notify"]["advanced"]["cool"]["attrib"] = "cool-attrib"
bookmark["extensions"]["notify"]["advanced"]["cool"]["content"] = "cool-content"
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<never client-type="pc" />
<on-mention client-type="mobile" />
<advanced>
<cool xmlns="cool-ns" attrib="cool-attrib">cool-content</cool>
</advanced>
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_change_config(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never")
bookmark["extensions"]["notify"].configure("never", client_type="pc")
bookmark["extensions"]["notify"].configure("on-mention", client_type="mobile")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<never />
<never client-type="pc" />
<on-mention client-type="mobile" />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
bookmark["extensions"]["notify"].configure("always")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<always />
<never client-type="pc" />
<on-mention client-type="mobile" />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
bookmark["extensions"]["notify"].configure("always", "mobile")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<always />
<never client-type="pc" />
<always client-type="mobile" />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_get_config(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never")
bookmark["extensions"]["notify"].configure("never", client_type="pc")
bookmark["extensions"]["notify"].configure("on-mention", client_type="mobile")
self.assertEqual(bookmark["extensions"]["notify"].get_config(), "never")
self.assertEqual(bookmark["extensions"]["notify"].get_config("pc"), "never")
self.assertEqual(
bookmark["extensions"]["notify"].get_config("mobile"), "on-mention"
)
class AdvancedExtension(ElementBase):
namespace = "cool-ns"
name = "cool"
plugin_attrib = name
interfaces = {"attrib", "content"}
def set_content(self, content: str):
self.xml.text = content
suite = unittest.TestLoader().loadTestsFromTestCase(TestNotificationSetting)

View File

@@ -1,5 +0,0 @@
[tox]
envlist = py34
[testenv]
deps = nose
commands = nosetests --where=tests --exclude=live -i slixtest.py