symple-client
Version:
Symple realtime messaging client
905 lines (796 loc) • 28.3 kB
JavaScript
const Symple = {}
// (function (S) {
// Parse a Symple address into a peer object.
Symple.parseAddress = function (str) {
var addr = {},
arr = str.split('|')
if (arr.length > 0) // no id
{ addr.user = arr[0] }
if (arr.length > 1) // has id
{ addr.id = arr[1] }
return addr
}
// Build a Symple address from the given peer object.
Symple.buildAddress = function (peer) {
return (peer.user ? (peer.user + '|') : '') + (peer.id ? peer.id : '')
}
// Return an array of nested objects matching
// the given key/value strings.
Symple.filterObject = function (obj, key, value) { // (Object[, String, String])
var r = []
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var v = obj[k]
if ((!key || k === key) && (!value || v === value)) {
r.push(obj)
} else if (typeof v === 'object') {
var a = Symple.filterObject(v, key, value)
if (a) r = r.concat(a)
}
}
}
return r
}
// Delete nested objects with properties that match the given key/value strings.
Symple.deleteNested = function (obj, key, value) { // (Object[, String, String])
for (var k in obj) {
var v = obj[k]
if ((!key || k === key) && (!value || v === value)) {
delete obj[k]
} else if (typeof v === 'object') {
Symple.deleteNested(v, key)
}
}
}
// Count nested object properties that match the given key/value strings.
Symple.countNested = function (obj, key, value, count) {
if (count === undefined) count = 0
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var v = obj[k]
if ((!key || k === key) && (!value || v === value)) {
count++
} else if (typeof (v) === 'object') {
// else if (v instanceof Object) {
count = Symple.countNested(v, key, value, count)
}
}
}
return count
}
// Traverse an objects nested properties
Symple.traverse = function (obj, fn) { // (Object, Function)
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var v = obj[k]
fn(k, v)
if (typeof v === 'object') { Symple.traverse(v, fn) }
}
}
}
// Generate a random string
Symple.randomString = function (n) {
return Math.random().toString(36).slice(2) // Math.random().toString(36).substring(n || 7)
}
// Recursively merge object properties of r into l
Symple.merge = function (l, r) { // (Object, Object)
for (var p in r) {
try {
// Property in destination object set; update its value.
// if (typeof r[p] === "object") {
if (r[p].constructor === Object) {
l[p] = merge(l[p], r[p])
} else {
l[p] = r[p]
}
} catch (e) {
// Property in destination object not set;
// create it and set its value.
l[p] = r[p]
}
}
return l
}
// Object extend functionality
Symple.extend = function () {
var process = function (destination, source) {
for (var key in source) {
if (hasOwnProperty.call(source, key)) {
destination[key] = source[key]
}
}
return destination
}
var result = arguments[0]
for (var i = 1; i < arguments.length; i++) {
result = process(result, arguments[i])
}
return result
}
// Run a vendor prefixed method from W3C standard method.
Symple.runVendorMethod = function (obj, method) {
var p = 0, m, t, pfx = ['webkit', 'moz', 'ms', 'o', '']
while (p < pfx.length && !obj[m]) {
m = method
if (pfx[p] === '') {
m = m.substr(0, 1).toLowerCase() + m.substr(1)
}
m = pfx[p] + m
t = typeof obj[m]
if (t !== 'undefined') {
pfx = [pfx[p]]
return (t === 'function' ? obj[m]() : obj[m])
}
p++
}
}
// Date parsing for ISO 8601
// Based on https://github.com/csnover/js-iso8601
//
// Parses dates like:
// 2001-02-03T04:05:06.007+06:30
// 2001-02-03T04:05:06.007Z
// 2001-02-03T04:05:06Z
Symple.parseISODate = function (date) { // (String)
// ISO8601 dates were introduced with ECMAScript v5,
// try to parse it natively first...
var timestamp = Date.parse(date)
if (isNaN(timestamp)) {
var struct,
minutesOffset = 0,
numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]
// ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date
// Time String Format string before falling back to any implementation-specific
// date parsing, so that's what we do, even if native implementations could be faster
//
// 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm
if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) {
// Avoid NaN timestamps caused by "undefined" values being passed to Date.UTC
for (var i = 0, k; (k = numericKeys[i]); ++i) { struct[k] = +struct[k] || 0 }
// Allow undefined days and months
struct[2] = (+struct[2] || 1) - 1
struct[3] = +struct[3] || 1
if (struct[8] !== 'Z' && struct[9] !== undefined) {
minutesOffset = struct[10] * 60 + struct[11]
if (struct[9] === '+') { minutesOffset = 0 - minutesOffset }
}
timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7])
}
}
return new Date(timestamp)
}
Symple.isMobileDevice = function () {
return 'ontouchstart' in document.documentElement
}
// Returns the current iOS version, or false if not iOS
Symple.iOSVersion = function (l, r) {
return parseFloat(('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ''])[1])
.replace('undefined', '3_2').replace('_', '.').replace('_', '')) || false
}
// Match the object properties of l with r
Symple.match = function (l, r) { // (Object, Object)
var res = true
for (var prop in l) {
if (!l.hasOwnProperty(prop) ||
!r.hasOwnProperty(prop) ||
r[prop] !== l[prop]) {
res = false
break
}
}
return res
}
Symple.formatTime = function (date) {
function pad (n) { return n < 10 ? ('0' + n) : n }
return pad(date.getHours()).toString() + ':' +
pad(date.getMinutes()).toString() + ':' +
pad(date.getSeconds()).toString() + ' ' +
pad(date.getDate()).toString() + '/' +
pad(date.getMonth()).toString()
}
// Return true if the DOM element has the specified class.
Symple.hasClass = function (element, className) {
return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1
}
// Debug logger
Symple.log = function () {
if (typeof console !== 'undefined' &&
typeof console.log !== 'undefined') {
console.log.apply(console, arguments)
}
}
// -------------------------------------------------------------------------
// Symple OOP Base Class
//
var initializing = false,
fnTest = /xyz/.test(function () { xyz }) ? /\b_super\b/ : /.*/
// The base Class implementation (does nothing)
Symple.Class = function () {}
// Create a new Class that inherits from this class
Symple.Class.extend = function (prop) {
var _super = this.prototype
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true
var prototype = new this()
initializing = false
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] === 'function' &&
typeof _super[name] === 'function' && fnTest.test(prop[name])
? (function (name, fn) {
return function () {
var tmp = this._super
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name]
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments)
this._super = tmp
return ret
}
})(name, prop[name])
: prop[name]
}
// The dummy class constructor
function Class () {
// All construction is actually done in the init method
if (!initializing && this.init) { this.init.apply(this, arguments) }
}
// Populate our constructed prototype object
Class.prototype = prototype
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class
// And make this class extendable
Class.extend = arguments.callee
return Class
}
// -------------------------------------------------------------------------
// Emitter
//
Symple.Emitter = Symple.Class.extend({
init: function () {
this.listeners = {}
},
on: function (event, fn) {
if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] }
if (typeof fn !== 'undefined' && fn.constructor === Function) { this.listeners[event].push(fn) }
},
clear: function (event, fn) {
if (typeof this.listeners[event] !== 'undefined') {
for (var i = 0; i < this.listeners[event].length; i++) {
if (this.listeners[event][i] === fn) {
this.listeners[event].splice(i, 1)
}
}
}
},
emit: function () {
// Symple.log('Emitting: ', arguments);
var event = arguments[0]
var args = Array.prototype.slice.call(arguments, 1)
if (typeof this.listeners[event] !== 'undefined') {
for (var i = 0; i < this.listeners[event].length; i++) {
// Symple.log('Emitting: Function: ', this.listeners[event][i]);
if (this.listeners[event][i].constructor === Function) {
this.listeners[event][i].apply(this, args)
}
}
}
}
})
// -------------------------------------------------------------------------
// Manager
//
Symple.Manager = Symple.Class.extend({
init: function (options) {
this.options = options || {}
this.key = this.options.key || 'id'
this.store = []
},
add: function (value) {
this.store.push(value)
},
remove: function (key) {
var res = null
for (var i = 0; i < this.store.length; i++) {
if (this.store[i][this.key] === key) {
res = this.store[i]
this.store.splice(i, 1)
break
}
}
return res
},
get: function (key) {
for (var i = 0; i < this.store.length; i++) {
if (this.store[i][this.key] === key) {
return this.store[i]
}
}
return null
},
find: function (params) {
var res = []
for (var i = 0; i < this.store.length; i++) {
if (Symple.match(params, this.store[i])) {
res.push(this.store[i])
}
}
return res
},
findOne: function (params) {
var res = this.find(params)
return res.length ? res[0] : undefined
},
last: function () {
return this.store[this.store.length - 1]
},
size: function () {
return this.store.length
}
})
// })(window.Symple = window.Symple || {})
/**
* Module exports.
*/
module.exports = Symple;
;
const Symple = require('./symple');
const { io } = require('socket.io-client');
// (function (S) {
// Symple client class
Symple.Client = Symple.Emitter.extend({
init: function (options) {
this.options = Symple.extend({
url: options.url ? options.url : 'http://localhost:4000',
secure: !!(options.url && (options.url.indexOf('https') === 0 ||
options.url.indexOf('wss') === 0)),
token: undefined, // pre-arranged server session token
peer: {}
}, options)
this._super()
this.options.auth = Symple.extend({
token: this.options.token || '',
user: this.options.peer.user || '',
name: this.options.peer.name || '',
type: this.options.peer.type || ''
}, this.options.auth)
this.peer = options.peer // Symple.extend(this.options.auth, options.peer)
this.peer.rooms = this.peer.rooms || []
// delete this.peer.token
this.roster = new Symple.Roster(this)
this.socket = null
},
// Connects and authenticates on the server.
// If the server is down the 'error' event will fire.
connect: function () {
Symple.log('symple:client: connecting', this.options)
var self = this
if (this.socket) { throw 'The client socket is not null' }
// var io = io || window.io
// console.log(io)
// this.options.auth || {}
// this.options.auth.user = this.peer.user
// this.options.auth.token = this.options.token
this.socket = io.connect(this.options.url, this.options)
this.socket.on('connect', function () {
Symple.log('symple:client: connected')
// self.socket.emit('announce', {
// token: self.options.token || '',
// user: self.peer.user || '',
// name: self.peer.name || '',
// type: self.peer.type || ''
// }, function (res) {
// Symple.log('symple:client: announced', res)
// if (res.status !== 200) {
// self.setError('auth', res)
// return
// }
// self.peer = Symple.extend(self.peer, res.data)
// self.roster.add(res.data)
self.peer.id = self.socket.id
self.peer.online = true
self.roster.add(self.peer)
self.sendPresence({ probe: true })
self.emit('connect')
self.socket.on('message', function (m) {
Symple.log('symple:client: receive', m);
if (typeof (m) === 'object') {
switch (m.type) {
case 'message':
m = new Symple.Message(m)
break
case 'command':
m = new Symple.Command(m)
break
case 'event':
m = new Symple.Event(m)
break
case 'presence':
m = new Symple.Presence(m)
if (m.data.online) {
self.roster.update(m.data)
} else {
setTimeout(function () { // remove after timeout
self.roster.remove(m.data.id)
})
}
if (m.probe) {
self.sendPresence(new Symple.Presence({
to: Symple.parseAddress(m.from).id
}))
}
break
default:
m.type = m.type || 'message'
break
}
if (typeof (m.from) !== 'string') {
Symple.log('symple:client: invalid sender address', m)
return
}
// Replace the from attribute with the full peer object.
// This will only work for peer messages, not server messages.
var rpeer = self.roster.get(m.from)
if (rpeer) {
m.from = rpeer
} else {
Symple.log('symple:client: got message from unknown peer', m)
}
// Dispatch to the application
self.emit(m.type, m)
}
})
// })
})
this.socket.on('error', function () {
// This is triggered when any transport fails,
// so not necessarily fatal.
self.emit('connect')
})
this.socket.on('connecting', function () {
Symple.log('symple:client: connecting')
self.emit('connecting')
})
this.socket.on('reconnecting', function () {
Symple.log('symple:client: reconnecting')
self.emit('reconnecting')
})
this.socket.on('connect_error', (err) => {
// Called when authentication middleware fails
self.emit('connect_error')
self.setError('auth', err.message)
Symple.log('symple:client: connect error', err)
})
this.socket.on('connect_failed', function () {
// Called when all transports fail
Symple.log('symple:client: connect failed')
self.emit('connect_failed')
self.setError('connect')
})
this.socket.on('disconnect', function () {
Symple.log('symple:client: disconnect')
self.peer.online = false
self.emit('disconnect')
})
},
// Disconnect from the server
disconnect: function () {
if (this.socket) { this.socket.disconnect() }
},
// Return the online status
online: function () {
return this.peer.online
},
// Join a room
join: function (room) {
this.socket.emit('join', room)
},
// Leave a room
leave: function (room) {
this.socket.emit('leave', room)
},
// Send a message to the given peer
send: function (m, to) {
// Symple.log('symple:client: before send', m, to);
if (!this.online()) { throw 'Cannot send messages while offline' } // add to pending queue?
if (typeof (m) !== 'object') { throw 'Message must be an object' }
if (typeof (m.type) !== 'string') { m.type = 'message' }
if (!m.id) { m.id = Symple.randomString(8) }
if (to) { m.to = to }
if (m.to && typeof (m.to) === 'object') { m.to = Symple.buildAddress(m.to) }
if (m.to && typeof (m.to) !== 'string') { throw 'Message `to` attribute must be an address string' }
m.from = Symple.buildAddress(this.peer)
if (m.from === m.to) { throw 'Message sender cannot match the recipient' }
Symple.log('symple:client: sending', m)
this.socket.emit('message', m)
// this.socket.json.send(m)
},
respond: function (m) {
this.send(m, m.from)
},
sendMessage: function (m, to) {
this.send(m, to)
},
sendPresence: function (p) {
p = p || {}
if (p.data) { p.data = Symple.merge(this.peer, p.data) } else { p.data = this.peer }
this.send(new Symple.Presence(p))
},
sendCommand: function (c, to, fn, once) {
var self = this
c = new Symple.Command(c, to)
this.send(c)
if (fn) {
this.onResponse('command', {
id: c.id
}, fn, function (res) {
// NOTE: 202 (Accepted) and 406 (Not acceptable) response codes
// signal that the command has not yet completed.
if (once || (res.status !== 202 &&
res.status !== 406)) {
self.clear('command', fn)
}
})
}
},
// Adds a capability for our current peer
addCapability: function (name, value) {
var peer = this.peer
if (peer) {
if (typeof value === 'undefined') { value = true }
if (typeof peer.capabilities === 'undefined') { peer.capabilities = {} }
peer.capabilities[name] = value
// var idx = peer.capabilities.indexOf(name);
// if (idx === -1) {
// peer.capabilities.push(name);
// this.sendPresence();
// }
}
},
// Removes a capability from our current peer
removeCapability: function (name) {
var peer = this.peer
if (peer && typeof peer.capabilities !== 'undefined' &&
typeof peer.capabilities[name] !== 'undefined') {
delete peer.capabilities[key]
this.sendPresence()
// var idx = peer.capabilities.indexOf(name)
// if (idx !== -1) {
// peer.capabilities.pop(name);
// this.sendPresence();
// }
}
},
// Checks if a peer has a specific capbility and returns a boolean
hasCapability: function (id, name) {
var peer = this.roster.get(id)
if (peer) {
if (typeof peer.capabilities !== 'undefined' &&
typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] !== false }
if (typeof peer.data !== 'undefined' &&
typeof peer.data.capabilities !== 'undefined' &&
typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] !== false }
}
return false
},
// Checks if a peer has a specific capbility and returns the value
getCapability: function (id, name) {
var peer = this.roster.get(id)
if (peer) {
if (typeof peer.capabilities !== 'undefined' &&
typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] }
if (typeof peer.data !== 'undefined' &&
typeof peer.data.capabilities !== 'undefined' &&
typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] }
}
return undefined
},
// Sets the client to an error state and disconnect
setError: function (error, message) {
Symple.log('symple:client: fatal error', error, message)
// if (this.error === error)
// return;
// this.error = error;
this.emit('error', error, message)
if (this.socket) { this.socket.disconnect() }
},
onResponse: function (event, filters, fn, after) {
if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] }
if (typeof fn !== 'undefined' && fn.constructor === Function) {
this.listeners[event].push({
fn: fn, // data callback function
after: after, // after data callback function
filters: filters // event filter object for matching response
})
}
},
clear: function (event, fn) {
Symple.log('symple:client: clearing callback', event)
if (typeof this.listeners[event] !== 'undefined') {
for (var i = 0; i < this.listeners[event].length; i++) {
if (this.listeners[event][i].fn === fn &&
String(this.listeners[event][i].fn) === String(fn)) {
this.listeners[event].splice(i, 1)
Symple.log('symple:client: cleared callback', event)
}
}
}
},
// Extended emit function to handle filtered message response
// callbacks first, and then standard events.
emit: function () {
if (!this.emitResponse.apply(this, arguments)) {
this._super.apply(this, arguments)
}
},
// Emit function for handling filtered message response callbacks.
emitResponse: function () {
var event = arguments[0]
var data = Array.prototype.slice.call(arguments, 1)
if (typeof this.listeners[event] !== 'undefined') {
for (var i = 0; i < this.listeners[event].length; i++) {
if (typeof this.listeners[event][i] === 'object' &&
this.listeners[event][i].filters !== 'undefined' &&
Symple.match(this.listeners[event][i].filters, data[0])) {
this.listeners[event][i].fn.apply(this, data)
if (this.listeners[event][i].after !== 'undefined') {
this.listeners[event][i].after.apply(this, data)
}
return true
}
}
}
return false
}
// getPeers: function(fn) {
// var self = this;
// this.socket.emit('peers', function(res) {
// Symple.log('Peers: ', res);
// if (typeof(res) !== 'object')
// for (var peer in res)
// self.roster.update(peer);
// if (fn)
// fn(res);
// });
// }
})
// -------------------------------------------------------------------------
// Symple Roster
//
Symple.Roster = Symple.Manager.extend({
init: function (client) {
this._super()
this.client = client
},
// Add a peer object to the roster
add: function (peer) {
Symple.log('symple:roster: adding', peer)
if (!peer || !peer.id || !peer.user) { throw 'Cannot add invalid peer' }
this._super(peer)
this.client.emit('addPeer', peer)
},
// Remove the peer matching an ID or address string: user|id
remove: function (id) {
id = Symple.parseAddress(id).id || id
var peer = this._super(id)
Symple.log('symple:roster: removing', id, peer)
if (peer) { this.client.emit('removePeer', peer) }
return peer
},
// Get the peer matching an ID or address string: user|id
get: function (id) {
// Handle IDs
peer = this._super(id) // id = Symple.parseIDFromAddress(id) || id;
if (peer) { return peer }
// Handle address strings
return this.findOne(Symple.parseAddress(id))
},
update: function (data) {
if (!data || !data.id) { return }
var peer = this.get(data.id)
if (peer) {
for (var key in data) { peer[key] = data[key] }
} else { this.add(data) }
}
// Get the peer matching an address string: user|id
// getForAddr: function(addr) {
// var o = Symple.parseAddress(addr);
// if (o && o.id)
// return this.get(o.id);
// return null;
// }
})
// -------------------------------------------------------------------------
// Message
//
Symple.Message = function (json) {
if (typeof (json) === 'object') { this.fromJSON(json) }
this.type = 'message'
}
Symple.Message.prototype = {
fromJSON: function (json) {
for (var key in json) { this[key] = json[key] }
},
valid: function () {
return this['id'] &&
this['from']
}
}
// -------------------------------------------------------------------------
// Command
//
Symple.Command = function (json) {
if (typeof (json) === 'object') { this.fromJSON(json) }
this.type = 'command'
}
Symple.Command.prototype = {
getData: function (name) {
return this['data'] ? this['data'][name] : null
},
params: function () {
return this['node'].split(':')
},
param: function (n) {
return this.params()[n - 1]
},
matches: function (xuser) {
xparams = xuser.split(':')
// No match if x params are greater than ours.
if (xparams.length > this.params().length) { return false }
for (var i = 0; i < xparams.length; i++) {
// Wildcard * matches everything until next parameter.
if (xparams[i] === '*') { continue }
if (xparams[i] !== this.params()[i]) { return false }
}
return true
},
fromJSON: function (json) {
for (var key in json) { this[key] = json[key] }
},
valid: function () {
return this['id'] &&
this['from'] &&
this['node']
}
}
// -------------------------------------------------------------------------
// Presence
//
Symple.Presence = function (json) {
if (typeof (json) === 'object') { this.fromJSON(json) }
this.type = 'presence'
}
Symple.Presence.prototype = {
fromJSON: function (json) {
for (var key in json) { this[key] = json[key] }
},
valid: function () {
return this['id'] && this['from']
}
}
// -------------------------------------------------------------------------
// Event
//
Symple.Event = function (json) {
if (typeof (json) === 'object') { this.fromJSON(json) }
this.type = 'event'
}
Symple.Event.prototype = {
fromJSON: function (json) {
for (var key in json) { this[key] = json[key] }
},
valid: function () {
return this['id'] &&
this['from'] &&
this.name
}
}
// })(window.Symple = window.Symple || {})
/**
* Module exports.
*/
module.exports = Symple;