muc.vala
author Stiletto <blasux@blasux.ru>
Mon, 05 Nov 2012 23:54:44 +0400
changeset 12 d3e36b368fc5
parent 11 0f0cf428409f
permissions -rw-r--r--
iq, commands, makefile up

abstract class Module : Object {
    protected weak Config cfg;
    protected weak Connection conn;

    public Module(Config cfg, Connection conn) {
        this.cfg = cfg;
        this.conn = conn;
    }
    public abstract string name();
}

class ModuleMuc : Module {
    public static const string NS_MUC_USER = "http://jabber.org/protocol/muc#user";

    public enum Affiliation {
        NONE,
        OUTCAST,
        MEMBER,
        ADMIN,
        OWNER
    }

    public enum Role {
        NONE,
        VISITOR,
        PARTICIPANT,
        MODERATOR
    }

    static Role role_from_string(string role) {
        switch (role) {
            case "moderator": return Role.MODERATOR;
            case "participant": return Role.PARTICIPANT;
            case "visitor": return Role.VISITOR;
        }
        return Role.NONE;
    }
    static string role_to_string(Role role) {
        switch (role) {
            case Role.MODERATOR: return "moderator";
            case Role.PARTICIPANT: return "participant";
            case Role.VISITOR: return "visitor";
        }
        return "none";
    }

    static Affiliation affil_from_string(string affil) {
        switch (affil) {
            case "owner": return Affiliation.OWNER;
            case "admin": return Affiliation.ADMIN;
            case "member": return Affiliation.MEMBER;
            case "outcast": return Affiliation.OUTCAST;
        }
        return Affiliation.NONE;
    }
    static string affil_to_string(Affiliation affil) {
        switch (affil) {
            case Affiliation.OWNER: return "owner";
            case Affiliation.ADMIN: return "admin";
            case Affiliation.MEMBER: return "member";
            case Affiliation.OUTCAST: return "outcast";
        }
        return "none";
    }

    public class Occupant : Object {
        public weak Conference conference;
        public string nick;
        public string real_jid;
        public string status;
        public Affiliation affil;
        public Role role;
        public bool isme;
        public void public_message(string text) {
            var msg = new Lm.Message(conference.jid, Lm.MessageType.MESSAGE);
            msg.node.set_attribute("type","groupchat");
            msg.node.add_child("body",nick+": "+text);
            conference.module.conn.cn.send(msg);
        }
    }

    public enum State {
        CONNECTED,
        DISCOVERING,
        CONNECTING,
        DISCONNECTING,
        DISCONNECTED
    }

    public signal void state_changed(Conference conf, State old_state, State new_state, string description);
    public signal void on_join(Conference conf, Occupant occupant);
    public signal void on_part(Conference conf, Occupant occupant, string? status);
    public signal void on_role(Conference conf, Occupant occupant, Role prev);
    public signal void on_affil(Conference conf, Occupant occupant, Affiliation prev);
    public signal void on_nick(Conference conf, Occupant occupant, string prev);
    public signal void on_message(Conference conf, Occupant? occupant, Lm.MessageNode message, string? body);
    public signal void on_subject(Conference conf, Occupant? occupant);

    public class Conference : Object {
        public string jid;
        public string nick;
        public string desired_nick;
        public bool enabled;
        public string subject;

        protected State _state;
        public Time time;
        public State state { get { return _state; } }
        public Gee.HashMap<string,Occupant> occupants;
        protected string presence_join_id;
        public string pingcheck_id;
        public time_t last_check;
        public weak ModuleMuc module;

        protected void _change_state(State new_state, string description) {
            var old_state = this._state;
            if (old_state != new_state) {
                this._state = new_state;
                stderr.printf("MUC State changed %s -> %s : %s\n",old_state.to_string(), new_state.to_string(), description);
                module.state_changed(this, old_state, new_state, description);
                if (new_state == State.DISCONNECTED)
                    occupants.clear();
            } else
                stderr.printf("MUC State not changed %s : %s\n",old_state.to_string(), description);
        }

        public Conference(ModuleMuc module, string jid) {
            this.module = module;
            var section = "muc "+jid;
            this.desired_nick = module.cfg.get_default(section,"nick",module.cfg.get_default("muc","nick",Random.next_int().to_string()));
            this.nick = this.desired_nick;
            this.jid = jid;
            this._state = State.DISCONNECTED;
            this.occupants = new Gee.HashMap<string,Occupant>();
        }
        public void join(string desc) {
            if (_state == State.DISCONNECTED) {
                var prs = new Lm.Message(jid+"/"+desired_nick, Lm.MessageType.PRESENCE);
                presence_join_id = jid+"_"+Random.next_int().to_string();
                prs.node.set_attribute("id",presence_join_id);
                prs.node.add_child("x",null).set_attribute("xmlns","http://jabber.org/protocol/muc");
                module.conn.cn.send(prs);
                nick = desired_nick;
                _change_state(State.CONNECTING, "requested to join: "+desc);
            }
        }
        
        public Lm.HandlerResult muc_handler(Lm.MessageHandler handler, Lm.Connection connection, Lm.Message message) {
            var node = message.node;
            var from = node.get_attribute("from").split("/",2);
            stdout.printf("MUC<%s>: %s\n",this.jid,node.to_string());
            var type = node.get_attribute("type");
            if (message.get_type()==Lm.MessageType.PRESENCE) {
                switch (type) {
                    case null:
                    case "unavailable":
                        if (from.length==2) {
                            var statuses = new Gee.HashSet<int>();
                            var occupant = occupants[from[1]];
                            var affil = Affiliation.NONE;
                            var role = Role.NONE;
                            string prs_nick = null;

                            var chn = node.children;
                            while (chn != null) {
                                switch (chn.name) {
                                    
                                    case "x":
                                        switch (chn.get_attribute("xmlns")) {
                                            case NS_MUC_USER:
                                                var child = chn.children;
                                                while (child != null) {
                                                    stdout.printf("Child %s %s\n", child.name, child.get_attribute("code"));
                                                    if (child.name == "status") {
                                                        var code = child.get_attribute("code");
                                                        if (code!=null)
                                                            statuses.add(code.to_int());
                                                    }
                                                    child = child.next;
                                                }
                                                var item = chn.get_child("item");
                                                if (item == null)
                                                    log("Muc", LogLevelFlags.LEVEL_ERROR, "Your MUC server is shit. No role and affiliation info in presences: %s", node.to_string());
                                                affil = affil_from_string(item.get_attribute("affiliation"));
                                                role = role_from_string(item.get_attribute("role")); 
                                                prs_nick = item.get_attribute("nick");
                                                break;
                                        }
                                        break;
                                }
                                chn = chn.next;
                            }

                            if (statuses.contains(110) && (_state == State.CONNECTED))
                                log("Muc", LogLevelFlags.LEVEL_INFO, "Joined a room which I was already in: %s", from[0]);

                            if (occupant != null) {
                                if (role == Role.NONE) {
                                    occupants.unset(from[1]);
                                    var status_child = node.get_child("status");
                                    log("Muc", LogLevelFlags.LEVEL_INFO, "MUC<%s> %s has parted.", this.jid, from[1]);
                                    module.on_part(this, occupant, (status_child!=null) ? status_child.get_value() : null);
                                    if (occupant.isme) {
                                        _change_state(State.DISCONNECTED, "we became unavailable");
                                    }
                                } else {
                                    if (affil != occupant.affil) {
                                        var prev = occupant.affil;
                                        occupant.affil = affil;
                                        module.on_affil(this, occupant, prev);
                                    }
                                    if (role != occupant.role) {
                                        var prev = occupant.role;
                                        occupant.role = role;
                                        module.on_role(this, occupant, prev);
                                    }
                                    if (statuses.contains(303) && (prs_nick!=null)) {
                                        occupants[prs_nick] = occupant;
                                        occupants.unset(from[1]);
                                        occupant.nick = prs_nick;
                                        module.on_nick(this, occupant, from[1]);
                                    }
                                }
                            } else {
                                if( role!= Role.NONE) {
                                    occupant = new Occupant();
                                    occupant.role = role;
                                    occupant.affil = affil;
                                    occupant.nick = from[1];
                                    occupant.conference = this;
                                    occupants[from[1]] = occupant;
                                    occupant.isme = statuses.contains(110);
                                    log("Muc", LogLevelFlags.LEVEL_INFO, "MUC<%s> %s has joined as %s/%s.", this.jid, from[1], affil.to_string(), role.to_string());
                                    if (statuses.contains(110)) {
                                        this.nick = from[1];
                                        _change_state(State.CONNECTED, "got own presence. we are "+this.nick);
                                    }
                                    module.on_join(this, occupant);
                                } else {
                                    if (statuses.contains(110))
                                        _change_state(State.DISCONNECTED, "got role none while joining");
                                    log("Muc", LogLevelFlags.LEVEL_WARNING, "Got NONE role for new participant. Maybe we are reconnecting.");
                                }
                            }
                            
                            stdout.printf("User list: ");
                            foreach (var a in occupants.keys)
                                stdout.printf("%s <%s,%s>, ", a, occupants[a].affil.to_string(), occupants[a].role.to_string());
                            stdout.printf("\n");
                        }

                        break;
                    case "error":
                        if ((from.length==1)||(node.get_attribute("id")==presence_join_id)) {
                            _change_state(State.DISCONNECTED, node.get_child("error").to_string());
                            return Lm.HandlerResult.ALLOW_MORE_HANDLERS;
                        } else if (from.length==2) {
                            //if (from[1]==this.nick) 
                        }
                        break;
                }
            } else if (message.get_type()==Lm.MessageType.MESSAGE)
                switch (type) {
                    case "groupchat":
                        var subj = node.get_attribute("subject");
                        var occupant = (from.length > 1) ? occupants[from[1]] : null;
                        if (subj != null) {
                            if (subj != this.subject) {
                                this.subject = subj;
                                module.on_subject(this, occupant);
                            }
                        } else {
                            var body = node.find_child("body");
                            module.on_message(this, occupant, node, (body!=null) ? body.get_value() : null);
                        }
                        break;
                    case "error":
                        if (node.get_attribute("id")==pingcheck_id) {
                            _change_state(State.DISCONNECTED, node.get_child("error").to_string());
                        }
                        break;
                }
            return Lm.HandlerResult.ALLOW_MORE_HANDLERS;
        }
        public void part(string desc) {
            if (_state != State.DISCONNECTED) {
                var prs = new Lm.Message(jid+"/"+desired_nick, Lm.MessageType.PRESENCE);
                presence_join_id = jid+"_"+Random.next_int().to_string();
                prs.node.set_attribute("type","unavailable");
                prs.node.add_child("x",null).set_attribute("xmlns","http://jabber.org/protocol/muc");
                module.conn.cn.send(prs);
                nick = desired_nick;
            }
        }

        public void connection_lost() {
            _change_state(State.DISCONNECTED, "connection lost");
        }
    }

    public override string name() { return "muc"; }

    public Gee.HashMap<string,Conference> rooms;

    public ModuleMuc(Config cfg, Connection conn) {
        base(cfg,conn);
        this.rooms = new Gee.HashMap<string,Conference>();
        
        var muc_handler = new Lm.MessageHandler((handler, connection, message) => {
            var node = message.node;
            var from = node.get_attribute("from");
            if (from != null) {
                var froms = from.split("/",2);
                if (rooms.has_key(froms[0])) {
                    return rooms[froms[0]].muc_handler(handler, connection, message);
                }
            }
            return Lm.HandlerResult.ALLOW_MORE_HANDLERS;
        }, null);
        var names = cfg.get_default("muc","mucs","").split(" ");
        foreach (var name in names) {
            var room = new Conference(this, name); //, "Ζαλυπα");
            //room.join("Oh hai");
            room.enabled = true;
            this.rooms[room.jid] = room;
        }
        conn.cn.register_message_handler(muc_handler, Lm.MessageType.MESSAGE, Lm.HandlerPriority.NORMAL);
        conn.cn.register_message_handler(muc_handler, Lm.MessageType.PRESENCE, Lm.HandlerPriority.NORMAL);
        conn.cn.register_message_handler(muc_handler, Lm.MessageType.IQ, Lm.HandlerPriority.NORMAL);
        conn.state_changed.connect( (olds, news, desc) => {
            switch (news) {
                case Connection.State.CONNECTED:
                    break;
                case Connection.State.DISCONNECTED:
                    foreach(var room in rooms.values)
                        room.connection_lost();
                    break;
            }
        });
        conn.check_time.connect( () => {
            if (conn.state == Connection.State.CONNECTED)
                foreach (var room in rooms.values) {
                    if (room.enabled) {
                        if (room.state == State.DISCONNECTED)
                            room.join("Reconnect");
                        else {
                            time_t curtime = time_t();
                            if ((curtime - room.last_check) > 60) { // crappy check for netsplits
                                room.last_check = curtime;
                                var msg = new Lm.Message(room.jid, Lm.MessageType.MESSAGE);
                                room.pingcheck_id = room.jid+"_"+Random.next_int().to_string();
                                msg.node.set_attribute("id",room.pingcheck_id);
                                log("Muc", LogLevelFlags.LEVEL_DEBUG, "Ping check to %s", room.jid);
                                room.module.conn.cn.send(msg);

                            }
                        }
                    } else {
                        if (room.state == State.CONNECTED)
                            room.part("Disabled");
                    }
                }
        });
    }
}