Anton Shestakov <av6@dwimlabs.net>, Sat, 16 Apr 2016 23:50:55 +0800
index: save and load caps cache from private storage (XEP-0049)
coffee/index.coffee
Permissions: -rw-r--r--
Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps') Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2') Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates') Strophe.addNamespace('DELAY', 'urn:xmpp:delay') Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0') Strophe.addNamespace('LAST', 'jabber:iq:last') Strophe.addNamespace('PRIVATE', 'jabber:iq:private') 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.settings = new Tram.ClientSettings() window.serverInfo = new Tram.ServerInfo() 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 settings.get('chatstates') and 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.CARBONS) 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.PRIVATE) 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) loadSettings(dSettings.resolve, dSettings.reject) loadCapsCache(dCaps.resolve, dCaps.reject) getServerVersion(dVersion.resolve, dVersion.reject) X.conn.roster.get(dRoster.resolve) getServerInfo(dInfo.resolve, dInfo.reject) if Strophe.NS.CARBONS in serverInfo.get('features') serverInfo.set('carbons', true) serverInfo.set('carbons', false) enableCarbons(okcb, failcb) $.when(dSettings, dVersion, dInfo, dRoster, dCarbons).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) name: getText(stanza.getElementsByTagName('name')[0]) version: getText(stanza.getElementsByTagName('version')[0]) os: getText(stanza.getElementsByTagName('os')[0]) console.error("couldn't get version", stanza?.innerHTML) X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout) enableCarbons = (doneCallback, failCallback) -> iq = $iq(type: 'set', id: X.conn.getUniqueId('enable')) .c('enable', xmlns: Strophe.NS.CARBONS) serverInfo.set('carbons', true) serverInfo.set('carbons', false) X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout) loadSettings = (doneCallback, failCallback) -> iq = $iq(type: 'get', id: X.conn.getUniqueId('settings')) .c('query', xmlns: Strophe.NS.PRIVATE) .c('settings', xmlns: Tram.NS.PRIVATE) el = stanza.getElementsByTagName('settings')[0] data = JSON.parse(el.firstChild.nodeValue) console.error("couldn't load settings", stanza) X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout) _saveSettings = (doneCallback, failCallback) -> payload = JSON.stringify(settings.toJSON()) iq = $iq(type: 'set', id: X.conn.getUniqueId('settings')) .c('query', xmlns: Strophe.NS.PRIVATE) .c('settings', xmlns: Tram.NS.PRIVATE).t(payload) console.error("couldn't save settings", stanza) X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout) saveSettings = _(_saveSettings).debounce(1000) loadCapsCache = (doneCallback, failCallback) -> iq = $iq(type: 'get', id: X.conn.getUniqueId('capscache')) .c('query', xmlns: Strophe.NS.PRIVATE) .c('capscache', xmlns: Tram.NS.PRIVATE) el = stanza.getElementsByTagName('capscache')[0] data = JSON.parse(el.firstChild.nodeValue) _(capscache).defaults(data) console.error("couldn't load caps cache", stanza) X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout) payload = JSON.stringify(capscache) iq = $iq(type: 'set', id: X.conn.getUniqueId('capscache')) .c('query', xmlns: Strophe.NS.PRIVATE) .c('capscache', xmlns: Tram.NS.PRIVATE).t(payload) console.debug('caps cache saved', stanza) console.error("couldn't save caps cache", stanza) X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout) saveCapsCache = _(_saveCapsCache).debounce(1000) 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, Tram.config.iqTimeout) getServerInfo = (doneCallback, failCallback) -> caps = X.conn.features.getElementsByTagNameNS(Strophe.NS.CAPS, 'c')[0] node = caps?.getAttribute('node') ver = caps?.getAttribute('ver') serverId = "#{ node }##{ ver }" serverInfo.set('features', capscache[serverId]) features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature')) serverInfo.set('features', features) capscache[serverId] = features console.warn("couldn't get server info", stanza) X.conn.disco.info(X.conn.domain, serverId, okcb, failcb, Tram.config.iqTimeout) 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) -> sent = stanza.getElementsByTagNameNS(Strophe.NS.CARBONS, 'sent')[0] received = stanza.getElementsByTagNameNS(Strophe.NS.CARBONS, 'received')[0] forwarded = sent.getElementsByTagNameNS(Strophe.NS.FORWARD, 'forwarded')[0] forwarded = received.getElementsByTagNameNS(Strophe.NS.FORWARD, 'forwarded')[0] message = forwarded.getElementsByTagName('message')[0] text = getText(message.getElementsByTagName('body')[0]) xhtml = message.getElementsByTagNameNS(Strophe.NS.XHTML_IM, 'html')[0] to = message.getAttribute('to') from = message.getAttribute('from') type = message.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 if forwarded? and contact.get('bjid') is Strophe.getBareJidFromJid(X.conn.jid) chat = getContact(to).chat id: message.getAttribute('id') thread: getText(message.getElementsByTagName('thread')[0]) subject: getText(message.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, 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 settings.get('chatstates') and 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', -> $('#settings-modal').one 'show.uk.modal', -> window.settingsRivet = rivets.bind($('[data-app="settings"]'), settings: settings) window.serverRivet = rivets.bind($('[data-app="server-info"]'), server: serverInfo) settings.on('change', saveSettings) $('#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())