diff --git a/doap.xml b/doap.xml
index 9d3c66c7..20ad179d 100644
--- a/doap.xml
+++ b/doap.xml
@@ -916,6 +916,14 @@
1.6.0
+
+
+
+ complete
+ 0.2.0
+ 1.8.7
+
+
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index dd43077c..589cef46 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -118,6 +118,7 @@ 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
diff --git a/slixmpp/plugins/xep_0446/file_metadata.py b/slixmpp/plugins/xep_0446/file_metadata.py
index e917f6b4..56c7e01b 100644
--- a/slixmpp/plugins/xep_0446/file_metadata.py
+++ b/slixmpp/plugins/xep_0446/file_metadata.py
@@ -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()
diff --git a/slixmpp/plugins/xep_0446/stanza.py b/slixmpp/plugins/xep_0446/stanza.py
index f887272d..4335383b 100644
--- a/slixmpp/plugins/xep_0446/stanza.py
+++ b/slixmpp/plugins/xep_0446/stanza.py
@@ -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)
diff --git a/tests/test_stanza_xep_0446.py b/tests/test_stanza_xep_0446.py
new file mode 100644
index 00000000..1a245081
--- /dev/null
+++ b/tests/test_stanza_xep_0446.py
@@ -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,
+ """
+
+ a description
+ toto.jpg
+ image/jpeg
+ 1024
+ 768
+ 2048
+
+ """,
+ )
+
+ 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,
+ """
+
+ My great video
+ toto.3gp
+ video/3gpp
+ 1024
+ 768
+ 2000
+ abcdef=
+
+ """,
+ )
+
+ 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,
+ """
+
+ a description
+ toto.jpg
+ image/jpeg
+ 1024
+ 768
+ 2048
+
+
+ """,
+ )
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestFileMeta)