/* XMPP/Jabber Noosfero's client
XMPP Core:
http://xmpp.org/rfcs/rfc3920.html
MUC support:
http://xmpp.org/extensions/xep-0045.html
Messages and presence:
http://xmpp.org/rfcs/rfc3921.html
*/
jQuery(function($) {
// extending the current namespaces in Strophe.NS
Strophe.addNamespace('MUC_USER', 'http://jabber.org/protocol/muc#user');
Strophe.addNamespace('MUC_OWNER', 'http://jabber.org/protocol/muc#owner');
Strophe.addNamespace('CHAT_STATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('DATA_FORMS', 'jabber:x:data');
var Jabber = {
debug: true,
connection: null,
bosh_service: $bosh_service,
muc_domain: $muc_domain,
muc_supported: false,
presence_status: '',
conversation_prefix: 'conversation-',
conversations_order: null,
notification_sound: new Audio('/sounds/receive.wav'),
window_visibility: null,
jids: {},
rooms: {},
no_more_messages: {},
avatars: {},
template: function(selector) {
return $('#chat #chat-templates '+selector).clone().html();
},
jid_to_id: function (jid) {
return Strophe.getBareJidFromJid(jid).replace(/@/g, "-").replace(/\./g, "-");
},
jid_of: function(jid_id) {
return Jabber.jids[jid_id].jid;
},
name_of: function(jid_id) {
return Jabber.jids[jid_id].name;
},
type_of: function(jid_id) {
return Jabber.jids[jid_id].type;
},
presence_of: function(jid_id) {
return Jabber.jids[jid_id].presence;
},
unread_messages_of: function(jid_id, value) {
Jabber.jids[jid_id].unread_messages = (value == undefined ? Jabber.jids[jid_id].unread_messages : value);
return Jabber.jids[jid_id].unread_messages;
},
insert_or_update_user: function (list, item, jid, name, presence, template, type, remove_on_offline) {
var jid_id = Jabber.jid_to_id(jid);
var identifier = Strophe.getNodeFromJid(jid);
var html = template
.replace('%{jid_id}', jid_id)
.replace(/%{presence_status}/g, presence)
.replace('%{avatar}', getAvatar(identifier))
.replace('%{name}', name);
$(item).parent().remove();
if(presence != 'offline' || !remove_on_offline){
$(list).append(html);
sort_conversations();
}
Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence};
},
insert_or_update_group: function (jid, presence) {
var jid_id = Jabber.jid_to_id(jid);
var list = $('#buddy-list .buddies ul.'+presence);
var item = $('#' + jid_id);
presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');
log('adding or updating contact ' + jid + ' as ' + presence);
Jabber.insert_or_update_user(list, item, jid, Jabber.name_of(jid_id), presence, Jabber.template('.buddy-item'), 'groupchat');
$("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")
.removeClass()
.addClass('icon-menu-' + presence + '-11');
},
insert_or_update_contact: function (jid, name, presence) {
var jid_id = Jabber.jid_to_id(jid);
var item = $('#' + jid_id);
presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');
var list = $('#buddy-list .buddies ul' + (presence=='offline' ? '.offline' : '.online'));
log('adding or updating contact ' + jid + ' as ' + presence);
Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.buddy-item'), 'chat');
$("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")
.removeClass()
.addClass('icon-menu-' + presence + '-11');
},
insert_or_update_occupant: function (jid, name, presence, room_jid) {
log('adding or updating occupant ' + jid + ' as ' + presence);
var jid_id = Jabber.jid_to_id(jid);
var room_jid_id = Jabber.jid_to_id(room_jid);
var list = $('#' + Jabber.conversation_prefix + room_jid_id + ' .occupants ul');
var item = $(list).find('a[data-id='+ jid_id +']');
Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.occupant-item'), 'chat', true);
if (Jabber.rooms[room_jid_id] === undefined)
Jabber.rooms[room_jid_id] = {};
var room = Jabber.rooms[room_jid_id];
if(presence == 'offline') {
delete Jabber.rooms[room_jid_id][name];
}
else {
Jabber.rooms[room_jid_id][name] = jid;
}
list.parents('.occupants').find('.occupants-online').text(Object.keys(Jabber.rooms[room_jid_id]).length);
},
remove_contact: function(jid) {
var jid_id = Jabber.jid_to_id(jid)
log('Removing contact ' + jid);
$('#' + jid_id).parent('li').remove();
},
render_body_message: function(body) {
body = body.replace(/\r?\n/g, '
');
body = $().emoticon(body);
body = linkify(body, {
callback: function(text, href) {
return href ? '' + text + '' : text;
}
});
return body;
},
show_message: function (jid, name, body, who, identifier, time, offset) {
if(!offset) offset = 0;
if (body) {
body = Jabber.render_body_message(body);
var jid_id = Jabber.jid_to_id(jid);
var tab_id = '#' + Jabber.conversation_prefix + jid_id;
var history = $(tab_id).find('.history');
var offset_container = history.find('.chat-offset-container-'+offset);
if(offset_container.length == 0)
offset_container = $('
' + body + '
'); } else { if (time==undefined) { time = new Date().toISOString(); } var message_html = Jabber.template('.message') .replace('%{message}', body) .replace(/%{who}/g, who) .replace('%{time}', time) .replace('%{name}', name) .replace('%{avatar}', getAvatar(identifier)); offset_container.append(message_html); $(".message span.time").timeago(); } if(offset == 0) history.scrollTo({top:'100%', left:'0%'}); else history.scrollTo(offset_container.height()); if (who != "self") { if ($(tab_id).find('.history:visible').length == 0) { count_unread_messages(jid_id); } document.alert_title = name; } } }, show_status: function(presence) { log('changing my status to ' + presence); $('#buddy-list .user-status .simplemenu-trigger') .removeClass('icon-menu-chat') .removeClass('icon-menu-offline') .removeClass('icon-menu-dnd') .addClass('icon-menu-' + (presence || 'offline')); $('#buddy-list #user-status img.avatar').replaceWith(getMyAvatar()); $.get('/chat/update_presence_status', { status: {chat_status: presence, last_chat_status: presence} }); }, send_availability_status: function(presence) { log('send availability status ' + presence); Jabber.connection.send($pres().c('show').t(presence).up()); Jabber.show_status(presence); }, enter_room: function(jid, push) { if(push == undefined) push = true var jid_id = Jabber.jid_to_id(jid); var conversation_id = Jabber.conversation_prefix + jid_id; var button = $('#' + conversation_id + ' .join'); button.hide(); button.siblings('.leave').show(); Jabber.connection.send( $pres({to: jid + '/' + $own_name}).c('x', {xmlns: Strophe.NS.MUC}).c('history', {maxchars: 0}) ); Jabber.insert_or_update_group(jid, 'online'); Jabber.update_chat_title(); sort_conversations(); if(push) $.post('/chat/join', {room_id: jid}); }, leave_room: function(jid, push) { if(push == undefined) push = true var jid_id = Jabber.jid_to_id(jid); var conversation_id = Jabber.conversation_prefix + jid_id; var button = $('#' + conversation_id + ' .leave'); button.hide(); button.siblings('.join').show(); Jabber.connection.send($pres({from: Jabber.connection.jid, to: jid + '/' + $own_name, type: 'unavailable'})) Jabber.insert_or_update_group(jid, 'offline'); sort_conversations(); if(push) $.post('/chat/leave', {room_id: jid}); }, update_chat_title: function () { var friends_online = $('#buddy-list #friends .buddy-list.online li').length; $('#friends-online').text(friends_online); var friends_offline = $('#buddy-list #friends .buddy-list.offline li').length; $('#friends-offline').text(friends_offline); var groups_online = $('#buddy-list #rooms .buddy-list li').length; $('#groups-online').text(groups_online); }, on_connect: function (status) { switch (status) { case Strophe.Status.CONNECTING: log('connecting...'); break; case Strophe.Status.CONNFAIL: log('failed to connect'); setTimeout(function(){Jabber.connect()}, 10000); break; case Strophe.Status.DISCONNECTING: log('disconnecting...'); $('#buddy-list .toolbar').addClass('small-loading-dark'); break; case Strophe.Status.DISCONNECTED: log('disconnected'); $('#buddy-list ul.buddy-list, .occupants ul.occupant-list').html(''); Jabber.update_chat_title(); $('#buddy-list .toolbar').removeClass('small-loading-dark'); $('textarea').prop('disabled', 'disabled'); if(Jabber.presence_status != 'offline') Jabber.connect(); break; case Strophe.Status.CONNECTED: log('connected'); case Strophe.Status.ATTACHED: log('XMPP/BOSH session attached'); $('#buddy-list .toolbar').removeClass('small-loading-dark'); $('textarea').prop('disabled', ''); break; } }, on_roster: function (iq) { log('receiving roster'); var profiles = []; var contacts_to_insert = {}; var groups_to_insert = []; $(iq).find('item').each(function () { var jid = $(this).attr('jid'); profiles.push(getIdentifier(jid)); var name = $(this).attr('name') || jid; var jid_id = Jabber.jid_to_id(jid); contacts_to_insert[jid] = name; }); //TODO Add groups through roster too... $.ajax({ url: '/chat/roster_groups', dataType: 'json', success: function(data){ $(data).each(function(index, room){ profiles.push(getIdentifier(room.jid)); var jid_id = Jabber.jid_to_id(room.jid); Jabber.jids[jid_id] = {jid: room.jid, name: room.name, type: 'groupchat'}; //FIXME This must check on session if the user is inside the room... groups_to_insert.push(room.jid); }); $.getJSON('/chat/avatars', {profiles: profiles}, function(data) { for(identifier in data) Jabber.avatars[identifier] = data[identifier]; // Insert contacts for(contact_jid in contacts_to_insert) Jabber.insert_or_update_contact(contact_jid, contacts_to_insert[contact_jid]); // Insert groups for (var i = 0; i < groups_to_insert.length; i++) Jabber.insert_or_update_group(groups_to_insert[i], 'offline'); $.getJSON('/chat/recent_conversations', {}, function(data) { Jabber.conversations_order = data; sort_conversations(); }); // set up presence handler and send initial presence Jabber.connection.addHandler(Jabber.on_presence, null, "presence"); Jabber.send_availability_status(Jabber.presence_status); load_defaults(); }); }, error: function(data, textStatus, jqXHR){ console.log(data); }, }); }, // NOTE: cause Noosfero store's rosters in database based on friendship relation between people // these event never occurs cause jabber service (ejabberd) didn't know when a roster was changed on_roster_changed: function (iq) { log('roster changed'); $(iq).find('item').each(function () { var sub = $(this).attr('subscription'); var jid = $(this).attr('jid'); var name = $(this).attr('name') || jid; if (sub == 'remove') { // contact is being removed Jabber.remove_contact(jid); } else { // contact is being added or modified Jabber.insert_or_update_contact(jid, name); } }); return true; }, parse: function (stanza) { var result = {}; if (Strophe.isTagEqual(stanza, 'presence')) { result.from = $(stanza).attr('from'); result.type = $(stanza).attr('type'); if (result.type == 'unavailable') { result.show = 'offline'; } else { var show = $(stanza).find("show").text(); if (show === "" || show == "chat") { result.show = 'chat'; } else if (show == "dnd" || show == "xa") { result.show = 'dnd'; } else { result.show = 'away'; } } if ($(stanza).find('x[xmlns="'+ Strophe.NS.MUC_USER +'"]').length > 0) { result.is_from_room = true; result.from_user = $(stanza).find('x item').attr('jid'); if ($(stanza).find('x item').attr('affiliation') == 'owner') { result.awaiting_configuration = ($(stanza).find('x status').attr('code') == '201'); } } } else if (Strophe.isTagEqual(stanza, 'message')) { result.from = $(stanza).attr('from'); result.body = $(stanza).find('body').text(); if ($(stanza).find('error').length > 0) { result.error = $(stanza).find('error text').text(); if (!result.error && $(stanza).find('error').find('service-unavailable').length > 0) { result.error = $user_unavailable_error; } } } return result; }, on_presence: function (presence) { presence = Jabber.parse(presence); if (presence.type != 'error') { if (presence.is_from_room) { log('receiving room presence from ' + presence.from + ' as ' + presence.show); var name = Strophe.getResourceFromJid(presence.from); if (presence.from_user) { Jabber.insert_or_update_occupant(presence.from_user, name, presence.show, presence.from); } else { log('ooops! user jid not found in presence stanza'); } if (presence.awaiting_configuration) { log('sending instant room configuration to ' + Strophe.getBareJidFromJid(presence.from)); Jabber.connection.sendIQ( $iq({type: 'set', to: Strophe.getBareJidFromJid(presence.from)}) .c('query', {xmlns: Strophe.NS.MUC_OWNER}) .c('x', {xmlns: Strophe.NS.DATA_FORMS, type: 'submit'}) ); } } else { log('receiving contact presence from ' + presence.from + ' as ' + presence.show); var jid = Strophe.getBareJidFromJid(presence.from); if (jid != Jabber.connection.jid) { var jid_id = Jabber.jid_to_id(jid); var name = Jabber.name_of(jid_id); if(presence.show == 'chat') Jabber.remove_notice(jid_id); Jabber.insert_or_update_contact(jid, name, presence.show); Jabber.update_chat_title(); } else { // why server sends presence from myself to me? log('ignoring presence from myself'); if(presence.show=='offline') { Jabber.send_availability_status(Jabber.presence_status); } } } } return true; }, on_private_message: function (message) { message = Jabber.parse(message); log('receiving message from ' + message.from); var jid = Strophe.getBareJidFromJid(message.from); var jid_id = Jabber.jid_to_id(jid); var name = Jabber.name_of(jid_id); create_conversation_tab(name, jid_id); Jabber.show_message(jid, name, escape_html(message.body), 'other', Strophe.getNodeFromJid(jid)); renew_conversation_order(jid); notifyMessage(message); return true; }, on_public_message: function (message) { message = Jabber.parse(message); log('receiving message from ' + message.from); var name = Strophe.getResourceFromJid(message.from); // is a message from the room itself if (! name) { // FIXME Ignoring message from room for now. // Jabber.show_notice(Jabber.jid_to_id(message.from), message.body); } // is a message from another user, not mine else if ($own_name != name) { var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name]; Jabber.show_message(message.from, name, escape_html(message.body), name, Strophe.getNodeFromJid(jid)); renew_conversation_order(jid); notifyMessage(message); } return true; }, on_message_error: function (message) { }, on_muc_support: function(iq) { if ($(iq).find('identity[category=conference]').length > 0 && $(iq).find('feature[var="'+ Strophe.NS.MUC +'"]').length > 0) { var name = $(iq).find('identity[category=conference]').attr('name'); log('muc support found with identity '+ name); Jabber.muc_supported = true; } else { log('muc support not found'); } }, attach_connection: function(data) { // create the connection and attach it Jabber.connection = new Strophe.Connection(Jabber.bosh_service); Jabber.connection.attach(data.jid, data.sid, data.rid, Jabber.on_connect); // handle get roster list (buddy list) Jabber.connection.sendIQ($iq({type: 'get'}).c('query', {xmlns: Strophe.NS.ROSTER}), Jabber.on_roster); // handle presence updates in roster list Jabber.connection.addHandler(Jabber.on_roster_changed, 'jabber:iq:roster', 'iq', 'set'); // Handle messages Jabber.connection.addHandler(Jabber.on_private_message, null, "message", "chat"); // Handle conference messages Jabber.connection.addHandler(Jabber.on_public_message, null, "message", "groupchat"); // Handle message errors Jabber.connection.addHandler(Jabber.on_message_error, null, "message", "error"); // discovering MUC support Jabber.connection.sendIQ( $iq({type: 'get', from: Jabber.connection.jid, to: Jabber.muc_domain}) .c('query', {xmlns: Strophe.NS.DISCO_INFO}), Jabber.on_muc_support ); // uncomment for extra debugging //Strophe.log = function (lvl, msg) { log(msg); }; }, connect: function() { if (Notification.permission !== "granted" && Notification.permission !== "denied") { Notification.requestPermission(function (permission) { if (!('permission' in Notification)) { Notification.permission = permission; } }); } if (Jabber.connection && Jabber.connection.connected) { Jabber.send_availability_status(Jabber.presence_status); } else { log('starting XMPP/BOSH session...'); $('#buddy-list .toolbar').removeClass('small-loading-dark').addClass('small-loading-dark'); $('.dialog-error').hide(); $.ajax({ url: '/chat/start_session', dataType: 'json', success: function(data) { Jabber.attach_connection(data) }, error: function(error) { $('#buddy-list .toolbar').removeClass('small-loading-dark'); $('#buddy-list .dialog-error') .html(error.responseText) .show('highlight') .unbind('click') .click(function() { $(this).hide('highlight'); }); } }); } }, deliver_message: function(jid, body) { var jid_id = Jabber.jid_to_id(jid); var type = Jabber.type_of(jid_id); var presence = Jabber.presence_of(jid_id); var message = $msg({to: jid, from: Jabber.connection.jid, "type": type}) .c('body').t(body).up() .c('active', {xmlns: Strophe.NS.CHAT_STATES}); Jabber.connection.send(message); Jabber.show_message(jid, $own_name, escape_html(body), 'self', Strophe.getNodeFromJid(Jabber.connection.jid)); save_message(jid, body); renew_conversation_order(jid); move_conversation_to_the_top(jid); if (presence == 'offline') Jabber.show_notice(jid_id, $user_unavailable_error); }, is_a_room: function(jid_id) { return Jabber.type_of(jid_id) == 'groupchat'; }, show_notice: function(jid_id, msg) { var tab_id = '#' + Jabber.conversation_prefix + jid_id; var history = $(tab_id).find('.history'); var notice = $(tab_id).find('.history .notice'); if (notice.length > 0) notice.html(msg) else $(tab_id).find('.history').append("" + msg + ""); history.scrollTo({top:'100%', left:'0%'}); }, remove_notice: function(jid_id) { var tab_id = '#' + Jabber.conversation_prefix + jid_id; var notice = $(tab_id).find('.history .notice').remove(); }, }; $('#chat-connect').live('click', function() { Jabber.presence_status = 'chat'; Jabber.connect(); $('#chat .simplemenu-submenu').hide(); return false; }); $('#chat-disconnect').click(function() { disconnect(); $('#chat .simplemenu-submenu').hide(); return false; }); $('#chat-busy').click(function() { Jabber.presence_status = 'dnd'; Jabber.connect(); $('#chat .simplemenu-submenu').hide(); return false; }); $('#chat-retry').live('click', function() { Jabber.presence_status = Jabber.presence_status || 'chat'; Jabber.connect(); return false; }); $('.conversation textarea').live('keydown', function(e) { if (e.keyCode == 13) { var jid = $(this).attr('data-to'); var body = $(this).val(); body = $('