Commit 32bbe544 authored by Yiorgis Gozadinos's avatar Yiorgis Gozadinos Committed by Sindre Sorhus

Close GH-263: Backbone.xmpp.

parent b3957fba
#Introduction
This is a demo of the TodoMVC app using [Backbone.xmpp](http://github.com/ggozad/Backbone.xmpp).
Backbone.xmpp is a drop-in replacement for Backbone’s RESTful API, allowing models/collections to be persisted on XMPP Pub-Sub nodes. Naturally, Collections are mapped to nodes, whereas Models to the items of these nodes. Additionally, it listens for events on these nodes, receiving and propagating real-time updates of the models/collections from the server.
This makes it easy to build applications that receive updates in real-time without the need to revert to polling.
Migrating existing Backbone models/collections to use Backbone.xmpp is trivial: You can construct your models extending from `PubSubItem` instead of `Backbone.Model` and your collections from `PubSubNode` instead of `Backbone.Collection` as such:
```javascript
var MyModel = PubSubItem.extend({
...
});
var MyCollection = PubSubNode.extend({
model: MyModel,
...
});
````
and you create instances of your collections passing the `id` of the node and your XMPP `connection` object.
```javascript
var mycollection = new MyCollection([], {id: 'mymodels', connection: connection});
```
Events are handled automatically, so when for instance a model is destroyed by some client, other clients will receive a `remove` event on their collections. Please refer to the [original](http://ggozad.com/Backbone.xmpp/) documentation for more information.
To have an idea of the effort involved, here is the diff between the localStorage version and the Backbone.xmpp one: https://github.com/ggozad/todomvc/compare/17ead933...f0729d79
#Installation of a demo XMPP server with the ejabberd installer
In the `server` directory scripts are included to help you build and configure an XMPP server without too much hussle.
If you wish you to use the ejabberd installer you can get it [here](http://www.process-one.net/en/ejabberd/downloads/). When prompted for the domain and admin user, use `localhost` as the domain and `admin` for the user with `admin` as password.
Once the installation is complete, run the following to generate a config file with all you need.
cd server
python bootstrap.py
./bin/buildout
Replace now the default `ejabberd.cfg` with the one found at `server/etc/ejabberd.cfg`.
#Installation of the demo XMPP server from source
You will need to have erlang and python installed to compile it for your platform. Follow the following steps:
* Uncomment the `ejabberd` line in the parts section of `buildout.cfg`.
* Specify the path to the erlang binary by changing the `erlang-path` variable in the `buildout.cfg` file.
* Run buildout as in the section above.
Once buildout completes, you should have a compiled ejabberd.
Start ejabberd
./bin/ejabberd
and set up the `admin` user:
./bin/ejabberdctl register admin localhost admin
Usage
-----
While ejabberd is running, you can open the `index.html` and a few browser windows (use different browsers, or "incognito" mode) to observe real-time updates across them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Backbone.js • TodoMVC</title>
<link rel="stylesheet" href="../../../assets/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
</section>
<footer id="footer"></footer>
</section>
<div id="info">
<p>Double-click to edit a todo</p>
<p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a></p>
<p>Adapted for
<a href="https://github.com/ggozad/Backbone.xmpp">Backbone.xmpp</a>
by <a href="https://github.com/ggozad">Yiorgis Gozadinos</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div>
<script type="text/template" id="item-template">
<div class="view">
<input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
<label><%- title %></label>
<button class="destroy"></button>
</div>
<input class="edit" value="<%- title %>">
</script>
<script type="text/template" id="stats-template">
<span id="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>
<ul id="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<% if (completed) { %>
<button id="clear-completed">Clear completed (<%= completed %>)</button>
<% } %>
</script>
<script src="../../../assets/base.js"></script>
<script src="../../../assets/jquery.min.js"></script>
<script src="../../../architecture-examples/backbone/js/lib/underscore-min.js"></script>
<script src="../../../architecture-examples/backbone/js/lib/backbone-min.js"></script>
<script src="js/lib/strophe.js"></script>
<script src="js/lib/strophe.forms.js"></script>
<script src="js/lib/strophe.pubsub.js"></script>
<script src="js/lib/backbone.xmpp.storage.js"></script>
<script src="js/lib/backbone.xmpp.node.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/views/todos.js"></script>
<script src="js/views/app.js"></script>
<script src="js/routers/router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
var app = app || {},
boshUrl = 'http://localhost:5280/http-bind';
app.start = function () {
// Create our global collection of **Todos**.
app.Todos.initialize([], {id: 'todos', connection: app.connection});
Backbone.history.start();
// Kick things off by creating the **App**.
new app.AppView();
};
var ENTER_KEY = 13;
$(function() {
// Connect to XMPP
var XMPPConnection = new Strophe.Connection(boshUrl),
chars = 'abcdefghijklmnopqrstuvwxyz',
resource = '';
// A random resource so that the same client can connect more than once
for(var i=0; i < 5; i++) {
resource += chars.charAt(Math.floor(Math.random() * chars.length));
}
XMPPConnection.connect('admin@localhost/' + resource, 'admin', function (status) {
// Set the connection on the storage
if (status === Strophe.Status.CONNECTED) {
this.xmlInput = function (data) { console.log ('IN:', data);};
this.xmlOutput = function (data) { console.log ('OUT:', data);};
// Send online presence
this.send($pres());
// Save the connection
app.connection = this;
// Create the node. If this fails it's probably cause it's already there.
// If it is created succesfully then subscribe to it.
// All this should be happening on the server...
var cp = app.connection.PubSub.createNode('todos');
cp.done(function () {
app.connection.PubSub.subscribe('todos');
});
cp.always(function () {
app.start();
});
}
});
});
\ No newline at end of file
var app = app || {};
(function() {
'use strict';
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = PubSubNode.extend({
// Reference to this collection's model.
model: app.Todo,
// Filter down the list of all todo items that are finished.
completed: function() {
return this.filter(function( todo ) {
return todo.get('completed');
});
},
// Filter down the list to only todo items that are still not finished.
remaining: function() {
return this.without.apply( this, this.completed() );
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function() {
if ( !this.length ) {
return 1;
}
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function( todo ) {
return todo.get('order');
}
});
// Create our global collection of **Todos**.
app.Todos = new TodoList();
}());
// Constants
var ENTER_KEY = 13;
// Setup namespace for the app
window.app = window.app || {};
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
var Store = function(name) {
this.name = name;
var store = localStorage.getItem(this.name);
this.data = (store && JSON.parse(store)) || {};
};
_.extend(Store.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
localStorage.setItem(this.name, JSON.stringify(this.data));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function(model) {
if (!model.id) model.id = model.attributes.id = guid();
this.data[model.id] = model;
this.save();
return model;
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.data[model.id] = model;
this.save();
return model;
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return this.data[model.id];
},
// Return the array of all models currently in storage.
findAll: function() {
return _.values(this.data);
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
delete this.data[model.id];
this.save();
return model;
}
});
// Override `Backbone.sync` to use delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
Backbone.sync = function(method, model, options) {
var resp;
var store = model.localStorage || model.collection.localStorage;
switch (method) {
case "read": resp = model.id ? store.find(model) : store.findAll(); break;
case "create": resp = store.create(model); break;
case "update": resp = store.update(model); break;
case "delete": resp = store.destroy(model); break;
}
if (resp) {
options.success(resp);
} else {
options.error("Record not found");
}
};
// Backbone XMPP PubSub Storage v0.3
// (c) 2012 Yiorgis Gozadinos.
// Backbone.xmpp is distributed under the MIT license.
// http://github.com/ggozad/Backbone.xmpp
// A simple model/collection using **Backbone.xmpp.storage** and supporting XMPP
// notifications. Can be used to base your models upon.
(function ($, _, Backbone, Strophe, PubSubStorage) {
// PubSub Item
var PubSubItem = Backbone.Model.extend({
sync: Backbone.xmppSync
});
// PubSub Items collection
var PubSubNode = Backbone.Collection.extend({
model: PubSubItem,
node: null,
sync: Backbone.xmppSync,
// **initialize** expects the id of the node to be passed in `options`
// as well as the Strophe connection.
// If you do not know it ahead you should add the `node` attribute and
// subscribe to the XMPP events manually.
initialize: function (models, options) {
options = options || {};
if (options.id && options.connection) {
this.setNode(options.id, options.connection, options.payloadFormat);
}
},
setNode: function(id, connection, format) {
if (this.node) {
connection.PubSub.off('xmpp:pubsub:item-published:' + this.node.id, this.onItemPublished, this);
connection.PubSub.off('xmpp:pubsub:item-deleted:' + this.node.id, this.onItemDeleted, this);
}
this.node = new PubSubStorage(id, connection, format);
connection.PubSub.on('xmpp:pubsub:item-published:' + id, this.onItemPublished, this);
connection.PubSub.on('xmpp:pubsub:item-deleted:' + id, this.onItemDeleted, this);
},
// **onItemPublished** is a subscriber to the `xmpp:pubsub:item-published` event.
// When a model has been pushed to the server from a different client, it will be
// received and added automatically to the collection, triggering an `add` event.
// If the model already existed it will be updated triggering a `change` event.
onItemPublished: function (item) {
var payload = item.entry,
self = this,
d = $.Deferred(),
existing,
json;
d.promise().done(function () {
existing = self.get(item.id),
json = self.node.parseItem(payload);
if (existing) {
self.remove(existing, {silent: true});
self.add(existing, {at: 0, silent: true});
existing.set(json);
} else {
json.id = item.id;
self.add(json, {at: 0});
}
});
if (payload) {
d.resolve();
} else {
this.node.connection.PubSub.items(this.node.id, {item_ids: [item.id]})
.done(function (res) {
payload = $('entry', res);
d.resolve();
});
}
},
onItemDeleted: function (item) {
item = this.get(item.id);
if (item) {
this.remove(item);
}
}
});
this.PubSubItem = PubSubItem;
this.PubSubNode = PubSubNode;
})(this.jQuery, this._, this.Backbone, this.Strophe, this.PubSubStorage);
// Backbone XMPP PubSub Storage v0.3
// (c) 2012 Yiorgis Gozadinos.
// Backbone.xmpp is distributed under the MIT license.
// http://github.com/ggozad/Backbone.xmpp
// A simple module to replace **Backbone.sync** with *XMPP PubSub*-based
// persistence.
(function ($, _, Backbone, Strophe) {
// A PubSub node acting as storage.
// Create it with the `id` the node has on the XMPP server,
// and a Strophe `connection`.
var PubSubStorage = function(id, connection, payloadFormat) {
this.id = id;
this.connection = connection;
this.payloadFormat = payloadFormat || 'json';
};
// Attach methods to **PubSubStorage**.
_.extend(PubSubStorage.prototype, {
// **create** publishes to the node the model in JSON format.
//Resolves by setting the `id` on the item and returning it.
create: function(model) {
var d = $.Deferred(), res = {};
this._publish(this.id, model)
.done(function (id) {
res[model.idAttribute] = id;
d.resolve(res);
})
.fail(d.reject);
return d.promise();
},
// **update** a model by re-publishing it on the node.
// Resolves with no result as under no circumstances the server will change any attributes.
update: function(model) {
var d = $.Deferred();
this._publish(this.id, model, model.id)
.done(function () { d.resolve(); })
.fail(d.reject);
return d.promise();
},
// **getItem** retrieves a model from the node by `id`.
// Resolves by returning the attributes of the model that are different and their values.
getItem: function(model) {
var d = $.Deferred(), that = this;
this.connection.PubSub.items(this.id, {item_ids: [model.id]})
.done(function (item) {
var updated = {},
attrs = that.parseItem(item);
_.each(attrs, function (value, key) {
if (model.get(key) !== value) updated[key] = value;
});
d.resolve(updated);
})
.fail(d.reject);
return d.promise();
},
// **getItems** retrieves all models from the node.
// Resolves by returning a list of all its models in JSON format.
getItems: function(options) {
var d = $.Deferred(), that = this;
this.connection.PubSub.items(this.id, options)
.done(function (data) {
var attrs,
items = data.rsm ? data.items : data;
d.resolve(_.map(items, function (item) {
attrs = that.parseItem($('entry', item));
attrs.id = $(item).attr('id');
return attrs;
}), data.rsm);
})
.fail(d.reject);
return d.promise();
},
// **destroy** deletes the item correcsponding to the `model` from the node.
// Resolves by returning the `iq` response.
destroy: function(model) {
return this.connection.PubSub.deleteItem(this.id, model.id);
},
// Publish in particular format
_publish: function(node, model, item_id) {
if (this.payloadFormat === 'atom') {
return this.connection.PubSub.publishAtom(node, model.toJSON(), item_id);
}
else {
var entry = $build('entry').t(JSON.stringify(model.toJSON())).tree();
return this.connection.PubSub.publish(node, entry, item_id);
}
},
parseItem: function(item) {
if (this.payloadFormat === 'atom') {
return this.connection.PubSub._AtomToJson(item);
}
else {
return JSON.parse($(item).text());
}
}
});
// **xmppAsync** is the replacement for **sync**. It delegates sync operations
// to the model or collection's `node` property, which should be an instance
// of **PubSubStorage**.
Backbone.xmppSync = function(method, model, options) {
var p,
node = model.node || (model.collection && model.collection.node);
options = options || {};
// If there is no node, fail directly, somebody did not read the docs.
if (!node) return $.Deferred().reject().promise();
switch (method) {
case "read": p = typeof model.id !== 'undefined' ? node.getItem(model) : node.getItems(options); break;
case "create": p = node.create(model); break;
case "update": p = node.update(model); break;
case "delete": p = node.destroy(model); break;
}
// Fallback for old-style callbacks.
if (options.success) p.done(options.success);
if (options.error) p.fail(options.error);
return p;
};
this.PubSubStorage = PubSubStorage;
})(this.jQuery, this._, this.Backbone, this.Strophe);
// XMPP plugins for Strophe v0.2
// (c) 2012 Yiorgis Gozadinos.
// strophe.plugins is distributed under the MIT license.
// http://github.com/ggozad/strophe.plugins
// Helpers for dealing with
// [XEP-0004: Data Forms](http://xmpp.org/extensions/xep-0004.html)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery', 'underscore', 'strophe'], function ($, _, Strophe) {
// Also create a global in case some scripts
// that are loaded still are looking for
// a global even when an AMD loader is in use.
return (Strophe.x = factory($, _, Strophe));
});
} else {
// Browser globals
Strophe.x = factory(root.$, root._, root.Strophe);
}
}(this,function ($, _, Strophe) {
// **Option** contructor
var Option = function (opts) {
opts = opts || {};
this.value = opts.value || '';
this.label = opts.label;
};
// **Option.prototype** **toXMl** and **toJSON** extensions.
_.extend(Option.prototype, {
toXML: function () {
var el, attrs = {};
if (this.label) attrs.label = this.label;
el = $build('option', attrs)
.c('value').t(this.value.toString());
return el.tree();
},
toJSON: function () {
return {
label: this.label,
value: this.value
};
}
});
// Creates an **Option** from XML
Option.fromXML = function (xml) {
return new Option({
label: ($(xml)).attr('label'),
value: ($(xml)).text()
});
};
// **Field** constructor
var Field = function (opts) {
opts = opts || {};
this.type = opts.type || 'text-single';
this['var'] = opts['var'] || 'undefined';
this.desc = opts.desc;
this.label = opts.label;
this.required = opts.required === true || opts.required === 'true' || false;
this.options = opts.options || [];
this.values = opts.values || [];
if (opts.value) this.values.push(opts.value);
return this;
};
// **Field.prototype** **toXMl** and **toJSON** extensions.
_.extend(Field.prototype, {
toXML: function() {
var attrs = {
type: this.type,
'var': this['var']
};
if (this.label) attrs.label = this.label;
xml = $build('field', attrs);
if (this.desc) xml.c('desc').t(this.desc).up();
if (this.required) xml.c('required').up();
_.each(this.values, function (value) {
xml.c('value').t(value.toString()).up();
});
_.each(this.options, function (option) {
xml.cnode(option.toXML()).up();
});
return xml.tree();
},
toJSON: function () {
return {
type: this.type,
'var': this['var'],
desc: this.desc,
label: this.label,
required: this.required,
options: _.map(this.options, function (option) { return option.toJSON(); }),
values: this.values
};
}
});
// Creates a **Field** from XML
Field.fromXML = function (xml) {
xml = $(xml);
return new Field({
type: xml.attr('type'),
'var': xml.attr('var'),
label: xml.attr('label'),
desc: xml.find('desc').text(),
required: xml.find('required').length === 1,
options: _.map($('option', xml), function (option) { return new Option.fromXML(option);}),
values: _.map($('>value', xml), function (value) { return $(value).text(); })
});
};
// **Form** constructor
var Form = function (opts) {
opts = opts || {};
this.type = opts.type || 'form';
this.fields = opts.fields || [];
this.title = opts.title;
this.instructions = opts.instructions;
return this;
};
// **Form.prototype** **toXMl** and **toJSON** extensions.
_.extend(Form.prototype, {
toXML: function () {
var xml = $build('x', {
xmlns: 'jabber:x:data',
type: this.type
});
if (this.title) xml.c('title').t(this.title.toString()).up();
if (this.instructions) xml.c('instructions').t(this.instructions.toString()).up();
_.each(this.fields, function (field) { xml.cnode(field.toXML()).up(); });
return xml.tree();
},
toJSON: function () {
return {
type: this.type,
title: this.title,
instructions: this.instructions,
fields: _.map(this.fields, function (field) { return field.toJSON(); })
};
}
});
// Creates a **Form** from XML
Form.fromXML = function (xml) {
xml = $(xml);
return new Form({
type: xml.attr('type'),
title: xml.find('title').text(),
instructions: xml.find('instructions').text(),
fields: _.map($('>field', xml), function (field) { return new Field.fromXML(field); })
});
};
// Attach to **Strophe** as `x`. No need for a plugin.
return {
Form: Form,
Field: Field,
Option: Option
};
}));
This source diff could not be displayed because it is too large. You can view the blob instead.
// XMPP plugins for Strophe v0.2
// (c) 2012 Yiorgis Gozadinos.
// strophe.plugins is distributed under the MIT license.
// http://github.com/ggozad/strophe.plugins
// A Pub-Sub plugin partially implementing
// [XEP-0060 Publish-Subscribe](http://xmpp.org/extensions/xep-0060.html)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery', 'underscore', 'backbone', 'strophe'], function ($, _, Backbone, Strophe) {
// Also create a global in case some scripts
// that are loaded still are looking for
// a global even when an AMD loader is in use.
return factory($, _, Backbone, Strophe);
});
} else {
// Browser globals
factory(root.$, root._, root.Backbone, root.Strophe);
}
}(this,function ($, _, Backbone, Strophe) {
// Add the **PubSub** plugin to Strophe
Strophe.addConnectionPlugin('PubSub', {
_connection: null,
service: null,
events: {},
// **init** adds the various namespaces we use and extends the component
// from **Backbone.Events**.
init: function (connection) {
this._connection = connection;
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
Strophe.addNamespace('PUBSUB_EVENT', Strophe.NS.PUBSUB + '#event');
Strophe.addNamespace('PUBSUB_OWNER', Strophe.NS.PUBSUB + '#owner');
Strophe.addNamespace('PUBSUB_NODE_CONFIG', Strophe.NS.PUBSUB + '#node_config');
Strophe.addNamespace('ATOM', 'http://www.w3.org/2005/Atom');
Strophe.addNamespace('DELAY', 'urn:xmpp:delay');
Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
_.extend(this, Backbone.Events);
},
// Register to PEP events when connected
statusChanged: function (status, condition) {
if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
this.service = 'pubsub.' + Strophe.getDomainFromJid(this._connection.jid);
this._connection.addHandler(this._onReceivePEPEvent.bind(this), null, 'message', null, null, this.service);
}
},
// Handle PEP events and trigger own events.
_onReceivePEPEvent: function (ev) {
var self = this,
delay = $('delay[xmlns="' + Strophe.NS.DELAY + '"]', ev).attr('stamp');
$('item', ev).each(function (idx, item) {
var node = $(item).parent().attr('node'),
id = $(item).attr('id'),
entry = $('entry', item).filter(':first');
if (entry.length) {
entry = entry[0];
} else {
entry = null;
}
if (delay) {
// PEP event for the last-published item on a node.
self.trigger('xmpp:pubsub:last-published-item', {
node: node,
id: id,
entry: entry,
timestamp: delay
});
self.trigger('xmpp:pubsub:last-published-item:' + node, {
id: id,
entry: entry,
timestamp: delay
});
} else {
// PEP event for an item newly published on a node.
self.trigger('xmpp:pubsub:item-published', {
node: node,
id: id,
entry: entry
});
self.trigger('xmpp:pubsub:item-published:' + node, {
id: id,
entry: entry
});
}
});
// PEP event for the item deleted from a node.
$('retract', ev).each(function (idx, item) {
var node = $(item).parent().attr('node'),
id = $(item).attr('id');
self.trigger('xmpp:pubsub:item-deleted', {node: node, id: id});
self.trigger('xmpp:pubsub:item-deleted:' + node, {id: id});
});
return true;
},
// **createNode** creates a PubSub node with id `node` with configuration options defined by `options`.
// See [http://xmpp.org/extensions/xep-0060.html#owner-create](http://xmpp.org/extensions/xep-0060.html#owner-create)
createNode: function (node, options) {
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'set', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB})
.c('create', {node: node}),
fields = [],
option,
form;
if (options) {
fields.push(new Strophe.x.Field({'var': 'FORM_TYPE', type: 'hidden', value: Strophe.NS.PUBSUB_NODE_CONFIG}));
_.each(options, function (value, option) {
fields.push(new Strophe.x.Field({'var': option, value: value}));
});
form = new Strophe.x.Form({type: 'submit', fields: fields});
iq.up().c('configure').cnode(form.toXML());
}
this._connection.sendIQ(iq, d.resolve, d.reject);
return d.promise();
},
// **deleteNode** deletes the PubSub node with id `node`.
// See [http://xmpp.org/extensions/xep-0060.html#owner-delete](http://xmpp.org/extensions/xep-0060.html#owner-delete)
deleteNode: function (node) {
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'set', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB_OWNER})
.c('delete', {node: node});
this._connection.sendIQ(iq, d.resolve, d.reject);
return d.promise();
},
// **getNodeConfig** returns the node's with id `node` configuration options in JSON format.
// See [http://xmpp.org/extensions/xep-0060.html#owner-configure](http://xmpp.org/extensions/xep-0060.html#owner-configure)
getNodeConfig: function (node) {
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'get', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB_OWNER})
.c('configure', {node: node}),
form;
this._connection.sendIQ(iq, function (result) {
form = Strophe.x.Form.fromXML($('x', result));
d.resolve(form.toJSON().fields);
}, d.reject);
return d.promise();
},
// **discoverNodes** returns the nodes of a *Collection* node with id `node`.
// If `node` is not passed, the nodes of the root node on the service are returned instead.
// See [http://xmpp.org/extensions/xep-0060.html#entity-nodes](http://xmpp.org/extensions/xep-0060.html#entity-nodes)
discoverNodes: function (node) {
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'get', id: this._connection.getUniqueId('pubsub')});
if (node) {
iq.c('query', {xmlns: Strophe.NS.DISCO_ITEMS, node: node});
} else {
iq.c('query', {xmlns: Strophe.NS.DISCO_ITEMS});
}
this._connection.sendIQ(iq,
function (result) {
d.resolve($.map($('item', result), function (item, idx) { return $(item).attr('node'); }));
}, d.reject);
return d.promise();
},
// **publish** publishes `item`, an XML tree typically built with **$build** to the node specific by `node`.
// Optionally, takes `item_id` as the desired id of the item.
// Resolves on success to the id of the item on the node.
// See [http://xmpp.org/extensions/xep-0060.html#publisher-publish](http://xmpp.org/extensions/xep-0060.html#publisher-publish)
publish: function (node, item, item_id) {
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'set', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB})
.c('publish', {node: node})
.c('item', item_id ? {id: item_id} : {})
.cnode(item);
this._connection.sendIQ(iq.tree(),
function (result) {
d.resolve($('item', result).attr('id'));
}, d.reject);
return d.promise();
},
// **publishAtom** publishes a JSON object as an ATOM entry.
publishAtom: function (node, json, item_id) {
json.updated = json.updated || (this._ISODateString(new Date()));
return this.publish(node, this._JsonToAtom(json), item_id);
},
// **deleteItem** deletes the item with id `item_id` from the node with id `node`.
// `notify` specifies whether the service should notify all subscribers with a PEP event.
// See [http://xmpp.org/extensions/xep-0060.html#publisher-delete](http://xmpp.org/extensions/xep-0060.html#publisher-delete)
deleteItem: function(node, item_id, notify) {
notify = notify || true;
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'set', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB })
.c('retract', notify ? {node: node, notify: "true"} : {node: node})
.c('item', {id: item_id});
this._connection.sendIQ(iq.tree(), d.resolve, d.reject);
return d.promise();
},
// **items** retrieves the items from the node with id `node`.
// Optionally, you can specify `max_items` to retrieve a maximum number of items,
// or a list of item ids with `item_ids` in `options` parameters.
// See [http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve](http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve)
// Resolves with an array of items.
// Also if your server supports [Result Set Management](http://xmpp.org/extensions/xep-0059.html)
// on PubSub nodes, you can pass in options an `rsm` object literal with `before`, `after`, `max` parameters.
// You cannot specify both `rsm` and `max_items` or `items_ids`.
// Requesting with `rsm` will resolve with an object literal with `items` providing a list of the items retrieved,
//and `rsm` with `last`, `first`, `count` properties.
items: function (node, options) {
var d = $.Deferred(),
iq = $iq({to: this.service, type: 'get'})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB })
.c('items', {node: node});
options = options || {};
if (options.rsm) {
var rsm = $build('set', {xmlns: Strophe.NS.RSM});
_.each(options.rsm, function (val, key) { rsm.c(key, {}, val); });
iq.up();
iq.cnode(rsm.tree());
} else if (options.max_items) {
iq.attrs({max_items: options.max_items});
} else if (options.item_ids) {
_.each(options.item_ids, function (id) {
iq.c('item', {id: id}).up();
});
}
this._connection.sendIQ(iq.tree(),
function (res) {
var items = _.map($('item', res), function (item) {
return item.cloneNode(true);
});
if (options.rsm && $('set', res).length) {
d.resolve({
items: items,
rsm: {
count: parseInt($('set > count', res).text(), 10),
first: $('set >first', res).text(),
last: $('set > last', res).text()
}
});
} else {
d.resolve(items);
}
}, d.reject);
return d.promise();
},
// **subscribe** subscribes the user's bare JID to the node with id `node`.
// See [http://xmpp.org/extensions/xep-0060.html#subscriber-subscribe](http://xmpp.org/extensions/xep-0060.html#subscriber-subscribe)
subscribe: function (node) {
var d = $.Deferred();
var iq = $iq({to: this.service, type: 'set', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB })
.c('subscribe', {node: node, jid: Strophe.getBareJidFromJid(this._connection.jid)});
this._connection.sendIQ(iq, d.resolve, d.reject);
return d.promise();
},
// **unsubscribe** unsubscribes the user's bare JID from the node with id `node`. If managing multiple
// subscriptions it is possible to optionally specify the `subid`.
// See [http://xmpp.org/extensions/xep-0060.html#subscriber-unsubscribe](http://xmpp.org/extensions/xep-0060.html#subscriber-unsubscribe)
unsubscribe: function (node, subid) {
var d = $.Deferred();
var iq = $iq({to: this.service, type: 'set', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB })
.c('unsubscribe', {node: node, jid: Strophe.getBareJidFromJid(this._connection.jid)});
if (subid) iq.attrs({subid: subid});
this._connection.sendIQ(iq, d.resolve, d.reject);
return d.promise();
},
// **getSubscriptions** retrieves the subscriptions of the user's bare JID to the service.
// See [http://xmpp.org/extensions/xep-0060.html#entity-subscriptions](http://xmpp.org/extensions/xep-0060.html#entity-subscriptions)
getSubscriptions: function () {
var d = $.Deferred();
var iq = $iq({to: this.service, type: 'get', id: this._connection.getUniqueId('pubsub')})
.c('pubsub', {xmlns: Strophe.NS.PUBSUB})
.c('subscriptions'),
$item;
this._connection.sendIQ(iq.tree(),
function (res) {
d.resolve(_.map($('subscription', res), function (item) {
$item = $(item);
return {
node: $item.attr('node'),
jid: $item.attr('jid'),
subid: $item.attr('subid'),
subscription: $item.attr('subscription')
};
}));
}, d.reject);
return d.promise();
},
// Private utility functions
// **_ISODateString** converts a date to an ISO-formatted string.
_ISODateString: function (d) {
function pad(n) {
return n < 10 ? '0' + n : n;
}
return d.getUTCFullYear() + '-' +
pad(d.getUTCMonth() + 1) + '-' +
pad(d.getUTCDate()) + 'T' +
pad(d.getUTCHours()) + ':' +
pad(d.getUTCMinutes()) + ':' +
pad(d.getUTCSeconds()) + 'Z';
},
// **_JsonToAtom** produces an atom-format XML tree from a JSON object.
_JsonToAtom: function (obj, tag) {
var builder;
if (!tag) {
builder = $build('entry', {xmlns: Strophe.NS.ATOM});
} else {
builder = $build(tag);
}
_.each(obj, function (value, key) {
if (typeof value === 'string') {
builder.c(key, {}, value);
} else if (typeof value === 'number') {
builder.c(key, {}, value.toString());
} else if (typeof value === 'boolean') {
builder.c(key, {}, value.toString());
} else if (typeof value === 'object' && 'toUTCString' in value) {
builder.c(key, {}, this._ISODateString(value));
} else if (typeof value === 'object') {
builder.cnode(this._JsonToAtom(value, key)).up();
} else {
this.c(key).up();
}
}, this);
return builder.tree();
},
// **_AtomToJson** produces a JSON object from an atom-formatted XML tree.
_AtomToJson: function (xml) {
var json = {},
self = this,
jqEl,
val;
$(xml).children().each(function (idx, el) {
jqEl = $(el);
if (jqEl.children().length === 0) {
val = jqEl.text();
if ($.isNumeric(val)) {
val = Number(val);
}
json[el.nodeName.toLowerCase()] = val;
} else {
json[el.nodeName.toLowerCase()] = self._AtomToJson(el);
}
});
return json;
}
});
}));
var app = app || {};
(function() {
'use strict';
// Todo Model
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
app.Todo = PubSubItem.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
completed: false
},
// Toggle the `completed` state of this todo item.
toggle: function() {
this.save({
completed: !this.get('completed')
});
}
});
}());
var app = app || {};
(function() {
'use strict';
// Todo Router
// ----------
var Workspace = Backbone.Router.extend({
routes:{
'*filter': 'setFilter'
},
setFilter: function( param ) {
// Set the current filter to be used
window.app.TodoFilter = param.trim() || '';
// Trigger a collection reset/addAll
window.app.Todos.trigger('reset');
}
});
app.TodoRouter = new Workspace();
}());
var app = app || {};
$(function( $ ) {
'use strict';
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
app.AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: '#todoapp',
// Our template for the line of statistics at the bottom of the app.
statsTemplate: _.template( $('#stats-template').html() ),
// Delegated events for creating new items, and clearing completed ones.
events: {
'keypress #new-todo': 'createOnEnter',
'click #clear-completed': 'clearCompleted',
'click #toggle-all': 'toggleAllComplete'
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize: function() {
this.input = this.$('#new-todo');
this.allCheckbox = this.$('#toggle-all')[0];
window.app.Todos.on( 'add', this.addAll, this );
window.app.Todos.on( 'reset', this.addAll, this );
window.app.Todos.on( 'change:completed', this.addAll, this );
window.app.Todos.on( 'all', this.render, this );
this.$footer = this.$('#footer');
this.$main = this.$('#main');
app.Todos.fetch();
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function() {
var completed = app.Todos.completed().length;
var remaining = app.Todos.remaining().length;
if ( app.Todos.length ) {
this.$main.show();
this.$footer.show();
this.$footer.html(this.statsTemplate({
completed: completed,
remaining: remaining
}));
this.$('#filters li a')
.removeClass('selected')
.filter('[href="#/' + ( app.TodoFilter || '' ) + '"]')
.addClass('selected');
} else {
this.$main.hide();
this.$footer.hide();
}
this.allCheckbox.checked = !remaining;
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function( todo ) {
var view = new app.TodoView({ model: todo });
$('#todo-list').append( view.render().el );
},
// Add all items in the **Todos** collection at once.
addAll: function() {
this.$('#todo-list').html('');
switch( app.TodoFilter ) {
case 'active':
_.each( app.Todos.remaining(), this.addOne );
break;
case 'completed':
_.each( app.Todos.completed(), this.addOne );
break;
default:
app.Todos.each( this.addOne, this );
break;
}
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
title: this.input.val().trim(),
order: app.Todos.nextOrder(),
completed: false
};
},
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function( e ) {
if ( e.which !== ENTER_KEY || !this.input.val().trim() ) {
return;
}
app.Todos.create( this.newAttributes(), {wait: true} );
this.input.val('');
},
// Clear all completed todo items, destroying their models.
clearCompleted: function() {
_.each( window.app.Todos.completed(), function( todo ) {
todo.destroy();
});
return false;
},
toggleAllComplete: function() {
var completed = this.allCheckbox.checked;
app.Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
}
});
});
var app = app || {};
$(function() {
'use strict';
// Todo Item View
// --------------
// The DOM element for a todo item...
app.TodoView = Backbone.View.extend({
//... is a list tag.
tagName: 'li',
// Cache the template function for a single item.
template: _.template( $('#item-template').html() ),
// The DOM events specific to an item.
events: {
'click .toggle': 'togglecompleted',
'dblclick label': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close'
},
// The TodoView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between a **Todo** and a **TodoView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
this.model.on( 'change', this.render, this );
this.model.on( 'destroy', this.remove, this );
this.model.collection.on( 'remove', this.remoteRemove, this);
},
remoteRemove: function (model) {
if (model.id === this.model.id) {
this.remove();
}
},
// Re-render the titles of the todo item.
render: function() {
this.$el.html( this.template( this.model.toJSON() ) );
this.$el.toggleClass( 'completed', this.model.get('completed') );
this.input = this.$('.edit');
return this;
},
// Toggle the `"completed"` state of the model.
togglecompleted: function() {
this.model.toggle();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit: function() {
this.$el.addClass('editing');
this.input.focus();
},
// Close the `"editing"` mode, saving changes to the todo.
close: function() {
var value = this.input.val().trim();
if ( value ) {
this.model.save({ title: value });
} else {
this.clear();
}
this.$el.removeClass('editing');
},
// If you hit `enter`, we're through editing the item.
updateOnEnter: function( e ) {
if ( e.which === ENTER_KEY ) {
this.close();
}
},
// Remove the item, destroy the model from *localStorage* and delete its view.
clear: function() {
this.model.destroy();
}
});
});
##############################################################################
#
# Copyright (c) 2006 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bootstrap a buildout-based project
Simply run this script in a directory containing a buildout.cfg.
The script accepts buildout command-line options, so you can
use the -c option to specify an alternate configuration file.
$Id$
"""
import os, shutil, sys, tempfile, urllib2
from optparse import OptionParser
tmpeggs = tempfile.mkdtemp()
is_jython = sys.platform.startswith('java')
# parsing arguments
parser = OptionParser(
'This is a custom version of the zc.buildout %prog script. It is '
'intended to meet a temporary need if you encounter problems with '
'the zc.buildout 1.5 release.')
parser.add_option("-v", "--version", dest="version", default='1.5.2',
help='Use a specific zc.buildout version. *This '
'bootstrap script defaults to '
'1.5.2, unlike usual buildout bootstrap scripts.*')
parser.add_option("-d", "--distribute",
action="store_true", dest="distribute", default=True,
help="Use Disribute rather than Setuptools.")
parser.add_option("-c", None, action="store", dest="config_file",
help=("Specify the path to the buildout configuration "
"file to be used."))
options, args = parser.parse_args()
# if -c was provided, we push it back into args for buildout' main function
if options.config_file is not None:
args += ['-c', options.config_file]
if options.version is not None:
VERSION = '==%s' % options.version
else:
VERSION = ''
USE_DISTRIBUTE = options.distribute
args = args + ['bootstrap']
to_reload = False
try:
import pkg_resources
if not hasattr(pkg_resources, '_distribute'):
to_reload = True
raise ImportError
except ImportError:
ez = {}
if USE_DISTRIBUTE:
exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True)
else:
exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
if to_reload:
reload(pkg_resources)
else:
import pkg_resources
if sys.platform == 'win32':
def quote(c):
if ' ' in c:
return '"%s"' % c # work around spawn lamosity on windows
else:
return c
else:
def quote (c):
return c
ws = pkg_resources.working_set
if USE_DISTRIBUTE:
requirement = 'distribute'
else:
requirement = 'setuptools'
env = dict(os.environ,
PYTHONPATH=
ws.find(pkg_resources.Requirement.parse(requirement)).location
)
cmd = [quote(sys.executable),
'-c',
quote('from setuptools.command.easy_install import main; main()'),
'-mqNxd',
quote(tmpeggs)]
if 'bootstrap-testing-find-links' in os.environ:
cmd.extend(['-f', os.environ['bootstrap-testing-find-links']])
cmd.append('zc.buildout' + VERSION)
if is_jython:
import subprocess
exitcode = subprocess.Popen(cmd, env=env).wait()
else: # Windows prefers this, apparently; otherwise we would prefer subprocess
exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
assert exitcode == 0
ws.add_entry(tmpeggs)
ws.require('zc.buildout' + VERSION)
import zc.buildout.buildout
zc.buildout.buildout.main(args)
shutil.rmtree(tmpeggs)
[buildout]
parts =
ejabberd-conf
# ejabberd
[ejabberd-conf]
recipe = collective.recipe.template
input = templates/ejabberd.cfg.in
output = ${buildout:directory}/etc/ejabberd.cfg
pubsub_max_items_node = 1000
xmppdomain = localhost
admin_userid = admin
[ejabberd]
recipe = rod.recipe.ejabberd
erlang-path = /usr/local/bin
url = http://www.process-one.net/downloads/ejabberd/2.1.11/ejabberd-2.1.11.tgz
{loglevel, 4}.
{hosts, ["${xmppdomain}"]}.
{listen,
[
{{5222, {127, 0, 0, 1}}, ejabberd_c2s, [
{access, c2s},
{shaper, c2s_shaper},
{max_stanza_size, 65536}
]},
{{5269, {127, 0, 0, 1}}, ejabberd_s2s_in, [
{shaper, s2s_shaper},
{max_stanza_size, 131072}
]},
{{5280, {127, 0, 0, 1}}, ejabberd_http, [
http_bind,
web_admin
]}
]}.
{auth_method, internal}.
{shaper, normal, {maxrate, 1000}}.
{shaper, fast, {maxrate, 50000}}.
{host_config, "${xmppdomain}", [{acl, admin, {user, "${admin_userid}", "${xmppdomain}"}}]}.
{acl, local, {user_regexp, ""}}.
{access, max_user_sessions, [{10, all}]}.
{access, local, [{allow, local}]}.
{access, c2s, [{deny, blocked},
{allow, all}]}.
{access, c2s_shaper, [{none, admin},
{normal, all}]}.
{access, s2s_shaper, [{fast, all}]}.
{access, announce, [{allow, admin}]}.
{access, configure, [{allow, admin}]}.
{access, muc_admin, [{allow, admin}]}.
{access, muc, [{allow, all}]}.
{access, muc_create, [{allow, local}]}.
{access, register, [{allow, all}]}.
{access, pubsub_createnode, [{allow, local}]}.
{language, "en"}.
{modules,
[
{mod_adhoc, []},
{mod_announce, [{access, announce}]},
{mod_caps, []},
{mod_configure,[]},
{mod_disco, []},
{mod_http_bind,[]},
{mod_last, []},
{mod_offline, []},
{mod_privacy, []},
{mod_private, []},
{mod_pubsub, [
{access_createnode, pubsub_createnode},
{ignore_pep_from_offline, false},
{last_item_cache, false},
{nodetree, "dag"},
{plugins, ["flat", "pep"]},
{max_items_node, ${pubsub_max_items_node}}
]},
{mod_register, [
{welcome_message, {"Welcome!",
"Welcome to ${xmppdomain} Jabber server."}},
{access, register}
]},
{mod_roster, []},
{mod_shared_roster,[]},
{mod_time, []},
{mod_vcard, []},
{mod_version, []}
]}.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment