aboutsummaryrefslogtreecommitdiff
path: root/xmpp-vala
diff options
context:
space:
mode:
authorPatiga <dev@patiga.eu>2022-06-28 12:11:17 +0200
committerfiaxh <git@lightrise.org>2024-11-14 10:20:12 -0600
commitaaf4542e6208460c305db4be36b15dc832ddc95a (patch)
treeec7b60b0f0ea74e21403788e8345336bd0f3939b /xmpp-vala
parent909f569318835d11703c49fba7dbe49996759f38 (diff)
downloaddino-aaf4542e6208460c305db4be36b15dc832ddc95a.tar.gz
dino-aaf4542e6208460c305db4be36b15dc832ddc95a.zip
Implement XEP-0447: Stateless file sharing
Diffstat (limited to 'xmpp-vala')
-rw-r--r--xmpp-vala/CMakeLists.txt4
-rw-r--r--xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala49
-rw-r--r--xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala136
-rw-r--r--xmpp-vala/src/module/xep/0446_file_metadata_element.vala120
-rw-r--r--xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala225
5 files changed, 534 insertions, 0 deletions
diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt
index 4a213fda..824b667e 100644
--- a/xmpp-vala/CMakeLists.txt
+++ b/xmpp-vala/CMakeLists.txt
@@ -125,10 +125,12 @@ SOURCES
"src/module/xep/0249_direct_muc_invitations.vala"
"src/module/xep/0260_jingle_socks5_bytestreams.vala"
"src/module/xep/0261_jingle_in_band_bytestreams.vala"
+ "src/module/xep/0264_jingle_content_thumbnails.vala"
"src/module/xep/0272_muji.vala"
"src/module/xep/0280_message_carbons.vala"
"src/module/xep/0297_stanza_forwarding.vala"
"src/module/xep/0298_coin.vala"
+ "src/module/xep/0300_cryptographic_hashes.vala"
"src/module/xep/0308_last_message_correction.vala"
"src/module/xep/0313_message_archive_management.vala"
"src/module/xep/0313_2_message_archive_management.vala"
@@ -143,6 +145,8 @@ SOURCES
"src/module/xep/0421_occupant_ids.vala"
"src/module/xep/0428_fallback_indication.vala"
"src/module/xep/0444_reactions.vala"
+ "src/module/xep/0446_file_metadata_element.vala"
+ "src/module/xep/0447_stateless_file_sharing.vala"
"src/module/xep/0461_replies.vala"
"src/module/xep/0482_call_invites.vala"
"src/module/xep/pixbuf_storage.vala"
diff --git a/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala b/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala
new file mode 100644
index 00000000..cb281f80
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0264_jingle_content_thumbnails.vala
@@ -0,0 +1,49 @@
+namespace Xmpp.Xep.JingleContentThumbnails {
+ public const string NS_URI = "urn:xmpp:thumbs:1";
+ public const string STANZA_NAME = "thumbnail";
+
+ public class Thumbnail {
+ public string uri;
+ public string? media_type;
+ public int width;
+ public int height;
+
+ const string URI_ATTRIBUTE = "uri";
+ const string MIME_ATTRIBUTE = "media-type";
+ const string WIDTH_ATTRIBUTE = "width";
+ const string HEIGHT_ATTRIBUTE = "height";
+
+ public StanzaNode to_stanza_node() {
+ StanzaNode node = new StanzaNode.build(STANZA_NAME, NS_URI).add_self_xmlns()
+ .put_attribute(URI_ATTRIBUTE, this.uri);
+ if (this.media_type != null) {
+ node.put_attribute(MIME_ATTRIBUTE, this.media_type);
+ }
+ if (this.width != -1) {
+ node.put_attribute(WIDTH_ATTRIBUTE, this.width.to_string());
+ }
+ if (this.height != -1) {
+ node.put_attribute(HEIGHT_ATTRIBUTE, this.height.to_string());
+ }
+ return node;
+ }
+
+ public static Thumbnail? from_stanza_node(StanzaNode node) {
+ Thumbnail thumbnail = new Thumbnail();
+ thumbnail.uri = node.get_attribute(URI_ATTRIBUTE);
+ if (thumbnail.uri == null) {
+ return null;
+ }
+ thumbnail.media_type = node.get_attribute(MIME_ATTRIBUTE);
+ string? width = node.get_attribute(WIDTH_ATTRIBUTE);
+ if (width != null) {
+ thumbnail.width = int.parse(width);
+ }
+ string? height = node.get_attribute(HEIGHT_ATTRIBUTE);
+ if (height != null) {
+ thumbnail.height = int.parse(height);
+ }
+ return thumbnail;
+ }
+ }
+} \ No newline at end of file
diff --git a/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala b/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala
new file mode 100644
index 00000000..00f9e2ee
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0300_cryptographic_hashes.vala
@@ -0,0 +1,136 @@
+using GLib;
+using Gee;
+
+namespace Xmpp.Xep.CryptographicHashes {
+ public const string NS_URI = "urn:xmpp:hashes:2";
+
+ public enum HashCmp {
+ Match,
+ Mismatch,
+ None,
+ }
+
+ public class Hash {
+ public string algo;
+ // hash encoded in Base64
+ public string val;
+
+ public static string hash_name(ChecksumType type) {
+ switch(type) {
+ case ChecksumType.MD5:
+ return "md5";
+ case ChecksumType.SHA1:
+ return "sha-1";
+ case ChecksumType.SHA256:
+ return "sha-256";
+ case ChecksumType.SHA384:
+ return "sha-384";
+ case ChecksumType.SHA512:
+ return "sha-512";
+ }
+ return "(null)";
+ }
+
+ public static ChecksumType? supported_hash(string hash) {
+ switch (hash) {
+ case "sha-1":
+ return ChecksumType.SHA1;
+ case "sha-256":
+ return ChecksumType.SHA256;
+ case "sha-384":
+ return ChecksumType.SHA384;
+ case "sha-512":
+ return ChecksumType.SHA512;
+ }
+ return null;
+ }
+
+ public Hash.from_data(GLib.ChecksumType type, uint8[] data) {
+ GLib.Checksum checksum = new GLib.Checksum(type);
+ checksum.update(data, data.length);
+ // 64 * 8 = 512 (sha-512 is the longest hash variant)
+ uint8[] digest = new uint8[64];
+ size_t length = digest.length;
+ checksum.get_digest(digest, ref length);
+ this.algo = hash_name(type);
+ this.val = GLib.Base64.encode(digest[0:length]);
+ }
+
+ public HashCmp compare(Hash other) {
+ if (this.algo != other.algo) {
+ return HashCmp.None;
+ }
+ if (this.val == other.val) {
+ return HashCmp.Match;
+ } else {
+ return HashCmp.Mismatch;
+ }
+ }
+
+ public StanzaNode to_stanza_node() {
+ return new StanzaNode.build("hash", NS_URI).add_self_xmlns()
+ .put_attribute("algo", this.algo)
+ .put_node(new StanzaNode.text(this.val));
+ }
+
+ public Hash.from_stanza_node(StanzaNode node) {
+ this.algo = node.get_attribute("algo");
+ this.val = node.get_string_content();
+ }
+ }
+
+ public class Hashes {
+ public Gee.List<Hash> hashes = new ArrayList<Hash>();
+
+ public Gee.List<ChecksumType> supported_hashes() {
+ Gee.List<ChecksumType> supported = new ArrayList<ChecksumType>();
+ foreach (Hash hash in this.hashes) {
+ ChecksumType? hash_type = Hash.supported_hash(hash.algo);
+ if (hash_type != null) {
+ supported.add(hash_type);
+ }
+ }
+ return supported;
+ }
+
+ public Hashes.from_data(Gee.List<ChecksumType> types, uint8[] data) {
+ foreach (ChecksumType type in types) {
+ this.hashes.add(new Hash.from_data(type, data));
+ }
+ }
+
+ public HashCmp compare(Hashes other) {
+ HashCmp cmp = HashCmp.None;
+ foreach (Hash this_hash in this.hashes) {
+ foreach (Hash other_hash in other.hashes) {
+ switch (this_hash.compare(other_hash)) {
+ case HashCmp.Mismatch:
+ return HashCmp.Mismatch;
+ case HashCmp.Match:
+ cmp = HashCmp.Match;
+ break;
+ case HashCmp.None:
+ continue;
+ }
+ }
+ }
+ return cmp;
+ }
+
+ public Gee.List<StanzaNode> to_stanza_nodes() {
+ Gee.List<StanzaNode> nodes = new ArrayList<StanzaNode>();
+ foreach (Hash hash in this.hashes) {
+ nodes.add(hash.to_stanza_node());
+ }
+ return nodes;
+ }
+
+ public Hashes.from_stanza_subnodes(StanzaNode node) {
+ Gee.List<StanzaNode> subnodes = node.get_subnodes("hash", NS_URI);
+ this.hashes = new ArrayList<Hash>();
+ foreach (StanzaNode subnode in subnodes) {
+ this.hashes.add(new Hash.from_stanza_node(subnode));
+ }
+ }
+ }
+}
diff --git a/xmpp-vala/src/module/xep/0446_file_metadata_element.vala b/xmpp-vala/src/module/xep/0446_file_metadata_element.vala
new file mode 100644
index 00000000..d7fbb06f
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0446_file_metadata_element.vala
@@ -0,0 +1,120 @@
+using Xmpp.Xep.CryptographicHashes;
+
+namespace Xmpp.Xep.FileMetadataElement {
+ public const string NS_URI = "urn:xmpp:file:metadata:0";
+
+ public class FileMetadata {
+ public string name { get; set; }
+ public string? mime_type { get; set; }
+ public int64 size { get; set; default=-1; }
+ public string? desc { get; set; }
+ public DateTime? date { get; set; }
+ public int width { get; set; default=-1; } // Width of image in pixels
+ public int height { get; set; default=-1; } // Height of image in pixels
+ public CryptographicHashes.Hashes hashes = new CryptographicHashes.Hashes();
+ public int64 length { get; set; default=-1; } // Length of audio/video in milliseconds
+ public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>();
+
+ public StanzaNode to_stanza_node() {
+ StanzaNode node = new StanzaNode.build("file", NS_URI).add_self_xmlns()
+ .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(this.name)));
+ if (this.mime_type != null) {
+ node.put_node(new StanzaNode.build("media_type", NS_URI).put_node(new StanzaNode.text(this.mime_type)));
+ }
+ if (this.size != -1) {
+ node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(this.size.to_string())));
+ }
+ if (this.date != null) {
+ node.put_node(new StanzaNode.build("date", NS_URI).put_node(new StanzaNode.text(this.date.to_string())));
+ }
+ if (this.desc != null) {
+ node.put_node(new StanzaNode.build("desc", NS_URI).put_node(new StanzaNode.text(this.desc)));
+ }
+ if (this.width != -1) {
+ node.put_node(new StanzaNode.build("width", NS_URI).put_node(new StanzaNode.text(this.width.to_string())));
+ }
+ if (this.height != -1) {
+ node.put_node(new StanzaNode.build("height", NS_URI).put_node(new StanzaNode.text(this.height.to_string())));
+ }
+ if (this.length != -1) {
+ node.put_node(new StanzaNode.build("length", NS_URI).put_node(new StanzaNode.text(this.length.to_string())));
+ }
+ node.sub_nodes.add_all(this.hashes.to_stanza_nodes());
+ foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in this.thumbnails) {
+ node.put_node(thumbnail.to_stanza_node());
+ }
+ return node;
+ }
+
+ public void add_to_message(MessageStanza message) {
+ StanzaNode node = this.to_stanza_node();
+ printerr("Attaching file metadata:\n");
+ printerr("%s\n", node.to_ansi_string(true));
+ message.stanza.put_node(node);
+ }
+
+ public static FileMetadata? from_stanza_node(StanzaNode node) {
+ FileMetadata metadata = new FileMetadata();
+ // TODO: null checks on final values
+ StanzaNode? name_node = node.get_subnode("name");
+ if (name_node == null || name_node.get_string_content() == null) {
+ return null;
+ } else {
+ metadata.name = name_node.get_string_content();
+ }
+ StanzaNode? desc_node = node.get_subnode("desc");
+ if (desc_node != null && desc_node.get_string_content() != null) {
+ metadata.desc = desc_node.get_string_content();
+ }
+ StanzaNode? mime_node = node.get_subnode("media_type");
+ if (mime_node != null && mime_node.get_string_content() != null) {
+ metadata.mime_type = mime_node.get_string_content();
+ }
+ StanzaNode? size_node = node.get_subnode("size");
+ if (size_node != null && size_node.get_string_content() != null) {
+ metadata.size = int64.parse(size_node.get_string_content());
+ }
+ StanzaNode? date_node = node.get_subnode("date");
+ if (date_node != null && date_node.get_string_content() != null) {
+ metadata.date = new DateTime.from_iso8601(date_node.get_string_content(), null);
+ }
+ StanzaNode? width_node = node.get_subnode("width");
+ if (width_node != null && width_node.get_string_content() != null) {
+ metadata.width = int.parse(width_node.get_string_content());
+ }
+ StanzaNode? height_node = node.get_subnode("height");
+ if (height_node != null && height_node.get_string_content() != null) {
+ metadata.height = int.parse(height_node.get_string_content());
+ }
+ StanzaNode? length_node = node.get_subnode("length");
+ if (length_node != null && length_node.get_string_content() != null) {
+ metadata.length = int.parse(length_node.get_string_content());
+ }
+ foreach (StanzaNode thumbnail_node in node.get_subnodes(Xep.JingleContentThumbnails.STANZA_NAME, Xep.JingleContentThumbnails.NS_URI)) {
+ Xep.JingleContentThumbnails.Thumbnail? thumbnail = Xep.JingleContentThumbnails.Thumbnail.from_stanza_node(thumbnail_node);
+ if (thumbnail != null) {
+ metadata.thumbnails.add(thumbnail);
+ }
+ }
+ metadata.hashes = new CryptographicHashes.Hashes.from_stanza_subnodes(node);
+ return metadata;
+ }
+
+ public static FileMetadata? from_message(MessageStanza message) {
+ StanzaNode? node = message.stanza.get_subnode("file", NS_URI);
+ if (node == null) {
+ return null;
+ }
+ printerr("Parsing metadata from message:\n");
+ printerr("%s\n", node.to_xml());
+ FileMetadata metadata = FileMetadata.from_stanza_node(node);
+ if (metadata != null) {
+ printerr("Parsed metadata:\n");
+ printerr("%s\n", metadata.to_stanza_node().to_ansi_string(true));
+ } else {
+ printerr("Failed to parse metadata!\n");
+ }
+ return FileMetadata.from_stanza_node(node);
+ }
+ }
+}
diff --git a/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala b/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala
new file mode 100644
index 00000000..285bfe78
--- /dev/null
+++ b/xmpp-vala/src/module/xep/0447_stateless_file_sharing.vala
@@ -0,0 +1,225 @@
+using Xmpp;
+
+namespace Xmpp.Xep.StatelessFileSharing {
+
+
+ public const string STANZA_NAME = "file-transfer";
+
+ public interface SfsSource: Object {
+ public abstract string type();
+ public abstract string serialize();
+
+ public abstract StanzaNode to_stanza_node();
+ }
+
+ public class HttpSource: Object, SfsSource {
+ public string url;
+
+ public const string HTTP_NS_URI = "http://jabber.org/protocol/url-data";
+ public const string HTTP_STANZA_NAME = "url-data";
+ public const string HTTP_URL_ATTRIBUTE = "target";
+ public const string SOURCE_TYPE = "http";
+
+ public string type() {
+ return SOURCE_TYPE;
+ }
+
+ public string serialize() {
+ return this.to_stanza_node().to_xml();
+ }
+
+ public StanzaNode to_stanza_node() {
+ StanzaNode node = new StanzaNode.build(HTTP_STANZA_NAME, HTTP_NS_URI).add_self_xmlns();
+ node.put_attribute(HTTP_URL_ATTRIBUTE, this.url);
+ return node;
+ }
+
+ public static async HttpSource deserialize(string data) {
+ StanzaNode node = yield new StanzaReader.for_string(data).read_stanza_node();
+ HttpSource source = HttpSource.from_stanza_node(node);
+ assert(source != null);
+ return source;
+ }
+
+ public static HttpSource? from_stanza_node(StanzaNode node) {
+ string? url = node.get_attribute(HTTP_URL_ATTRIBUTE);
+ if (url == null) {
+ return null;
+ }
+ HttpSource source = new HttpSource();
+ source.url = url;
+ return source;
+ }
+
+ public static Gee.List<HttpSource> extract_sources(StanzaNode node) {
+ Gee.List<HttpSource> sources = new Gee.ArrayList<HttpSource>();
+ foreach (StanzaNode http_node in node.get_subnodes(HTTP_STANZA_NAME, HTTP_NS_URI)) {
+ HttpSource? source = HttpSource.from_stanza_node(http_node);
+ if (source != null) {
+ sources.add(source);
+ }
+ }
+ return sources;
+ }
+ }
+
+ public class SfsElement {
+ public Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata();
+ public Gee.List<SfsSource> sources = new Gee.ArrayList<SfsSource>();
+
+ public static SfsElement? from_stanza_node(StanzaNode node) {
+ SfsElement element = new SfsElement();
+ StanzaNode? metadata_node = node.get_subnode("file", Xep.FileMetadataElement.NS_URI);
+ if (metadata_node == null) {
+ return null;
+ }
+ Xep.FileMetadataElement.FileMetadata metadata = Xep.FileMetadataElement.FileMetadata.from_stanza_node(metadata_node);
+ if (metadata == null) {
+ return null;
+ }
+ element.metadata = metadata;
+ StanzaNode? sources_node = node.get_subnode("sources");
+ if (sources_node == null) {
+ return null;
+ }
+ Gee.List<HttpSource> sources = HttpSource.extract_sources(sources_node);
+ if (sources.is_empty) {
+ return null;
+ }
+ element.sources = sources;
+ return element;
+ }
+
+ public StanzaNode to_stanza_node() {
+ StanzaNode node = new StanzaNode.build(STANZA_NAME, NS_URI).add_self_xmlns();
+ node.put_node(this.metadata.to_stanza_node());
+ StanzaNode sources_node = new StanzaNode.build("sources", NS_URI);
+ Gee.List<StanzaNode> sources = new Gee.ArrayList<StanzaNode>();
+ foreach (SfsSource source in this.sources) {
+ sources.add(source.to_stanza_node());
+ }
+ sources_node.sub_nodes = sources;
+ node.put_node(sources_node);
+ return node;
+ }
+ }
+
+ public class SfsSourceAttachment {
+ public string sfs_id;
+ public Gee.List<SfsSource> sources = new Gee.ArrayList<SfsSource>();
+
+ public const string ATTACHMENT_NS_URI = "urn:xmpp:message-attaching:1";
+ public const string ATTACH_TO_STANZA_NAME = "attach-to";
+ public const string SOURCES_STANZA_NAME = "sources";
+ public const string ID_ATTRIBUTE_NAME = "id";
+
+
+ public static SfsSourceAttachment? from_message_stanza(MessageStanza stanza) {
+ StanzaNode? attach_to = stanza.stanza.get_subnode(ATTACH_TO_STANZA_NAME, ATTACHMENT_NS_URI);
+ StanzaNode? sources = stanza.stanza.get_subnode(SOURCES_STANZA_NAME, NS_URI);
+ if (attach_to == null || sources == null) {
+ return null;
+ }
+ string? id = attach_to.get_attribute(ID_ATTRIBUTE_NAME, ATTACHMENT_NS_URI);
+ if (id == null) {
+ return null;
+ }
+ SfsSourceAttachment attachment = new SfsSourceAttachment();
+ attachment.sfs_id = id;
+ Gee.List<HttpSource> http_sources = HttpSource.extract_sources(sources);
+ if (http_sources.is_empty) {
+ return null;
+ }
+ attachment.sources = http_sources;
+ return attachment;
+ }
+
+ public MessageStanza to_message_stanza(Jid to, string message_type) {
+ MessageStanza stanza = new MessageStanza() { to=to, type_=message_type };
+ Xep.MessageProcessingHints.set_message_hint(stanza, Xep.MessageProcessingHints.HINT_STORE);
+
+ StanzaNode attach_to = new StanzaNode.build(ATTACH_TO_STANZA_NAME, ATTACHMENT_NS_URI);
+ attach_to.add_attribute(new StanzaAttribute.build(ATTACHMENT_NS_URI, "id", this.sfs_id));
+ stanza.stanza.put_node(attach_to);
+
+ StanzaNode sources = new StanzaNode.build(SOURCES_STANZA_NAME, NS_URI);
+ Gee.List<StanzaNode> sources_nodes = new Gee.ArrayList<StanzaNode>();
+ foreach (SfsSource source in this.sources) {
+ sources_nodes.add(source.to_stanza_node());
+ }
+ sources.sub_nodes = sources_nodes;
+ stanza.stanza.put_node(sources);
+
+ return stanza;
+ }
+ }
+
+ public class MessageFlag : Xmpp.MessageFlag {
+ public const string ID = "stateless_file_sharing";
+
+ public static MessageFlag? get_flag(MessageStanza message) {
+ return (MessageFlag) message.get_flag(NS_URI, ID);
+ }
+
+ public override string get_ns() {
+ return NS_URI;
+ }
+
+ public override string get_id() {
+ return ID;
+ }
+ }
+
+ public class Module : XmppStreamModule {
+ public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "stateless_file_sharing");
+
+ public signal void received_sfs(Jid from, Jid to, SfsElement sfs_element, MessageStanza message);
+ public signal void received_sfs_attachment(Jid from, Jid to, SfsSourceAttachment attachment, MessageStanza message);
+
+ public void send_stateless_file_transfer(XmppStream stream, MessageStanza sfs_message, SfsElement sfs_element) {
+ StanzaNode sfs_node = sfs_element.to_stanza_node();
+ printerr(sfs_node.to_ansi_string(true));
+
+ sfs_message.stanza.put_node(sfs_node);
+ printerr("Sending message:\n");
+ printerr(sfs_message.stanza.to_ansi_string(true));
+ stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, sfs_message);
+ }
+
+ public void send_stateless_file_transfer_attachment(XmppStream stream, Jid to, string message_type, SfsSourceAttachment attachment) {
+ MessageStanza message = attachment.to_message_stanza(to, message_type);
+
+ printerr("Sending message:\n");
+ printerr(message.stanza.to_ansi_string(true));
+ stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
+ }
+
+ private void on_received_message(XmppStream stream, MessageStanza message) {
+ StanzaNode? sfs_node = message.stanza.get_subnode(STANZA_NAME, NS_URI);
+ if (sfs_node != null) {
+ SfsElement? sfs_element = SfsElement.from_stanza_node(sfs_node);
+ if (sfs_element == null) {
+ return;
+ }
+ message.add_flag(new MessageFlag());
+ received_sfs(message.from, message.to, sfs_element, message);
+ }
+ SfsSourceAttachment? attachment = SfsSourceAttachment.from_message_stanza(message);
+ if (attachment != null) {
+ received_sfs_attachment(message.from, message.to, attachment, message);
+ }
+
+ }
+
+ public override void attach(XmppStream stream) {
+ stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message);
+ }
+
+ public override void detach(XmppStream stream) {
+ stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message);
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return IDENTITY.id; }
+ }
+}