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