Commit 80e0caa2 authored by Sindre Sorhus's avatar Sindre Sorhus

Remove Backbone.xmpp

fixes #446
fixes #474

It's offline and not maintained anymore.
parent 397d38ee
{
"name": "backbone.xmpp",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.6",
"jquery": "~2.0.0",
"lodash": "~1.2.1",
"backbone": "~1.0.0",
"Strophe.js": "~1.0.2"
}
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
button,
input[type="checkbox"] {
outline: none;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}
(function () {
'use strict';
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base;
[/labs/, /\w*-examples/].forEach(function (href) {
var match = location.href.match(href);
if (!base && match) {
base = location.href.indexOf(match);
}
});
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').getAttribute('data-framework');
}
if (template && learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.template = template;
this.append();
}
}
Learn.prototype.append = function () {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
});
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
redirect();
getFile('learn.json', Learn);
})();
<!doctype html>
<html lang="en" data-framework="backbonejs">
<head>
<meta charset="utf-8">
<title>Backbone.js • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
</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>
<footer 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>
</footer>
<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="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/lodash/lodash.js"></script>
<script src="bower_components/backbone/backbone.js"></script>
<script src="bower_components/Strophe.js/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
};
}));
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.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.$input = this.$('#new-todo');
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();
}
});
});
#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.
##############################################################################
#
# 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