aboutsummaryrefslogtreecommitdiff
path: root/libdino/src/service/call_state.vala
blob: 7d205f7f090ec396ea335c77d3317a586d2e0c3a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
using Dino.Entities;
using Gee;
using Xmpp;

public class Dino.CallState : Object {

    public signal void terminated(Jid who_terminated, string? reason_name, string? reason_text);
    public signal void peer_joined(Jid jid, PeerState peer_state);
    public signal void peer_left(Jid jid, PeerState peer_state, string? reason_name, string? reason_text);

    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; }

    public bool we_should_send_audio { get; set; default=false; }
    public bool we_should_send_video { get; set; default=false; }
    public HashMap<Jid, PeerState> peers = new HashMap<Jid, PeerState>(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;

        if (call.direction == Call.DIRECTION_OUTGOING) {
            accepted = true;

            Timeout.add_seconds(30, () => {
                if (this == null) return false; // TODO enough?
                if (call.state == Call.State.ESTABLISHING) {
                    call.state = Call.State.MISSED;
                    terminated(call.account.bare_jid, null, null);
                }
                return false;
            });
        }
    }

    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<Jid> 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;
        add_peer(peer_state);
        return peer_state;
    }

    internal void add_peer(PeerState peer) {
        call.add_peer(peer.jid.bare_jid);
        connect_peer_signals(peer);
        peer_joined(peer.jid, peer);
    }

    public void accept() {
        accepted = true;
        call.state = Call.State.ESTABLISHING;

        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);
            join_group_call.begin(invited_to_group_call);
        } else {
            foreach (PeerState peer in peers.values) {
                peer.accept();
            }
        }
    }

    public void reject() {
        call.state = Call.State.DECLINED;

        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);
        }
        var peers_cpy = new ArrayList<PeerState>();
        peers_cpy.add_all(peers.values);
        foreach (PeerState peer in peers_cpy) {
            peer.reject();
        }
        terminated(call.account.bare_jid, null, null);
    }

    public void end() {
        var peers_cpy = new ArrayList<PeerState>();
        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);
            }
            call.state = Call.State.ENDED;
        } else if (call.state == Call.State.RINGING) {
            foreach (PeerState peer in peers_cpy) {
                peer.end(Xep.Jingle.ReasonElement.CANCEL);
            }
            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);
            }
            call.state = Call.State.MISSED;
        } else {
            return;
        }

        call.end_time = new DateTime.now_utc();

        terminated(call.account.bare_jid, null, null);
    }

    public void mute_own_audio(bool mute) {
        we_should_send_audio = !mute;
        foreach (PeerState peer in peers.values) {
            peer.mute_own_audio(mute);
        }
    }

    public void mute_own_video(bool mute) {
        we_should_send_video = !mute;
        foreach (PeerState peer in peers.values) {
            peer.mute_own_video(mute);
        }
    }

    public bool should_we_send_video() {
        return we_should_send_video;
    }

    public async void invite_to_call(Jid invitee) {
        if (this.group_call == null) yield convert_into_group_call();
        if (this.group_call == null) return;

        XmppStream stream = stream_interactor.get_stream(call.account);
        if (stream == null) return;

        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);

        // If the peer hasn't accepted within a minute, retract the invite
        Timeout.add_seconds(60, () => {
            if (this == null) return false;

            bool contains_peer = false;
            foreach (Jid peer in peers.keys) {
                if (peer.equals_bare(invitee)) {
                    contains_peer = true;
                }
            }

            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);
                stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none");
            }
            return false;
        });
    }

    internal void rename_peer(Jid from_jid, Jid to_jid) {
        debug("[%s] Renaming %s to %s exists %s", call.account.bare_jid.to_string(), from_jid.to_string(), to_jid.to_string(), peers.has_key(from_jid).to_string());
        PeerState? peer_state = peers[from_jid];
        if (peer_state == null) return;

        // Adjust the internal mapping of this `PeerState` object
        peers.unset(from_jid);
        peers[to_jid] = peer_state;
        peer_state.jid = to_jid;
    }

    private void on_call_terminated(Jid who_terminated, bool we_terminated, string? reason_name, string? reason_text) {
        if (call.state == Call.State.RINGING || call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
            call.end_time = new DateTime.now_utc();
        }
        if (call.state == Call.State.IN_PROGRESS) {
            call.state = Call.State.ENDED;
        } else if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
            if (reason_name == Xep.Jingle.ReasonElement.DECLINE) {
                call.state = Call.State.DECLINED;
            } else {
                call.state = Call.State.FAILED;
            }
        }

        terminated(who_terminated, reason_name, reason_text);
    }

    private void connect_peer_signals(PeerState peer_state) {
        peers[peer_state.jid] = peer_state;

        this.bind_property("we-should-send-audio", peer_state, "we-should-send-audio", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
        this.bind_property("we-should-send-video", peer_state, "we-should-send-video", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
        this.bind_property("group-call", peer_state, "group-call", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);

        peer_state.session_terminated.connect((we_terminated, reason_name, reason_text) => {
            peers.unset(peer_state.jid);
            debug("[%s] Peer left %s left %i", call.account.bare_jid.to_string(), peer_state.jid.to_string(), peers.size);

            if (peers.is_empty) {
                if (group_call != null) group_call.leave(stream_interactor.get_stream(call.account));
                on_call_terminated(peer_state.jid, we_terminated, reason_name, reason_text);
            } else {
                peer_left(peer_state.jid, peer_state, reason_name, reason_text);
            }
        });
    }

    public async void convert_into_group_call() {
        XmppStream stream = stream_interactor.get_stream(call.account);
        if (stream == null) return;

        Jid? muc_jid = null;
        if (muc_jid == null) {
            warning("Failed to initiate group call: MUC server not known.");
            return;
        }

        muc_jid = new Jid("%08x@".printf(Random.next_int()) + muc_jid.to_string()); // TODO longer?

        debug("[%s] Converting call to groupcall %s", call.account.bare_jid.to_string(), muc_jid.to_string());
        yield join_group_call(muc_jid);

        Xep.DataForms.DataForm? data_form = yield stream_interactor.get_module(MucManager.IDENTITY).get_config_form(call.account, muc_jid);
        if (data_form == null) return;

        foreach (Xep.DataForms.DataForm.Field field in data_form.fields) {
            switch (field.var) {
                case "muc#roomconfig_allowinvites":
                    if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
                        ((Xep.DataForms.DataForm.BooleanField) field).value = true;
                    }
                    break;
                case "muc#roomconfig_persistentroom":
                    if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
                        ((Xep.DataForms.DataForm.BooleanField) field).value = false;
                    }
                    break;
                case "muc#roomconfig_membersonly":
                    if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
                        ((Xep.DataForms.DataForm.BooleanField) field).value = true;
                    }
                    break;
                case "muc#roomconfig_whois":
                    if (field.type_ == Xep.DataForms.DataForm.Type.LIST_SINGLE) {
                        ((Xep.DataForms.DataForm.ListSingleField) field).value = "anyone";
                    }
                    break;
            }
        }
        yield stream_interactor.get_module(MucManager.IDENTITY).set_config_form(call.account, muc_jid, data_form);

        foreach (Jid peer_jid in peers.keys) {
            debug("[%s] Group call inviting %s", call.account.bare_jid.to_string(), peer_jid.to_string());
            yield invite_to_call(peer_jid);
        }
    }

    public async void join_group_call(Jid muc_jid) {
        debug("[%s] Joining group call %s", call.account.bare_jid.to_string(), muc_jid.to_string());
        XmppStream stream = stream_interactor.get_stream(call.account);
        if (stream == null) return;

        this.group_call = yield stream.get_module(Xep.Muji.Module.IDENTITY).join_call(stream, muc_jid, we_should_send_video);
        if (this.group_call == null) {
            warning("[%s] Couldn't join MUJI MUC", call.account.bare_jid.to_string());
            return;
        }

        this.group_call.peer_joined.connect((jid) => {
            debug("[%s] Group call peer joined: %s", call.account.bare_jid.to_string(), jid.to_string());

            // Newly joined peers have to call us, not the other way round
            // Maybe they called us already. Accept the call.
            // (Except for the first peer, we already have a connection to that one.)
            if (peers.has_key(jid)) {
                if (!peers[jid].first_peer) {
                    peers[jid].accept();
                }
                // else: Connection to first peer already active
            } else {
                var peer_state = new PeerState(jid, call, stream_interactor);
                peer_state.waiting_for_inbound_muji_connection = true;
                debug("[%s] Waiting for call from %s", call.account.bare_jid.to_string(), jid.to_string());
                add_peer(peer_state);
            }
        });

        this.group_call.peer_left.connect((jid) => {
            debug("[%s] Group call peer left: %s", call.account.bare_jid.to_string(), jid.to_string());
            if (!peers.has_key(jid)) return;
            // end() will in the end cause a `peer_left` signal and removal from `peers`
            peers[jid].end(Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
        });

        // Call all peers that are in the room already
        foreach (Jid peer_jid in group_call.peers_to_connect_to) {
            // Don't establish connection if we have one already (the person that invited us to the call)
            if (peers.has_key(peer_jid)) continue;

            debug("[%s] Calling %s because they were in the MUC already", call.account.bare_jid.to_string(), peer_jid.to_string());

            PeerState peer_state = new PeerState(peer_jid, call, stream_interactor);
            add_peer(peer_state);
            peer_state.call_resource.begin(peer_jid);
        }

        debug("[%s] Finished joining MUJI muc %s", call.account.bare_jid.to_string(), muc_jid.to_string());
    }
}