diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala index 5221a807..3b48f664 100644 --- a/libdino/src/entity/call.vala +++ b/libdino/src/entity/call.vala @@ -39,11 +39,6 @@ namespace Dino.Entities { id = row[db.call.id]; account = db.get_account_by_id(row[db.call.account_id]); - counterpart = db.get_jid_by_id(row[db.call.counterpart_id]); - string counterpart_resource = row[db.call.counterpart_resource]; - if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource); - counterparts.add(counterpart); - string our_resource = row[db.call.our_resource]; if (our_resource != null) { ourpart = account.bare_jid.with_resource(our_resource); @@ -66,6 +61,14 @@ namespace Dino.Entities { if (counterpart == null) counterpart = peer; } + counterpart = db.get_jid_by_id(row[db.call.counterpart_id]); + string counterpart_resource = row[db.call.counterpart_resource]; + if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource); + if (counterparts.is_empty) { + counterparts.add(counterpart); + counterpart = counterpart; + } + notify.connect(on_update); } diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala index 385cf9dc..51563552 100644 --- a/libdino/src/service/call_state.vala +++ b/libdino/src/service/call_state.vala @@ -11,6 +11,7 @@ public class Dino.CallState : Object { public StreamInteractor stream_interactor; public Call call; public Xep.Muji.GroupCall? group_call { get; set; } + public Jid? parent_muc { get; set; } public Jid? invited_to_group_call = null; public Jid? group_call_inviter = null; public bool accepted { get; private set; default=false; } @@ -19,6 +20,8 @@ public class Dino.CallState : Object { public bool we_should_send_video { get; set; default=false; } public HashMap peers = new HashMap(Jid.hash_func, Jid.equals_func); + private string message_type = Xmpp.MessageStanza.TYPE_CHAT; + public CallState(Call call, StreamInteractor stream_interactor) { this.call = call; this.stream_interactor = stream_interactor; @@ -37,6 +40,29 @@ public class Dino.CallState : Object { } } + internal async void initiate_groupchat_call(Jid muc) { + parent_muc = muc; + message_type = Xmpp.MessageStanza.TYPE_GROUPCHAT; + + if (this.group_call == null) yield convert_into_group_call(); + if (this.group_call == null) return; + // The user might have retracted the call in the meanwhile + if (this.call.state != Call.State.RINGING) return; + + XmppStream stream = stream_interactor.get_stream(call.account); + if (stream == null) return; + + Gee.List occupants = stream_interactor.get_module(MucManager.IDENTITY).get_other_occupants(muc, call.account); + foreach (Jid occupant in occupants) { + Jid? real_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(occupant, call.account); + if (real_jid == null) continue; + debug(@"Adding MUC member as MUJI MUC owner %s", real_jid.bare_jid.to_string()); + 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); + } + internal PeerState set_first_peer(Jid peer) { var peer_state = new PeerState(peer, call, stream_interactor); peer_state.first_peer = true; @@ -57,7 +83,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); + stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_accept_to_peer(stream, group_call_inviter, invited_to_group_call, message_type); join_group_call.begin(invited_to_group_call); } else { foreach (PeerState peer in peers.values) { @@ -73,7 +99,7 @@ public class Dino.CallState : Object { 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); + stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_peer(stream, group_call_inviter, invited_to_group_call, message_type); } var peers_cpy = new ArrayList(); peers_cpy.add_all(peers.values); @@ -87,6 +113,10 @@ public class Dino.CallState : Object { var peers_cpy = new ArrayList(); peers_cpy.add_all(peers.values); + if (group_call != null) { + stream_interactor.get_module(MucManager.IDENTITY).part(call.account, group_call.muc_jid); + } + if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) { foreach (PeerState peer in peers_cpy) { peer.end(Xep.Jingle.ReasonElement.SUCCESS); @@ -96,6 +126,11 @@ public class Dino.CallState : Object { foreach (PeerState peer in peers_cpy) { peer.end(Xep.Jingle.ReasonElement.CANCEL); } + if (parent_muc != 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); + } call.state = Call.State.MISSED; } else { return; diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 3d1ed7e8..629d8074 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -39,7 +39,8 @@ namespace Dino { Call call = new Call(); call.direction = Call.DIRECTION_OUTGOING; call.account = conversation.account; - call.add_peer(conversation.counterpart); + // TODO we should only do that for Conversation.Type.CHAT, but the database currently requires a counterpart from the start + call.counterpart = conversation.counterpart; call.ourpart = conversation.account.full_jid; call.time = call.local_time = call.end_time = new DateTime.now_utc(); call.state = Call.State.RINGING; @@ -50,9 +51,14 @@ namespace Dino { call_state.we_should_send_video = video; call_state.we_should_send_audio = true; connect_call_state_signals(call_state); - PeerState peer_state = call_state.set_first_peer(conversation.counterpart); - yield peer_state.initiate_call(conversation.counterpart); + if (conversation.type_ == Conversation.Type.CHAT) { + call.add_peer(conversation.counterpart); + PeerState peer_state = call_state.set_first_peer(conversation.counterpart); + yield peer_state.initiate_call(conversation.counterpart); + } else { + call_state.initiate_groupchat_call(conversation.counterpart); + } conversation.last_active = call.time; @@ -63,7 +69,12 @@ namespace Dino { public async bool can_do_audio_calls_async(Conversation conversation) { if (!can_do_audio_calls()) return false; - return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); + + if (conversation.type_ == Conversation.Type.CHAT) { + return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); + } else { + return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart); + } } private bool can_do_audio_calls() { @@ -75,7 +86,12 @@ namespace Dino { public async bool can_do_video_calls_async(Conversation conversation) { if (!can_do_video_calls()) return false; - return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); + + if (conversation.type_ == Conversation.Type.CHAT) { + return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); + } else { + return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart); + } } private bool can_do_video_calls() { @@ -237,17 +253,24 @@ namespace Dino { private CallState? get_call_state_for_groupcall(Account account, Jid muc_jid) { foreach (CallState call_state in call_states.values) { - if (call_state.group_call != null && call_state.call.account.equals(account) && call_state.group_call.muc_jid.equals(muc_jid)) { - return call_state; - } + 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; } return null; } - private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, Gee.List descriptions) { - debug("[%s] on_muji_call_received", account.bare_jid.to_string()); + private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, Gee.List descriptions, 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) && call.counterparts.contains(inviter_jid) && call_states[call].accepted) { + 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.equals_bare(inviter_jid)) return; + + if (call.counterparts.contains(inviter_jid) && call_states[call].accepted) { // A call is converted into a group call. yield call_states[call].join_group_call(muc_jid); return; @@ -373,11 +396,11 @@ namespace Dino { }); 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) => { + 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); + on_muji_call_received.begin(account, inviter_jid, muc_jid, descriptions, message_type); }); - muji_meta_module.call_accepted.connect((from_jid, muc_jid) => { + muji_meta_module.call_accepted.connect((from_jid, muc_jid, message_type) => { if (!from_jid.equals_bare(account.bare_jid)) return; // We accepted the call from another device @@ -387,17 +410,24 @@ namespace Dino { call_state.call.state = Call.State.OTHER_DEVICE; remove_call_from_datastructures(call_state.call); }); - muji_meta_module.call_retracted.connect((from_jid, muc_jid) => { + muji_meta_module.call_retracted.connect((from_jid, to_jid, muc_jid, 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); if (call_state == null) return; + if (call_state.call.state != Call.State.RINGING) { + debug("%s tried to retract a call that's in state %s. Ignoring.", from_jid.to_string(), call_state.call.state.to_string()); + return; + } + + // TODO prevent other MUC occupants from retracting a call + 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) => { + muji_meta_module.call_rejected.connect((from_jid, to_jid, muc_jid, message_type) => { if (from_jid.equals_bare(account.bare_jid)) return; debug(@"[%s] rejected our MUJI invite to %s", account.bare_jid.to_string(), from_jid.to_string(), muc_jid.to_string()); }); diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala index 3b3a5b39..3fa399a6 100644 --- a/main/src/ui/conversation_titlebar/call_entry.vala +++ b/main/src/ui/conversation_titlebar/call_entry.vala @@ -115,17 +115,13 @@ namespace Dino.Ui { return; } - if (conversation.type_ == Conversation.Type.CHAT) { - Conversation conv_bak = conversation; - bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation); - bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation); - if (conv_bak != conversation) return; + Conversation conv_bak = conversation; + bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation); + bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation); + if (conv_bak != conversation) return; - visible = audio_works; - video_button.visible = video_works; - } else { - visible = false; - } + visible = audio_works; + video_button.visible = video_works; } public new void unset_conversation() { } diff --git a/xmpp-vala/src/module/xep/muji_meta.vala b/xmpp-vala/src/module/xep/muji_meta.vala index 4452e611..89a0e8de 100644 --- a/xmpp-vala/src/module/xep/muji_meta.vala +++ b/xmpp-vala/src/module/xep/muji_meta.vala @@ -6,48 +6,48 @@ namespace Xmpp.Xep.MujiMeta { 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); - public signal void call_retracted(Jid from, Jid to, Jid muc_jid); - public signal void call_accepted(Jid from, Jid muc_jid); - public signal void call_rejected(Jid from, Jid to, Jid muc_jid); + 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) { + public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video, string? message_type = null) { 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_=MessageStanza.TYPE_CHAT }; + 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) { - send_jmi_message(stream, "retract", invitee, muc_jid); + public void send_invite_retract_to_peer(XmppStream stream, Jid invitee, Jid muc_jid, string? message_type = null) { + send_jmi_message(stream, "retract", invitee, muc_jid, message_type); } - public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid) { - send_jmi_message(stream, "accept", invitor, muc_jid); + public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string? message_type = null) { + 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); } - public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid) { - send_jmi_message(stream, "reject", invitor, muc_jid); + public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string? message_type = null) { + 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); } - private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc) { + private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc, string? message_type = null) { 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_=MessageStanza.TYPE_CHAT }; + 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); } @@ -80,7 +80,7 @@ namespace Xmpp.Xep.MujiMeta { switch (mi_node.name) { case "accept": case "proceed": - call_accepted(message.from, muc_jid); + call_accepted(message.from, muc_jid, message.type_); break; case "propose": ArrayList descriptions = new ArrayList(); @@ -91,14 +91,14 @@ namespace Xmpp.Xep.MujiMeta { } if (descriptions.size > 0) { - call_proposed(message.from, message.to, muc_jid, descriptions); + call_proposed(message.from, message.to, muc_jid, descriptions, message.type_); } break; case "retract": - call_retracted(message.from, message.to, muc_jid); + call_retracted(message.from, message.to, muc_jid, message.type_); break; case "reject": - call_rejected(message.from, message.to, muc_jid); + call_rejected(message.from, message.to, muc_jid, message.type_); break; } }