1using Gee;
2
3namespace Xmpp.Xep.Muc {
4
5private const string NS_URI = "http://jabber.org/protocol/muc";
6private const string NS_URI_ADMIN = NS_URI + "#admin";
7private const string NS_URI_OWNER = NS_URI + "#owner";
8private const string NS_URI_USER = NS_URI + "#user";
9private const string NS_URI_REQUEST = NS_URI + "#request";
10
11public enum MucEnterError {
12    NONE,
13    PASSWORD_REQUIRED,
14    BANNED,
15    ROOM_DOESNT_EXIST,
16    CREATION_RESTRICTED,
17    USE_RESERVED_ROOMNICK,
18    NOT_IN_MEMBER_LIST,
19    NICK_CONFLICT,
20    OCCUPANT_LIMIT_REACHED,
21}
22
23public enum Affiliation {
24    NONE,
25    ADMIN,
26    MEMBER,
27    OUTCAST,
28    OWNER
29}
30
31public enum Role {
32    NONE,
33    MODERATOR,
34    PARTICIPANT,
35    VISITOR
36}
37
38public enum Feature {
39    REGISTER,
40    ROOMCONFIG,
41    ROOMINFO,
42    HIDDEN,
43    MEMBERS_ONLY,
44    MODERATED,
45    NON_ANONYMOUS,
46    OPEN,
47    PASSWORD_PROTECTED,
48    PERSISTENT,
49    PUBLIC,
50    ROOMS,
51    SEMI_ANONYMOUS,
52    STABLE_ID,
53    TEMPORARY,
54    UNMODERATED,
55    UNSECURED
56}
57
58public class JoinResult {
59    public MucEnterError? muc_error;
60    public string? stanza_error;
61    public string? nick;
62}
63
64public class Module : XmppStreamModule {
65    public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0045_muc_module");
66
67    public signal void received_occupant_affiliation(XmppStream stream, Jid jid, Affiliation? affiliation);
68    public signal void received_occupant_jid(XmppStream stream, Jid jid, Jid? real_jid);
69    public signal void received_occupant_role(XmppStream stream, Jid jid, Role? role);
70    public signal void subject_set(XmppStream stream, string? subject, Jid jid);
71    public signal void invite_received(XmppStream stream, Jid room_jid, Jid from_jid, string? password, string? reason);
72    public signal void voice_request_received(XmppStream stream, Jid room_jid, Jid from_jid, string nick);
73    public signal void room_info_updated(XmppStream stream, Jid muc_jid);
74
75    public signal void self_removed_from_room(XmppStream stream, Jid jid, StatusCode code);
76    public signal void removed_from_room(XmppStream stream, Jid jid, StatusCode? code);
77
78    private ReceivedPipelineListener received_pipeline_listener;
79
80    public Module() {
81        received_pipeline_listener = new ReceivedPipelineListener(this);
82    }
83
84    public async JoinResult? enter(XmppStream stream, Jid bare_jid, string nick, string? password, DateTime? history_since) {
85        try {
86            Presence.Stanza presence = new Presence.Stanza();
87            presence.to = bare_jid.with_resource(nick);
88
89            StanzaNode x_node = new StanzaNode.build("x", NS_URI).add_self_xmlns();
90            if (password != null) {
91                x_node.put_node(new StanzaNode.build("password", NS_URI).put_node(new StanzaNode.text(password)));
92            }
93            if (history_since != null) {
94                StanzaNode history_node = new StanzaNode.build("history", NS_URI);
95                history_node.set_attribute("since", DateTimeProfiles.to_datetime(history_since));
96                x_node.put_node(history_node);
97            }
98            presence.stanza.put_node(x_node);
99
100            stream.get_flag(Flag.IDENTITY).start_muc_enter(bare_jid, presence.id);
101
102            query_room_info.begin(stream, bare_jid);
103            stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence);
104
105            var promise = new Promise<JoinResult?>();
106            stream.get_flag(Flag.IDENTITY).enter_futures[bare_jid] = promise;
107            try {
108                JoinResult? enter_result = yield promise.future.wait_async();
109                stream.get_flag(Flag.IDENTITY).enter_futures.unset(bare_jid);
110                return enter_result;
111            } catch (Gee.FutureError e) {
112                return null;
113            }
114        } catch (InvalidJidError e) {
115            return new JoinResult() { muc_error = MucEnterError.NICK_CONFLICT };
116        }
117    }
118
119    public void exit(XmppStream stream, Jid jid) {
120        try {
121            string nick = stream.get_flag(Flag.IDENTITY).get_muc_nick(jid);
122            Presence.Stanza presence = new Presence.Stanza();
123            presence.to = jid.with_resource(nick);
124            presence.type_ = Presence.Stanza.TYPE_UNAVAILABLE;
125            stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence);
126        } catch (InvalidJidError e) {
127            warning("Tried to leave room with invalid nick: %s", e.message);
128        }
129    }
130
131    public void change_subject(XmppStream stream, Jid jid, string subject) {
132        MessageStanza message = new MessageStanza();
133        message.to = jid;
134        message.type_ = MessageStanza.TYPE_GROUPCHAT;
135        message.stanza.put_node((new StanzaNode.build("subject")).put_node(new StanzaNode.text(subject)));
136        stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
137    }
138
139    public void change_nick(XmppStream stream, Jid jid, string new_nick) {
140        // TODO: Return if successful
141        try {
142            Presence.Stanza presence = new Presence.Stanza();
143            presence.to = jid.with_resource(new_nick);
144            stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence);
145        } catch (InvalidJidError e) {
146            warning("Tried to change nick to invalid nick: %s", e.message);
147        }
148    }
149
150    public void invite(XmppStream stream, Jid to_muc, Jid jid) {
151        MessageStanza message = new MessageStanza();
152        message.to = to_muc;
153        StanzaNode invite_node = new StanzaNode.build("x", NS_URI_USER).add_self_xmlns()
154            .put_node(new StanzaNode.build("invite", NS_URI_USER).put_attribute("to", jid.to_string()));
155        message.stanza.put_node(invite_node);
156        stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
157    }
158
159    public void request_voice(XmppStream stream, Jid to_muc) {
160        MessageStanza message = new MessageStanza() { to=to_muc };
161
162        DataForms.DataForm submit_node = new DataForms.DataForm();
163        submit_node.get_submit_node();
164
165        DataForms.DataForm.Field field_node = new DataForms.DataForm.Field() { var="FORM_TYPE" };
166        field_node.set_value_string(NS_URI_REQUEST);
167
168        DataForms.DataForm.ListSingleField single_field = new DataForms.DataForm.ListSingleField(new StanzaNode.build("field", DataForms.NS_URI)) { var="muc#role", label="Requested role", value="participant" };
169
170        submit_node.add_field(field_node);
171        submit_node.add_field(single_field);
172
173        message.stanza.put_node(submit_node.stanza_node);
174
175        stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
176    }
177
178    public void kick(XmppStream stream, Jid jid, string nick) {
179        change_role(stream, jid, nick, "none");
180    }
181
182    /* XEP 0046: "A user cannot be kicked by a moderator with a lower affiliation." (XEP 0045 8.2) */
183    public bool kick_possible(XmppStream stream, Jid occupant) {
184        try {
185            Jid muc_jid = occupant.bare_jid;
186            Flag flag = stream.get_flag(Flag.IDENTITY);
187            string own_nick = flag.get_muc_nick(muc_jid);
188            Affiliation my_affiliation = flag.get_affiliation(muc_jid, muc_jid.with_resource(own_nick));
189            Affiliation other_affiliation = flag.get_affiliation(muc_jid, occupant);
190            switch (my_affiliation) {
191                case Affiliation.MEMBER:
192                    if (other_affiliation == Affiliation.ADMIN || other_affiliation == Affiliation.OWNER) return false;
193                    break;
194                case Affiliation.ADMIN:
195                    if (other_affiliation == Affiliation.OWNER) return false;
196                    break;
197            }
198            return true;
199        } catch (InvalidJidError e) {
200            warning("Tried to kick with invalid nick: %s", e.message);
201            return false;
202        }
203    }
204
205    public void change_role(XmppStream stream, Jid jid, string nick, string new_role) {
206        StanzaNode query = new StanzaNode.build("query", NS_URI_ADMIN).add_self_xmlns();
207        query.put_node(new StanzaNode.build("item", NS_URI_ADMIN).put_attribute("nick", nick, NS_URI_ADMIN).put_attribute("role", new_role, NS_URI_ADMIN));
208        Iq.Stanza iq = new Iq.Stanza.set(query) { to=jid };
209        stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq);
210    }
211
212    public void change_affiliation(XmppStream stream, Jid jid, string nick, string new_affiliation) {
213        StanzaNode query = new StanzaNode.build("query", NS_URI_ADMIN).add_self_xmlns();
214        query.put_node(new StanzaNode.build("item", NS_URI_ADMIN).put_attribute("nick", nick, NS_URI_ADMIN).put_attribute("affiliation", new_affiliation, NS_URI_ADMIN));
215        Iq.Stanza iq = new Iq.Stanza.set(query) { to=jid };
216        stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq);
217    }
218
219    public async DataForms.DataForm? get_config_form(XmppStream stream, Jid jid) {
220        Iq.Stanza get_iq = new Iq.Stanza.get(new StanzaNode.build("query", NS_URI_OWNER).add_self_xmlns()) { to=jid };
221        Iq.Stanza result_iq = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, get_iq);
222
223        StanzaNode? x_node = result_iq.stanza.get_deep_subnode(NS_URI_OWNER + ":query", DataForms.NS_URI + ":x");
224        if (x_node != null) {
225            DataForms.DataForm data_form = DataForms.DataForm.create_from_node(x_node);
226            return data_form;
227        }
228        return null;
229    }
230
231    public void set_config_form(XmppStream stream, Jid jid, DataForms.DataForm data_form) {
232        StanzaNode stanza_node = new StanzaNode.build("query", NS_URI_OWNER);
233        stanza_node.add_self_xmlns().put_node(data_form.get_submit_node());
234        Iq.Stanza set_iq = new Iq.Stanza.set(stanza_node) { to=jid };
235        stream.get_module(Iq.Module.IDENTITY).send_iq(stream, set_iq);
236    }
237
238    public override void attach(XmppStream stream) {
239        stream.add_flag(new Flag());
240        stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message);
241        stream.get_module(MessageModule.IDENTITY).received_pipeline.connect(received_pipeline_listener);
242        stream.get_module(Presence.Module.IDENTITY).received_presence.connect(check_for_enter_error);
243        stream.get_module(Presence.Module.IDENTITY).received_available.connect(on_received_available);
244        stream.get_module(Presence.Module.IDENTITY).received_unavailable.connect(on_received_unavailable);
245        stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
246    }
247
248    public override void detach(XmppStream stream) {
249        stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message);
250        stream.get_module(MessageModule.IDENTITY).received_pipeline.disconnect(received_pipeline_listener);
251        stream.get_module(Presence.Module.IDENTITY).received_presence.disconnect(check_for_enter_error);
252        stream.get_module(Presence.Module.IDENTITY).received_available.disconnect(on_received_available);
253        stream.get_module(Presence.Module.IDENTITY).received_unavailable.disconnect(on_received_unavailable);
254        stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
255    }
256
257    public override string get_ns() { return NS_URI; }
258    public override string get_id() { return IDENTITY.id; }
259
260    private void on_received_message(XmppStream stream, MessageStanza message) {
261        if (message.type_ == MessageStanza.TYPE_GROUPCHAT) {
262            StanzaNode? subject_node = message.stanza.get_subnode("subject");
263            if (subject_node != null) {
264                string subject = subject_node.get_string_content();
265                stream.get_flag(Flag.IDENTITY).set_muc_subject(message.from, subject);
266                subject_set(stream, subject, message.from);
267            }
268
269            StanzaNode? x_node = message.stanza.get_subnode("x", NS_URI_USER);
270            if (x_node != null) {
271                Gee.List<int> status_codes = get_status_codes(x_node);
272                if (!status_codes.is_empty) {
273                    if (status_codes.contains(StatusCode.CONFIG_CHANGE_NON_PRIVACY) ||
274                            status_codes.contains(StatusCode.NON_ANONYMOUS) ||
275                            status_codes.contains(StatusCode.SEMI_ANONYMOUS)) {
276                        query_room_info.begin(stream, message.from.bare_jid);
277                    }
278                }
279            }
280        }
281    }
282
283    private void check_for_enter_error(XmppStream stream, Presence.Stanza presence) {
284        Flag flag = stream.get_flag(Flag.IDENTITY);
285        if (presence.is_error() && flag.is_muc_enter_outstanding() && flag.is_occupant(presence.from)) {
286            Jid bare_jid = presence.from.bare_jid;
287            ErrorStanza? error_stanza = presence.get_error();
288            if (flag.get_enter_id(bare_jid) == presence.id) {
289                MucEnterError error = MucEnterError.NONE;
290                switch (error_stanza.condition) {
291                    case ErrorStanza.CONDITION_NOT_AUTHORIZED:
292                        if (ErrorStanza.TYPE_AUTH == error_stanza.type_) error = MucEnterError.PASSWORD_REQUIRED;
293                        break;
294                    case ErrorStanza.CONDITION_REGISTRATION_REQUIRED:
295                        if (ErrorStanza.TYPE_AUTH == error_stanza.type_) error = MucEnterError.NOT_IN_MEMBER_LIST;
296                        break;
297                    case ErrorStanza.CONDITION_FORBIDDEN:
298                        if (ErrorStanza.TYPE_AUTH == error_stanza.type_) error = MucEnterError.BANNED;
299                        break;
300                    case ErrorStanza.CONDITION_SERVICE_UNAVAILABLE:
301                        if (ErrorStanza.TYPE_WAIT == error_stanza.type_) error = MucEnterError.OCCUPANT_LIMIT_REACHED;
302                        break;
303                    case ErrorStanza.CONDITION_ITEM_NOT_FOUND:
304                        if (ErrorStanza.TYPE_CANCEL == error_stanza.type_) error = MucEnterError.ROOM_DOESNT_EXIST;
305                        break;
306                    case ErrorStanza.CONDITION_CONFLICT:
307                        if (ErrorStanza.TYPE_CANCEL == error_stanza.type_) error = MucEnterError.NICK_CONFLICT;
308                        break;
309                    case ErrorStanza.CONDITION_NOT_ALLOWED:
310                        if (ErrorStanza.TYPE_CANCEL == error_stanza.type_) error = MucEnterError.CREATION_RESTRICTED;
311                        break;
312                    case ErrorStanza.CONDITION_NOT_ACCEPTABLE:
313                        if (ErrorStanza.TYPE_CANCEL == error_stanza.type_) error = MucEnterError.USE_RESERVED_ROOMNICK;
314                        break;
315                }
316                if (error != MucEnterError.NONE) {
317                    flag.enter_futures[bare_jid].set_value(new JoinResult() {muc_error=error});
318                } else {
319                    flag.enter_futures[bare_jid].set_value(new JoinResult() {stanza_error=error_stanza.condition});
320                }
321                flag.finish_muc_enter(bare_jid);
322            }
323        }
324    }
325
326    private void on_received_available(XmppStream stream, Presence.Stanza presence) {
327        Flag flag = stream.get_flag(Flag.IDENTITY);
328        if (flag.is_occupant(presence.from)) {
329            StanzaNode? x_node = presence.stanza.get_subnode("x", NS_URI_USER);
330            if (x_node != null) {
331                ArrayList<int> status_codes = get_status_codes(x_node);
332                if (status_codes.contains(StatusCode.SELF_PRESENCE)) {
333                    Jid bare_jid = presence.from.bare_jid;
334                    if (flag.get_enter_id(bare_jid) != null) {
335
336                        query_affiliation.begin(stream, bare_jid, "member");
337                        query_affiliation.begin(stream, bare_jid, "admin");
338                        query_affiliation.begin(stream, bare_jid, "owner");
339
340                        flag.finish_muc_enter(bare_jid);
341                        flag.enter_futures[bare_jid].set_value(new JoinResult() {nick=presence.from.resourcepart});
342                    }
343
344                    flag.set_muc_nick(presence.from);
345                }
346                string? affiliation_str = x_node.get_deep_attribute("item", "affiliation");
347                Affiliation? affiliation = null;
348                if (affiliation_str != null) {
349                    affiliation = parse_affiliation(affiliation_str);
350                    flag.set_affiliation(presence.from.bare_jid, presence.from, affiliation);
351                    received_occupant_affiliation(stream, presence.from, affiliation);
352                }
353                string? jid_ = x_node.get_deep_attribute("item", "jid");
354                if (jid_ != null) {
355                    try {
356                        Jid jid = new Jid(jid_);
357                        flag.set_real_jid(presence.from, jid);
358                        if (affiliation != null) {
359                            stream.get_flag(Flag.IDENTITY).set_offline_member(presence.from, jid, affiliation);
360                        }
361                        received_occupant_jid(stream, presence.from, jid);
362                    } catch (InvalidJidError e) {
363                        warning("Received invalid occupant jid: %s", e.message);
364                    }
365                }
366                string? role_str = x_node.get_deep_attribute("item", "role");
367                if (role_str != null) {
368                    Role role = parse_role(role_str);
369                    flag.set_occupant_role(presence.from, role);
370                    received_occupant_role(stream, presence.from, role);
371                }
372            }
373        }
374    }
375
376    private void on_received_unavailable(XmppStream stream, Presence.Stanza presence) {
377        Flag flag = stream.get_flag(Flag.IDENTITY);
378        if (!flag.is_occupant(presence.from)) return;
379
380        StanzaNode? x_node = presence.stanza.get_subnode("x", NS_URI_USER);
381        if (x_node == null) return;
382
383        ArrayList<int> status_codes = get_status_codes(x_node);
384
385        if (StatusCode.SELF_PRESENCE in status_codes) {
386            flag.remove_occupant_info(presence.from);
387        }
388
389        foreach (StatusCode code in USER_REMOVED_CODES) {
390            if (code in status_codes) {
391                if (StatusCode.SELF_PRESENCE in status_codes) {
392                    flag.left_muc(stream, presence.from.bare_jid);
393                    self_removed_from_room(stream, presence.from, code);
394                    Presence.Flag presence_flag = stream.get_flag(Presence.Flag.IDENTITY);
395                    presence_flag.remove_presence(presence.from.bare_jid);
396                } else {
397                    removed_from_room(stream, presence.from, code);
398                }
399            }
400        }
401    }
402
403    private async void query_room_info(XmppStream stream, Jid jid) {
404        ServiceDiscovery.InfoResult? info_result = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, jid);
405        if (info_result == null) return;
406
407        Gee.List<Feature> features = new ArrayList<Feature>();
408
409        foreach (ServiceDiscovery.Identity identity in info_result.identities) {
410            if (identity.category == "conference" && identity.name != null) {
411                stream.get_flag(Flag.IDENTITY).set_room_name(jid, identity.name);
412            }
413        }
414
415        foreach (string feature in info_result.features) {
416            Feature? parsed = null;
417            switch (feature) {
418                case "http://jabber.org/protocol/muc#register": parsed = Feature.REGISTER; break;
419                case "http://jabber.org/protocol/muc#roomconfig": parsed = Feature.ROOMCONFIG; break;
420                case "http://jabber.org/protocol/muc#roominfo": parsed = Feature.ROOMINFO; break;
421                case "http://jabber.org/protocol/muc#stable_id": parsed = Feature.STABLE_ID; break;
422                case "muc_hidden": parsed = Feature.HIDDEN; break;
423                case "muc_membersonly": parsed = Feature.MEMBERS_ONLY; break;
424                case "muc_moderated": parsed = Feature.MODERATED; break;
425                case "muc_nonanonymous": parsed = Feature.NON_ANONYMOUS; break;
426                case "muc_open": parsed = Feature.OPEN; break;
427                case "muc_passwordprotected": parsed = Feature.PASSWORD_PROTECTED; break;
428                case "muc_persistent": parsed = Feature.PERSISTENT; break;
429                case "muc_public": parsed = Feature.PUBLIC; break;
430                case "muc_rooms": parsed = Feature.ROOMS; break;
431                case "muc_semianonymous": parsed = Feature.SEMI_ANONYMOUS; break;
432                case "muc_temporary": parsed = Feature.TEMPORARY; break;
433                case "muc_unmoderated": parsed = Feature.UNMODERATED; break;
434                case "muc_unsecured": parsed = Feature.UNSECURED; break;
435            }
436            if (parsed != null) features.add(parsed);
437        }
438        stream.get_flag(Flag.IDENTITY).set_room_features(jid, features);
439        room_info_updated(stream, jid);
440    }
441
442    private async Gee.List<Jid>? query_affiliation(XmppStream stream, Jid jid, string affiliation) {
443        Iq.Stanza iq = new Iq.Stanza.get(
444            new StanzaNode.build("query", NS_URI_ADMIN)
445                .add_self_xmlns()
446                .put_node(new StanzaNode.build("item", NS_URI_ADMIN)
447                    .put_attribute("affiliation", affiliation))
448        ) { to=jid };
449
450
451        Iq.Stanza iq_result = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq);
452        if (iq_result.is_error()) return null;
453
454        StanzaNode? query_node = iq_result.stanza.get_subnode("query", NS_URI_ADMIN);
455        if (query_node == null) return null;
456
457        Gee.List<StanzaNode> item_nodes = query_node.get_subnodes("item", NS_URI_ADMIN);
458        Gee.List<Jid> ret_jids = new ArrayList<Jid>(Jid.equals_func);
459        foreach (StanzaNode item in item_nodes) {
460            string jid__ = item.get_attribute("jid");
461            string? affiliation_ = item.get_attribute("affiliation");
462            if (jid__ != null && affiliation_ != null) {
463                try {
464                    Jid jid_ = new Jid(jid__);
465                    stream.get_flag(Flag.IDENTITY).set_offline_member(iq_result.from, jid_, parse_affiliation(affiliation_));
466                    ret_jids.add(jid_);
467                    received_occupant_jid(stream, iq_result.from, jid_);
468                } catch (InvalidJidError e) {
469                    warning("Received invalid occupant jid: %s", e.message);
470                }
471            }
472        }
473        return ret_jids;
474    }
475
476    private static ArrayList<int> get_status_codes(StanzaNode x_node) {
477        ArrayList<int> ret = new ArrayList<int>();
478        foreach (StanzaNode status_node in x_node.get_subnodes("status", NS_URI_USER)) {
479            ret.add(int.parse(status_node.get_attribute("code")));
480        }
481        return ret;
482    }
483
484    private static Affiliation parse_affiliation(string affiliation_str) {
485        Affiliation affiliation;
486        switch (affiliation_str) {
487            case "admin":
488                affiliation = Affiliation.ADMIN; break;
489            case "member":
490                affiliation = Affiliation.MEMBER; break;
491            case "outcast":
492                affiliation = Affiliation.OUTCAST; break;
493            case "owner":
494                affiliation = Affiliation.OWNER; break;
495            default:
496                affiliation = Affiliation.NONE; break;
497        }
498        return affiliation;
499    }
500
501    private static Role parse_role(string role_str) {
502        Role role;
503        switch (role_str) {
504            case "moderator":
505                role = Role.MODERATOR; break;
506            case "participant":
507                role = Role.PARTICIPANT; break;
508            case "visitor":
509                role = Role.VISITOR; break;
510            default:
511                role = Role.NONE; break;
512        }
513        return role;
514    }
515}
516
517public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
518
519    private const string[] after_actions_const = {"EXTRACT_MESSAGE_2"};
520
521    public override string action_group { get { return ""; } }
522    public override string[] after_actions { get { return after_actions_const; } }
523
524    Module outer;
525
526    public ReceivedPipelineListener(Module outer) {
527        this.outer = outer;
528    }
529
530    public override async bool run(XmppStream stream, MessageStanza message) {
531        if (message.type_ == MessageStanza.TYPE_NORMAL) {
532            StanzaNode? x_node = message.stanza.get_subnode("x", NS_URI_USER);
533            if (x_node != null) {
534                StanzaNode? invite_node = x_node.get_subnode("invite", NS_URI_USER);
535                string? password = null;
536                StanzaNode? password_node = x_node.get_subnode("password", NS_URI_USER);
537                if (password_node != null) password = password_node.get_string_content();
538                if (invite_node != null) {
539                    Jid? from_jid = null;
540                    try {
541                        string from = invite_node.get_attribute("from");
542                        if (from != null) from_jid = new Jid(from);
543                    } catch (InvalidJidError e) {
544                        warning("Received invite from invalid jid: %s", e.message);
545                    }
546                    if (from_jid != null) {
547                        StanzaNode? reason_node = invite_node.get_subnode("reason", NS_URI_USER);
548                        string? reason = null;
549                        if (reason_node != null) reason = reason_node.get_string_content();
550                        bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(message) != null; // TODO
551                        if (!is_mam_message) outer.invite_received(stream, message.from, from_jid, password, reason);
552                        return true;
553                    }
554                }
555            }
556
557            StanzaNode? x_field_node = message.stanza.get_subnode("x", DataForms.NS_URI);
558            if (x_field_node != null){
559                Gee.List<StanzaNode>? fields = x_field_node.get_subnodes("field", DataForms.NS_URI);
560                Jid? from_jid = null;
561                string? nick = null;
562
563                if (fields.size!=0){
564                    foreach (var field_node in fields){
565                        string? var_ = field_node.get_attribute("var");
566                        if (var_ == "muc#jid"){
567                            StanzaNode? value_node = field_node.get_subnode("value", DataForms.NS_URI);
568                            try {
569                                if (value_node != null) from_jid = new Jid(value_node.get_string_content());
570                            } catch (InvalidJidError e) {
571                                return false;
572                            }
573                        }
574                        else if (var_ == "muc#roomnick"){
575                            StanzaNode? value_node = field_node.get_subnode("value", DataForms.NS_URI);
576                            if (value_node != null) nick = value_node.get_string_content();
577                        }
578                        else if (var_ == "muc#role"){
579                            StanzaNode? value_node = field_node.get_subnode("value", DataForms.NS_URI);
580                            if (value_node != null) {
581                                if (value_node.get_string_content() != "participant") {
582                                    warning("Voice request with role other than participant");
583                                }
584                            }
585                        }
586                    }
587                    if (from_jid == null || nick == null) {
588                        warning("Voice request without from_jid or nick");
589                        return false;
590                    }
591
592                    outer.voice_request_received(stream, message.from, from_jid, nick);
593                    return true;
594                }
595            }
596        }
597        return false;
598    }
599}
600
601}
602