From 071d925e370b2238a9804733a484fe4ec9432f44 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Wed, 19 Jan 2022 16:54:56 +0100 Subject: [PATCH] Add support for call invite messages As of https://github.com/xsf/xeps/pull/1155 --- libdino/src/service/call_state.vala | 15 +-- libdino/src/service/calls.vala | 75 +++++++---- libdino/src/service/module_manager.vala | 2 +- main/src/ui/notifier_freedesktop.vala | 4 +- xmpp-vala/CMakeLists.txt | 2 +- .../xep/0334_message_processing_hints.vala | 8 +- .../module/xep/0353_call_invite_message.vala | 93 ++++++++++++++ xmpp-vala/src/module/xep/muji_meta.vala | 117 ------------------ 8 files changed, 161 insertions(+), 155 deletions(-) create mode 100644 xmpp-vala/src/module/xep/0353_call_invite_message.vala delete mode 100644 xmpp-vala/src/module/xep/muji_meta.vala diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala index 03ee9595..f49a77da 100644 --- a/libdino/src/service/call_state.vala +++ b/libdino/src/service/call_state.vala @@ -15,6 +15,7 @@ public class Dino.CallState : Object { public Jid? parent_muc { get; set; } public Jid? invited_to_group_call = null; public Jid? group_call_inviter = null; + public string? invite_id = null; public bool accepted { get; private set; default=false; } public bool we_should_send_audio { get; set; default=false; } @@ -61,7 +62,7 @@ public class Dino.CallState : Object { yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, real_jid.bare_jid, null, "owner"); } - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, muc, group_call.muc_jid, we_should_send_video, message_type); + stream.get_module(Xep.CallInvites.Module.IDENTITY).send_invite(stream, muc, group_call.muc_jid, we_should_send_video, message_type); } internal PeerState set_first_peer(Jid peer) { @@ -84,7 +85,7 @@ public class Dino.CallState : Object { if (invited_to_group_call != null) { XmppStream stream = stream_interactor.get_stream(call.account); if (stream == null) return; - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_accept_to_peer(stream, group_call_inviter, invited_to_group_call, message_type); + stream.get_module(Xep.CallInvites.Module.IDENTITY).send_accept(stream, group_call_inviter, invite_id, message_type); join_group_call.begin(invited_to_group_call); } else { foreach (PeerState peer in peers.values) { @@ -99,8 +100,7 @@ public class Dino.CallState : Object { if (invited_to_group_call != null) { XmppStream stream = stream_interactor.get_stream(call.account); if (stream == null) return; - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_self(stream, invited_to_group_call); - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_peer(stream, group_call_inviter, invited_to_group_call, message_type); + stream.get_module(Xep.CallInvites.Module.IDENTITY).send_reject(stream, invited_to_group_call, invite_id, message_type); } var peers_cpy = new ArrayList(); peers_cpy.add_all(peers.values); @@ -133,7 +133,7 @@ public class Dino.CallState : Object { if (parent_muc != null && group_call != null) { XmppStream stream = stream_interactor.get_stream(call.account); if (stream == null) return; - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, parent_muc, group_call.muc_jid, message_type); + stream.get_module(Xep.CallInvites.Module.IDENTITY).send_retract(stream, parent_muc, invite_id, message_type); } call.state = Call.State.MISSED; } else { @@ -172,9 +172,10 @@ public class Dino.CallState : Object { debug("[%s] Inviting to muji call %s", call.account.bare_jid.to_string(), invitee.to_string()); yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, invitee, null, "owner"); - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, invitee, group_call.muc_jid, we_should_send_video, message_type); + stream.get_module(Xep.CallInvites.Module.IDENTITY).send_invite(stream, invitee, group_call.muc_jid, we_should_send_video, "chat"); // If the peer hasn't accepted within a minute, retract the invite + // TODO this should be unset when we retract the invite. otherwise a second invite attempt might break due to this Timeout.add_seconds(60, () => { if (this == null) return false; @@ -187,7 +188,7 @@ public class Dino.CallState : Object { if (!contains_peer) { debug("[%s] Retracting invite to %s from %s", call.account.bare_jid.to_string(), group_call.muc_jid.to_string(), invitee.to_string()); - stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, invitee, group_call.muc_jid, message_type); +// stream.get_module(Xep.CallInvites.Module.IDENTITY).send_retract(stream, invitee, invite_id); stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none"); } return false; diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 44790014..7b6a2628 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -79,7 +79,10 @@ namespace Dino { return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); } else { bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart); - return is_private && can_initiate_groupcall(conversation.account); + EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); + bool supports_ussid = yield entity_info.has_feature(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI); + bool supports_mam = yield entity_info.has_feature(conversation.account, conversation.counterpart.bare_jid, Xep.MessageArchiveManagement.NS_URI_2); + return is_private && (supports_ussid || supports_mam) && can_initiate_groupcall(conversation.account); } } @@ -235,35 +238,43 @@ namespace Dino { return peer_state; } - private CallState? get_call_state_for_groupcall(Account account, Jid muc_jid) { + private CallState? get_call_state_by_invite_id(Account account, Jid peer_jid, string invite_id) { foreach (CallState call_state in call_states.values) { if (!call_state.call.account.equals(account)) continue; - if (call_state.group_call != null && call_state.group_call.muc_jid.equals(muc_jid)) return call_state; - if (call_state.invited_to_group_call != null && call_state.invited_to_group_call.equals(muc_jid)) return call_state; + if (call_state.group_call != null && call_state.invite_id == invite_id) { + foreach (Jid jid in call_state.peers.keys) { + if (jid.equals(peer_jid)) { + return call_state; + } + } + } + if (call_state.invited_to_group_call != null && call_state.invited_to_group_call.equals(peer_jid)) return call_state; } return null; } - private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, Gee.List descriptions, string message_type) { + private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, string invite_id, bool video, string message_type) { debug("[%s] Muji call received from %s for MUC %s, type %s", account.bare_jid.to_string(), inviter_jid.to_string(), muc_jid.to_string(), message_type); foreach (Call call in call_states.keys) { if (!call.account.equals(account)) return; - // We already know the call; this is a reflection of our own invite - if (call_states[call].parent_muc != null && call_states[call].parent_muc.equals_bare(inviter_jid)) return; + CallState call_state = call_states[call]; - if (call.counterparts.contains(inviter_jid) && call_states[call].accepted) { + // If this is a MUC reflection of our own invite, store the sid assigned by the MUC + if (call_state.parent_muc != null && call_state.parent_muc.equals_bare(inviter_jid)) { + call_state.invite_id = invite_id; + return; + } + + if (call.counterparts.contains(inviter_jid) && call_state.accepted) { // A call is converted into a group call. - yield call_states[call].join_group_call(muc_jid); + yield call_state.join_group_call(muc_jid); return; } } - bool audio_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "audio"); - bool video_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "video"); - Call call = new Call(); call.direction = Call.DIRECTION_INCOMING; call.ourpart = account.full_jid; @@ -280,12 +291,13 @@ namespace Dino { CallState call_state = new CallState(call, stream_interactor); connect_call_state_signals(call_state); call_state.we_should_send_audio = true; - call_state.we_should_send_video = video_requested; + call_state.we_should_send_video = video; call_state.invited_to_group_call = muc_jid; call_state.group_call_inviter = inviter_jid; + call_state.invite_id = invite_id; debug("[%s] on_muji_call_received accepting", account.bare_jid.to_string()); - call_incoming(call_state.call, call_state, conversation, video_requested); + call_incoming(call_state.call, call_state, conversation, video); } private void remove_call_from_datastructures(Call call) { @@ -379,26 +391,43 @@ namespace Dino { remove_call_from_datastructures(call); }); - Xep.MujiMeta.Module muji_meta_module = stream_interactor.module_manager.get_module(account, Xep.MujiMeta.Module.IDENTITY); - muji_meta_module.call_proposed.connect((inviter_jid, to, muc_jid, descriptions, message_type) => { - if (inviter_jid.equals_bare(account.bare_jid)) return; - on_muji_call_received.begin(account, inviter_jid, muc_jid, descriptions, message_type); + Xep.CallInvites.Module call_invites_module = stream_interactor.module_manager.get_module(account, Xep.CallInvites.Module.IDENTITY); + call_invites_module.call_proposed.connect((from_jid, to_jid, video, join_methods, message_stanza) => { + if (from_jid.equals_bare(account.bare_jid)) return; + foreach (StanzaNode join_method_node in join_methods) { + if (join_method_node.ns_uri == Xep.Muji.NS_URI) { + string? room_jid_str = join_method_node.get_attribute("room"); + if (room_jid_str == null) return; + Jid room_jid = new Jid(room_jid_str); + string? invite_id = null; + if (message_stanza.type_ == Xmpp.MessageStanza.TYPE_GROUPCHAT) { + invite_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message_stanza, from_jid.bare_jid); + } else { + invite_id = message_stanza.id; + } + if (invite_id == null) { + warning("Got call invite without ID"); + return; + } + on_muji_call_received.begin(account, from_jid, room_jid, id, video, message_stanza.type_); + } + } }); - muji_meta_module.call_accepted.connect((from_jid, muc_jid, message_type) => { + call_invites_module.call_accepted.connect((from_jid, invite_id, message_type) => { if (!from_jid.equals_bare(account.bare_jid)) return; // We accepted the call from another device - CallState? call_state = get_call_state_for_groupcall(account, muc_jid); + CallState? call_state = get_call_state_by_invite_id(account, from_jid, invite_id); if (call_state == null) return; call_state.call.state = Call.State.OTHER_DEVICE; remove_call_from_datastructures(call_state.call); }); - muji_meta_module.call_retracted.connect((from_jid, to_jid, muc_jid, message_type) => { + call_invites_module.call_retracted.connect((from_jid, to_jid, invite_id, message_type) => { if (from_jid.equals_bare(account.bare_jid)) return; // The call was retracted by the counterpart - CallState? call_state = get_call_state_for_groupcall(account, muc_jid); + CallState? call_state = get_call_state_by_invite_id(account, from_jid, invite_id); if (call_state == null) return; if (call_state.call.state != Call.State.RINGING) { @@ -411,7 +440,7 @@ namespace Dino { call_state.call.state = Call.State.MISSED; remove_call_from_datastructures(call_state.call); }); - muji_meta_module.call_rejected.connect((from_jid, to_jid, muc_jid, message_type) => { + call_invites_module.call_rejected.connect((from_jid, to_jid, muc_jid, message_type) => { if (from_jid.equals_bare(account.bare_jid)) return; debug(@"[%s] %s rejected our MUJI invite to %s", account.bare_jid.to_string(), from_jid.to_string(), muc_jid.to_string()); }); diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index 39ed8a7c..b54b1a1e 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -82,7 +82,7 @@ public class ModuleManager { module_map[account].add(new Xep.JingleMessageInitiation.Module()); module_map[account].add(new Xep.JingleRawUdp.Module()); module_map[account].add(new Xep.Muji.Module()); - module_map[account].add(new Xep.MujiMeta.Module()); + module_map[account].add(new Xep.CallInvites.Module()); module_map[account].add(new Xep.Coin.Module()); initialize_account_modules(account, module_map[account]); } diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index dfc5b28b..17de0ce9 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -267,7 +267,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { if (content_notifications != null) { foreach (uint32 id in content_notifications.values) { try { - yield dbus_notifications.close_notification(id); + dbus_notifications.close_notification.begin(id); } catch (Error e) { } } content_notifications.clear(); @@ -277,7 +277,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { public async void retract_conversation_notifications(Conversation conversation) { if (content_notifications.has_key(conversation)) { try { - yield dbus_notifications.close_notification(content_notifications[conversation]); + dbus_notifications.close_notification.begin(content_notifications[conversation]); } catch (Error e) { } } content_notifications.unset(conversation); diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 27f0d408..5b767448 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -132,12 +132,12 @@ SOURCES "src/module/xep/0333_chat_markers.vala" "src/module/xep/0334_message_processing_hints.vala" "src/module/xep/0353_jingle_message_initiation.vala" + "src/module/xep/0353_call_invite_message.vala" "src/module/xep/0359_unique_stable_stanza_ids.vala" "src/module/xep/0363_http_file_upload.vala" "src/module/xep/0380_explicit_encryption.vala" "src/module/xep/0391_jingle_encrypted_transports.vala" "src/module/xep/0410_muc_self_ping.vala" - "src/module/xep/muji_meta.vala" "src/module/xep/pixbuf_storage.vala" "src/util.vala" diff --git a/xmpp-vala/src/module/xep/0334_message_processing_hints.vala b/xmpp-vala/src/module/xep/0334_message_processing_hints.vala index 352e03ac..09fd7dd4 100644 --- a/xmpp-vala/src/module/xep/0334_message_processing_hints.vala +++ b/xmpp-vala/src/module/xep/0334_message_processing_hints.vala @@ -2,10 +2,10 @@ namespace Xmpp.Xep.MessageProcessingHints { private const string NS_URI = "urn:xmpp:hints"; -private const string HINT_NO_PERMANENT_STORE = "no-permanent-store"; -private const string HINT_NO_STORE = "no-store"; -private const string HINT_NO_COPY = "no-copy"; -private const string HINT_STORE = "store"; +public const string HINT_NO_PERMANENT_STORE = "no-permanent-store"; +public const string HINT_NO_STORE = "no-store"; +public const string HINT_NO_COPY = "no-copy"; +public const string HINT_STORE = "store"; public static void set_message_hint(MessageStanza message, string message_hint) { StanzaNode hint_node = (new StanzaNode.build(message_hint, NS_URI)).add_self_xmlns(); diff --git a/xmpp-vala/src/module/xep/0353_call_invite_message.vala b/xmpp-vala/src/module/xep/0353_call_invite_message.vala new file mode 100644 index 00000000..806db5e7 --- /dev/null +++ b/xmpp-vala/src/module/xep/0353_call_invite_message.vala @@ -0,0 +1,93 @@ +using Gee; +namespace Xmpp.Xep.CallInvites { + + public const string NS_URI = "urn:xmpp:call-invites:0"; + + public class Module : XmppStreamModule { + public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "call_invites"); + + public signal void call_proposed(Jid from, Jid to, bool video, Gee.List join_methods, MessageStanza message); + public signal void call_retracted(Jid from, Jid to, string invite_id, string message_type); + public signal void call_accepted(Jid from, string invite_id, string message_type); + public signal void call_rejected(Jid from, Jid to, string invite_id, string message_type); + + public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video, string message_type) { + StanzaNode muji_node = new StanzaNode.build("muji", Muji.NS_URI).add_self_xmlns().put_attribute("room", muc_jid.to_string()); + StanzaNode invite_node = new StanzaNode.build("propose", NS_URI).add_self_xmlns() + .put_attribute("video", video.to_string()) + .put_node(muji_node); + MessageStanza invite_message = new MessageStanza() { to=invitee, type_=message_type }; + MessageProcessingHints.set_message_hint(invite_message, MessageProcessingHints.HINT_STORE); + invite_message.stanza.put_node(invite_node); + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, invite_message); + } + + public void send_retract(XmppStream stream, Jid to, string invite_id, string message_type) { + send_message(stream, "retract", to, invite_id, message_type); + } + + public void send_accept(XmppStream stream, Jid to, string invite_id, string message_type) { + send_message(stream, "accept", to, invite_id, message_type); + } + + public void send_reject(XmppStream stream, Jid to, string invite_id, string message_type) { + send_message(stream, "reject", to, invite_id, message_type); + } + + private void send_message(XmppStream stream, string action, Jid to, string invite_id, string message_type) { + StanzaNode inner_node = new StanzaNode.build(action, NS_URI).add_self_xmlns().put_attribute("id", invite_id); + MessageStanza message = new MessageStanza() { to=to, type_=message_type }; + message.stanza.put_node(inner_node); + MessageProcessingHints.set_message_hint(message, MessageProcessingHints.HINT_STORE); + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message); + } + + private void on_received_message(XmppStream stream, MessageStanza message) { + Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message); + if (mam_flag != null) return; + + StanzaNode? relevant_node = null; + + foreach (StanzaNode node in message.stanza.sub_nodes) { + if (node.ns_uri == NS_URI) { + relevant_node = node; + break; + } + } + if (relevant_node == null) return; + + if (relevant_node.name == "propose") { + if (relevant_node.sub_nodes.is_empty) return; + bool video = relevant_node.get_attribute_bool("video", false); + call_proposed(message.from, message.to, video, relevant_node.sub_nodes, message); + return; + } + + string? invite_id = relevant_node.get_attribute("id"); + if (invite_id == null) return; + + switch (relevant_node.name) { + case "accept": + call_accepted(message.from, invite_id, message.type_); + break; + case "retract": + call_retracted(message.from, message.to, invite_id, message.type_); + break; + case "reject": + call_rejected(message.from, message.to, invite_id, message.type_); + break; + } + } + + 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; } + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/muji_meta.vala b/xmpp-vala/src/module/xep/muji_meta.vala deleted file mode 100644 index fa161f28..00000000 --- a/xmpp-vala/src/module/xep/muji_meta.vala +++ /dev/null @@ -1,117 +0,0 @@ -using Gee; -namespace Xmpp.Xep.MujiMeta { - - public const string NS_URI = "http://telepathy.freedesktop.org/muji"; - - public class Module : XmppStreamModule { - public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "muji_meta"); - - public signal void call_proposed(Jid from, Jid to, Jid muc_jid, Gee.List descriptions, string message_type); - public signal void call_retracted(Jid from, Jid to, Jid muc_jid, string message_type); - public signal void call_accepted(Jid from, Jid muc_jid, string message_type); - public signal void call_rejected(Jid from, Jid to, Jid muc_jid, string message_type); - - public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video, string message_type) { - var invite_node = new StanzaNode.build("propose", NS_URI).put_attribute("muc", muc_jid.to_string()); - invite_node.put_node(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio")); - if (video) { - invite_node.put_node(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "video")); - } - var muji_node = new StanzaNode.build("muji", NS_URI).add_self_xmlns().put_node(invite_node); - MessageStanza invite_message = new MessageStanza() { to=invitee, type_=message_type }; - invite_message.stanza.put_node(muji_node); - stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, invite_message); - } - - public void send_invite_retract_to_peer(XmppStream stream, Jid invitee, Jid muc_jid, string message_type) { - send_jmi_message(stream, "retract", invitee, muc_jid, message_type); - } - - public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string message_type) { - send_jmi_message(stream, "accept", invitor, muc_jid, message_type); - } - - public void send_invite_accept_to_self(XmppStream stream, Jid muc_jid) { - send_jmi_message(stream, "accept", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid, MessageStanza.TYPE_CHAT); - } - - public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string message_type) { - send_jmi_message(stream, "reject", invitor, muc_jid, message_type); - } - - public void send_invite_reject_to_self(XmppStream stream, Jid muc_jid) { - send_jmi_message(stream, "reject", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid, MessageStanza.TYPE_CHAT); - } - - private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc, string message_type) { - var jmi_node = new StanzaNode.build(name, NS_URI).add_self_xmlns().put_attribute("muc", muc.to_string()); - var muji_node = new StanzaNode.build("muji", NS_URI).add_self_xmlns().put_node(jmi_node); - - MessageStanza accepted_message = new MessageStanza() { to=to, type_= message_type ?? MessageStanza.TYPE_CHAT }; - accepted_message.stanza.put_node(muji_node); - stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, accepted_message); - } - - private void on_received_message(XmppStream stream, MessageStanza message) { - Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message); - if (mam_flag != null) return; - - var muji_node = message.stanza.get_subnode("muji", NS_URI); - if (muji_node == null) return; - - StanzaNode? mi_node = null; - foreach (StanzaNode node in muji_node.sub_nodes) { - if (node.ns_uri == NS_URI) { - mi_node = node; - } - } - if (mi_node == null) return; - - string? jid_str = mi_node.get_attribute("muc"); - if (jid_str == null) return; - - Jid muc_jid = null; - try { - muc_jid = new Jid(jid_str); - } catch (Error e) { - return; - } - - switch (mi_node.name) { - case "accept": - case "proceed": - call_accepted(message.from, muc_jid, message.type_); - break; - case "propose": - ArrayList descriptions = new ArrayList(); - - foreach (StanzaNode node in mi_node.sub_nodes) { - if (node.name != "description") continue; - descriptions.add(node); - } - - if (descriptions.size > 0) { - call_proposed(message.from, message.to, muc_jid, descriptions, message.type_); - } - break; - case "retract": - call_retracted(message.from, message.to, muc_jid, message.type_); - break; - case "reject": - call_rejected(message.from, message.to, muc_jid, message.type_); - break; - } - } - - 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; } - } -} \ No newline at end of file