Anton Shestakov <av6@dwimlabs.net>, Sun, 17 Apr 2016 09:45:41 +0800
index: also allow html messages from self
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) fromSelf = contact.get('bjid') is Strophe.getBareJidFromJid(X.conn.jid) if xhtml? and (fromSelf or X.conn.roster.findItem(contact.get('bjid'))) body = xhtml.getElementsByTagNameNS(Strophe.NS.XHTML, 'body')[0] html = Strophe.createHtml(body).innerHTML if forwarded? and fromSelf 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() connForm.unset('auth-errors') connForm.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') window.connForm = new Tram.ConnectionForm() X.connect(connForm.get('username').trim(), connForm.get('password')) $form = $('[data-form="connect"]') window.connRivet = rivets.bind($form, form: connForm, connect: connectfn) 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())