Anton Shestakov <av6@dwimlabs.net>, Thu, 14 Apr 2016 18:44:28 +0800
index: send presence only after all preparations
Currently the preparations include getting server version and features, and
getting roster items.
coffee/index.coffee
Permissions: -rw-r--r--
Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps') Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates') Strophe.addNamespace('DELAY', 'urn:xmpp:delay') Strophe.addNamespace('LAST', 'jabber:iq:last') Strophe.addNamespace('TIME', 'urn:xmpp:time') window.contacts = new Tram.Contacts() window.contactsApp = new Tram.ContactsApp(el: $('[data-app="contacts"]'), collection: contacts) window.calls = new Tram.Calls() window.callsApp = new Tram.CallsApp(el: $('[data-app="calls"]'), collection: calls) window.clientState = new Tram.ClientState() window.faviconApp = new Tram.FaviconApp(model: clientState) window.sidebarApp = new Tram.SidebarApp(el: $('[data-app="sidebar"]'), model: clientState) window.progressApp = new Tram.ProgressApp(el: $('[data-app="progress"]'), model: clientState) contacts.on 'change:show', (model) -> if model.get('type') is 'self' clientState.set('show', model.get('show')) contacts.on 'change:chatstate/self', (model, cs) -> if Strophe.NS.CHATSTATES in (model?.get('features') ? []) msg = $msg(to: model.get('jid'), from: X.conn.jid, type: 'chat') .c(cs, xmlns: Strophe.NS.CHATSTATES) contacts.on 'change:presence', (model) -> dupes = contacts.where(bjid: model.get('bjid')) offline = (c for c in dupes when c.get('presence') is 'unavailable') if offline.length is dupes.length offline = (c for c in offline when c.get('jid') isnt model.get('jid')) clientState.on 'action/show', (show, status) -> X.sendPresence(show: show, status: status, priority: 1) clientState.on 'action/disconnect', -> X.disconnect('Logged out') clientState.on 'change:contact', -> clientState.previous('contact')?.set('chatstate/self', 'inactive') contact = clientState.get('contact') contact?.set('chatstate/self', 'active') $logs = $('[data-app="logs"]') $logs.children().detach() $logs.append(contact.chat.render().el) $('[data-form="send"]').toggleClass('uk-hidden', not contact?) X.conn.addHandler(onPresence, null, 'presence') X.conn.addHandler(onChatMessage, null, 'message', 'chat') X.conn.addHandler(onChatState, Strophe.NS.CHATSTATES, 'message') X.conn.addHandler(onWebRTC, Tram.NS.WEBRTC, 'message', 'chat') X.conn.addHandler(onGetLast, Strophe.NS.LAST, 'iq', 'get') X.conn.addHandler(onGetTime, Strophe.NS.TIME, 'iq', 'get') X.conn.addHandler(onGetVersion, Strophe.NS.VERSION, 'iq', 'get') X.conn.ping.addPingHandler(onPing) X.conn.disco.addIdentity('client', 'web', Tram.info.client) X.conn.disco.addFeature(Strophe.NS.CAPS) X.conn.disco.addFeature(Strophe.NS.CHATSTATES) X.conn.disco.addFeature(Strophe.NS.DISCO_INFO) X.conn.disco.addFeature(Strophe.NS.LAST) X.conn.disco.addFeature(Strophe.NS.PING) X.conn.disco.addFeature(Strophe.NS.TIME) X.conn.disco.addFeature(Strophe.NS.VERSION) X.conn.disco.addFeature(Strophe.NS.XHTML_IM) X.conn.disco.addFeature(Tram.NS.WEBRTC) getServerVersion(dVersion.resolve, dVersion.reject) getServerInfo(dInfo.resolve, dInfo.reject) X.conn.roster.get(dRoster.resolve) $.when(dVersion, dInfo, dRoster).always -> X.sendPresence(status: 'Online', priority: 1) delay = stanza.getElementsByTagNameNS(Strophe.NS.DELAY, 'delay')[0] return new Date(delay?.getAttribute('stamp') ? Date.now()) if el.childNodes.length is 0 and el.nodeType is Strophe.ElementType.TEXT for node in el.childNodes if node.nodeType is Strophe.ElementType.TEXT getServerVersion = (doneCallback, failCallback) -> iq = $iq(type: 'get', id: X.conn.getUniqueId('version'), to: X.conn.domain) .c('query', xmlns: Strophe.NS.VERSION) 'server/name': getText(stanza.getElementsByTagName('name')[0]) 'server/version': getText(stanza.getElementsByTagName('version')[0]) 'server/os': getText(stanza.getElementsByTagName('os')[0]) console.error("couldn't get version", stanza?.innerHTML) X.conn.sendIQ(iq.tree(), okcb, failcb, 5000) id = stanza.getAttribute('id') from = stanza.getAttribute('from') iq = $iq(to: from, type: 'result', id: id) .c('query', xmlns: Strophe.NS.LAST, seconds: '0') id = stanza.getAttribute('id') from = stanza.getAttribute('from') iq = $iq(to: from, type: 'result', id: id) .c('time', xmlns: Strophe.NS.TIME) .c('tzo').t(now.format('Z')).up() .c('utc').t(now.toISOString()) onGetVersion = (stanza) -> id = stanza.getAttribute('id') from = stanza.getAttribute('from') iq = $iq(to: from, type: 'result', id: id) .c('query', xmlns: Strophe.NS.VERSION) .c('name').t(Tram.info.client).up() .c('version').t(Tram.info.version) chats[jid] ?= new Tram.LogApp(collection: new Tram.Messages()) getContact = (from, node, ver) -> contact = contacts.get(from) self = from is X.conn.jid model.set('type', 'contact') bjid = Strophe.getBareJidFromJid(from) type: if self then 'self' else 'contact' contact.chat = getChat(bjid) contact.on 'action/chat', -> clientState.set('contact', contact) contact.on 'action/authorize', -> X.conn.roster.authorize(contact.get('bjid')) contact.on 'action/unauthorize', -> X.conn.roster.unauthorize(contact.get('bjid')) contact.on 'action/remove', -> if X.conn.roster.findItem(contact.get('bjid')) X.conn.roster.remove contact.get('bjid'), -> contact.w = new Tram.WebRTCInterface(contact) contact.on 'action/call', (media) -> contact.set('callstate', 'outgoing') contact.w.init(true, audio: 'a' in media, video: 'v' in media) contact.on 'action/accept', (media) -> contact.w.init(false, audio: 'a' in media, video: 'v' in media) contact.on 'action/decline action/hangup', -> contact.w.sendIntent('terminate') contact.unset('callstate') getContactProfile(contact) if Strophe.getResourceFromJid(from)? getClientInfo(contact, node, ver) getContactProfile = (contact) -> bjid = contact.get('bjid') if bjid is Strophe.getBareJidFromJid(X.conn.jid) vcard = stanza.getElementsByTagNameNS(Strophe.NS.VCARD, 'vCard')[0] console.warn("no vcard in response", stanza) nickname: getText(vcard.querySelector('NICKNAME')) fullname: getText(vcard.querySelector('FN')) firstname: getText(vcard.querySelector('N > GIVEN')) lastname: getText(vcard.querySelector('N > FAMILY')) mime: getText(vcard.querySelector('PHOTO > TYPE')) data: getText(vcard.querySelector('PHOTO > BINVAL')) if avatar.mime and avatar.data console.warn("couldn't get vcard", stanza) X.conn.vcard.get(okcb, bjid, failcb) getClientInfo = (contact, node, ver) -> clientId = "#{ node }##{ ver }" return contact.set('features', capscache[clientId]) features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature')) contact.set('features', features) capscache[clientId] = features console.warn("couldn't get client info", stanza) X.conn.disco.info(jid, clientId, okcb, failcb, 5000) getServerInfo = (doneCallback, failCallback) -> caps = X.conn.features.getElementsByTagNameNS(Strophe.NS.CAPS, 'c')[0] node = caps?.getAttribute('node') ver = caps?.getAttribute('ver') serverId = "#{ node }##{ ver }" clientState.set('server/features', capscache[serverId]) features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature')) clientState.set('server/features', features) capscache[serverId] = features console.warn("couldn't get server info", stanza) X.conn.disco.info(X.conn.domain, serverId, okcb, failcb, 5000) type = stanza.getAttribute('type') ? 'available' from = stanza.getAttribute('from') # Upon receiving the presence stanza of type "unsubscribe", the contact # SHOULD acknowledge receipt of that subscription state notification # through either "affirming" it by sending a presence stanza of type # "unsubscribed" to the user or ... X.conn.roster.unauthorize(from) # Upon receiving the presence stanza of type "subscribed", the contact # SHOULD acknowledge receipt of that subscription state notification # through either "affirming" it by sending a presence stanza of type # "subscribe" to the user or ... X.conn.roster.subscribe(from) # Upon receiving the presence stanza of type "unsubscribed", the user # SHOULD acknowledge receipt of that subscription state notification # through either "affirming" it by sending a presence stanza of type # "unsubscribe" to the contact or ... X.conn.roster.unsubscribe(from) console.error('got <presence type="error">', stanza) caps = stanza.getElementsByTagNameNS(Strophe.NS.CAPS, 'c')[0] ver = caps?.getAttribute('ver') node = caps?.getAttribute('node') contact = getContact(from, node, ver) show = getText(stanza.getElementsByTagName('show')[0]) status = getText(stanza.getElementsByTagName('status')[0]) priority = getText(stanza.getElementsByTagName('priority')[0]) if type in ['available', 'unavailable'] and contact.get('type') isnt 'self' contact.chat.collection.add id: stanza.getAttribute('id') text: status ? "is now #{ show }" onChatMessage = (stanza) -> text = getText(stanza.getElementsByTagName('body')[0]) xhtml = stanza.getElementsByTagNameNS(Strophe.NS.XHTML_IM, 'html')[0] from = stanza.getAttribute('from') type = stanza.getAttribute('type') contact = getContact(from) if xhtml? and X.conn.roster.findItem(contact.get('bjid')) body = xhtml.getElementsByTagNameNS(Strophe.NS.XHTML, 'body')[0] html = Strophe.createHtml(body).innerHTML contact.chat.collection.add id: stanza.getAttribute('id') to: stanza.getAttribute('to') thread: getText(stanza.getElementsByTagName('thread')[0]) subject: getText(stanza.getElementsByTagName('subject')[0]) onChatState = (stanza) -> from = stanza.getAttribute('from') contact = getContact(from) elements = stanza.getElementsByTagNameNS(Strophe.NS.CHATSTATES, '*') contact.set('chatstate', elements[0].tagName.toLowerCase()) from = stanza.getAttribute('from') contact = getContact(from) intent = stanza.getElementsByTagNameNS(Tram.NS.WEBRTC, 'intent')[0] payload = stanza.getElementsByTagNameNS(Tram.NS.WEBRTC, 'payload')[0] contact.set('callstate', 'incoming') contact.chat.collection.add id: stanza.getAttribute('id') contact.unset('callstate') contact.w.onPayload(stanza) window.X = new Tram.XMPPInterface() connData.unset('auth-errors') connData.set('auth-errors', ['Invalid username or password.']) $('[data-step="login"]').removeClass('uk-hidden') $('[data-step="main"]').addClass('uk-hidden') X.on 'connected attached', -> $('[data-step="login"]').addClass('uk-hidden') $('[data-step="main"]').removeClass('uk-hidden') X.on 'status', (status) -> when Strophe.Status.CONNECTING clientState.set('progress', 0) when Strophe.Status.CONNECTED clientState.set('progress', 100) clientState.unset('progress') $(window).on 'beforeunload unload', -> X.disconnect('Window closed') class ConnectionData extends Backbone.Model validate: (attrs, options) -> @unset('username-errors') @unset('password-errors') if (attrs.username ? '').trim() is '' @set('username-errors', ['This field is required.']) if (attrs.password ? '') is '' @set('password-errors', ['This field is required.']) return @has('username-errors') or @has('password-errors') window.connData = new ConnectionData() X.connect(connData.get('username').trim(), connData.get('password')) $form = $('[data-form="connect"]') window.connRivet = rivets.bind($form.get(0), data: connData, connect: connectfn) $form.find('input').on 'keydown', (e) -> if (not @required or @value isnt '') and e.keyCode is 13 index = $form.find('input').index(@) $next = $form.find('input').eq(index + 1) $form.find('button').trigger('click') if text isnt '' and clientState.has('contact') contact = clientState.get('contact') msg = $msg(to: contact.get('jid'), from: X.conn.jid, type: 'chat').c('body').t(text).up() if Strophe.NS.CHATSTATES in (contact.get('features') ? []) msg.c('active', xmlns: Strophe.NS.CHATSTATES) contact.set('chatstate/self', 'active', silent: true) contact.chat.collection.add contact: contacts.findWhere(type: 'self') $('[data-send-button]').on 'click', sendMessage $('#msg').on 'keypress', (e) -> clientState.get('contact')?.set('chatstate/self', 'composing') if clientState.get('contact')?.get('chatstate/self') is 'composing' clientState.get('contact')?.set('chatstate/self', 'paused') $('[data-add-button]').on 'click', -> jid = $('#new-contact').val().trim() jid = "#{ jid }@#{ Tram.config.domain }" $('#new-contact').val('') X.conn.roster.add jid, null, [], -> X.conn.roster.subscribe(jid, 'I want to chat', null) $('#new-contact').on 'keypress', (e) -> $('[data-add-button]').trigger('click') $('[data-unregister-button]').on 'click', -> $('#profile-modal').one 'show.uk.modal', -> window.profileRivet = rivets.bind($('[data-app="profile"]'), contact: contacts.get(X.conn.jid)) $('time[datetime]').each -> $this.text(moment($this.attr('datetime')).fromNow())