Merge branch 'develop' of github.com:matrix-org/synapse into room_config

This commit is contained in:
Erik Johnston 2014-09-01 18:21:29 +01:00
commit 3faa2ae78c
43 changed files with 11416 additions and 252 deletions

View file

@ -1007,26 +1007,15 @@ for users from other servers entirely.
Presence
========
In the following messages, the presence state is an integer enumeration of the
following states:
0 : OFFLINE
1 : BUSY
2 : ONLINE
3 : FREE_TO_CHAT
Aside from OFFLINE, the protocol doesn't assign any special meaning to these
states; they are provided as an approximate signal for users to give to other
users and for clients to present them in some way that may be useful. Clients
could have different behaviours for different states of the user's presence, for
example to decide how much prominence or sound to use for incoming event
notifications.
In the following messages, the presence state is a presence string as described in
the main specification document.
Getting/Setting your own presence state
---------------------------------------
REST Path: /presence/$user_id/status
Valid methods: GET/PUT
Required keys:
state : [0|1|2|3] - The user's new presence state
presence : <string> - The user's new presence state
Optional keys:
status_msg : text string provided by the user to explain their status
@ -1039,7 +1028,7 @@ Fetching your presence list
following keys:
{
"user_id" : string giving the observed user's ID
"state" : int giving their status
"presence" : int giving their status
"status_msg" : optional text string
"displayname" : optional text string from the user's profile
"avatar_url" : optional text string from the user's profile

View file

@ -3,31 +3,31 @@
"swaggerVersion": "1.2",
"apis": [
{
"path": "/login",
"path": "-login",
"description": "Login operations"
},
{
"path": "/registration",
"path": "-registration",
"description": "Registration operations"
},
{
"path": "/rooms",
"path": "-rooms",
"description": "Room operations"
},
{
"path": "/profile",
"path": "-profile",
"description": "Profile operations"
},
{
"path": "/presence",
"path": "-presence",
"description": "Presence operations"
},
{
"path": "/events",
"path": "-events",
"description": "Event operations"
},
{
"path": "/directory",
"path": "-directory",
"description": "Directory operations"
}
],

View file

@ -0,0 +1,5 @@
To get this running:
ln -s ../swagger_matrix
python -m SimpleHTTPServer
Go to http://localhost:8000/swagger.html

View file

@ -0,0 +1,38 @@
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);

View file

@ -0,0 +1,16 @@
/* latin */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 400;
src: local('Droid Sans'), local('DroidSans'), url(http://fonts.gstatic.com/s/droidsans/v5/s-BiyweUPV0v-yRb-cjciPk_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
/* latin */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 700;
src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(http://fonts.gstatic.com/s/droidsans/v5/EFpQQyG9GqCrobXxL-KRMYWiMMZ7xLd792ULpGE4W_Y.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,18 @@
/*
* jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010
* http://benalman.com/projects/jquery-bbq-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
(function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M<N?O[P]||(R[M+1]&&isNaN(R[M+1])?{}:[]):J}}else{if($.isArray(H[P])){H[P].push(J)}else{if(H[P]!==i){H[P]=[H[P],J]}else{H[P]=J}}}}else{if(P){H[P]=F?i:""}}});return H};function z(H,F,G){if(F===i||typeof F==="boolean"){G=F;F=a[H?D:A]()}else{F=E(F)?F.replace(H?w:x,""):F}return l(F,G)}l[A]=B(z,0);l[D]=v=B(z,1);$[y]||($[y]=function(F){return $.extend(C,F)})({a:k,base:k,iframe:t,img:t,input:t,form:"action",link:k,script:t});j=$[y];function s(I,G,H,F){if(!E(H)&&typeof H!=="object"){F=H;H=G;G=i}return this.each(function(){var L=$(this),J=G||j()[(this.nodeName||"").toLowerCase()]||"",K=J&&L.attr(J)||"";L.attr(J,a[I](K,H,F))})}$.fn[A]=B(s,A);$.fn[D]=B(s,D);b.pushState=q=function(I,F){if(E(I)&&/^#/.test(I)&&F===i){F=2}var H=I!==i,G=c(p[g][k],H?I:{},H?F:2);p[g][k]=G+(/#/.test(G)?"":"#")};b.getState=u=function(F,G){return F===i||typeof F==="boolean"?v(F):v(G)[F]};b.removeState=function(F){var G={};if(F!==i){G=u();$.each($.isArray(F)?F:arguments,function(I,H){delete G[H]})}q(G,2)};e[d]=$.extend(e[d],{add:function(F){var H;function G(J){var I=J[D]=c();J.getState=function(K,L){return K===i||typeof K==="boolean"?l(I,K):l(I,L)[K]};H.apply(this,arguments)}if($.isFunction(F)){H=F;return G}else{H=F.handler;F.handler=G}}})})(jQuery,this);
/*
* jQuery hashchange event - v1.2 - 2/11/2010
* http://benalman.com/projects/jquery-hashchange-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
(function($,i,b){var j,k=$.event.special,c="location",d="hashchange",l="href",f=$.browser,g=document.documentMode,h=f.msie&&(g===b||g<8),e="on"+d in i&&!h;function a(m){m=m||i[c][l];return m.replace(/^[^#]*#?(.*)$/,"$1")}$[d+"Delay"]=100;k[d]=$.extend(k[d],{setup:function(){if(e){return false}$(j.start)},teardown:function(){if(e){return false}$(j.stop)}});j=(function(){var m={},r,n,o,q;function p(){o=q=function(s){return s};if(h){n=$('<iframe src="javascript:0"/>').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this);

View file

@ -0,0 +1 @@
(function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery);

View file

@ -0,0 +1,8 @@
/*
jQuery Wiggle
Author: WonderGroup, Jordan Thomas
URL: http://labs.wondergroup.com/demos/mini-ui/index.html
License: MIT (http://en.wikipedia.org/wiki/MIT_License)
*/
jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('<div class="wiggle-wrap"></div>').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);}
if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});};

View file

@ -0,0 +1,125 @@
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,211 @@
var appName;
var popupMask;
var popupDialog;
var clientId;
var realm;
function handleLogin() {
var scopes = [];
if(window.swaggerUi.api.authSchemes
&& window.swaggerUi.api.authSchemes.oauth2
&& window.swaggerUi.api.authSchemes.oauth2.scopes) {
scopes = window.swaggerUi.api.authSchemes.oauth2.scopes;
}
if(window.swaggerUi.api
&& window.swaggerUi.api.info) {
appName = window.swaggerUi.api.info.title;
}
if(popupDialog.length > 0)
popupDialog = popupDialog.last();
else {
popupDialog = $(
[
'<div class="api-popup-dialog">',
'<div class="api-popup-title">Select OAuth2.0 Scopes</div>',
'<div class="api-popup-content">',
'<p>Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
'<a href="#">Learn how to use</a>',
'</p>',
'<p><strong>' + appName + '</strong> API requires the following scopes. Select which ones you want to grant to Swagger UI.</p>',
'<ul class="api-popup-scopes">',
'</ul>',
'<p class="error-msg"></p>',
'<div class="api-popup-actions"><button class="api-popup-authbtn api-button green" type="button">Authorize</button><button class="api-popup-cancel api-button gray" type="button">Cancel</button></div>',
'</div>',
'</div>'].join(''));
$(document.body).append(popupDialog);
popup = popupDialog.find('ul.api-popup-scopes').empty();
for (i = 0; i < scopes.length; i ++) {
scope = scopes[i];
str = '<li><input type="checkbox" id="scope_' + i + '" scope="' + scope.scope + '"/>' + '<label for="scope_' + i + '">' + scope.scope;
if (scope.description) {
str += '<br/><span class="api-scope-desc">' + scope.description + '</span>';
}
str += '</label></li>';
popup.append(str);
}
}
var $win = $(window),
dw = $win.width(),
dh = $win.height(),
st = $win.scrollTop(),
dlgWd = popupDialog.outerWidth(),
dlgHt = popupDialog.outerHeight(),
top = (dh -dlgHt)/2 + st,
left = (dw - dlgWd)/2;
popupDialog.css({
top: (top < 0? 0 : top) + 'px',
left: (left < 0? 0 : left) + 'px'
});
popupDialog.find('button.api-popup-cancel').click(function() {
popupMask.hide();
popupDialog.hide();
});
popupDialog.find('button.api-popup-authbtn').click(function() {
popupMask.hide();
popupDialog.hide();
var authSchemes = window.swaggerUi.api.authSchemes;
var host = window.location;
var redirectUrl = host.protocol + '//' + host.host + "/o2c.html";
var url = null;
var p = window.swaggerUi.api.authSchemes;
for (var key in p) {
if (p.hasOwnProperty(key)) {
var o = p[key].grantTypes;
for(var t in o) {
if(o.hasOwnProperty(t) && t === 'implicit') {
var dets = o[t];
url = dets.loginEndpoint.url + "?response_type=token";
window.swaggerUi.tokenName = dets.tokenName;
}
}
}
}
var scopes = []
var o = $('.api-popup-scopes').find('input:checked');
for(k =0; k < o.length; k++) {
scopes.push($(o[k]).attr("scope"));
}
window.enabledScopes=scopes;
url += '&redirect_uri=' + encodeURIComponent(redirectUrl);
url += '&realm=' + encodeURIComponent(realm);
url += '&client_id=' + encodeURIComponent(clientId);
url += '&scope=' + encodeURIComponent(scopes);
window.open(url);
});
popupMask.show();
popupDialog.show();
return;
}
function handleLogout() {
for(key in window.authorizations.authz){
window.authorizations.remove(key)
}
window.enabledScopes = null;
$('.api-ic.ic-on').addClass('ic-off');
$('.api-ic.ic-on').removeClass('ic-on');
// set the info box
$('.api-ic.ic-warning').addClass('ic-error');
$('.api-ic.ic-warning').removeClass('ic-warning');
}
function initOAuth(opts) {
var o = (opts||{});
var errors = [];
appName = (o.appName||errors.push("missing appName"));
popupMask = (o.popupMask||$('#api-common-mask'));
popupDialog = (o.popupDialog||$('.api-popup-dialog'));
clientId = (o.clientId||errors.push("missing client id"));
realm = (o.realm||errors.push("missing realm"));
if(errors.length > 0){
log("auth unable initialize oauth: " + errors);
return;
}
$('pre code').each(function(i, e) {hljs.highlightBlock(e)});
$('.api-ic').click(function(s) {
if($(s.target).hasClass('ic-off'))
handleLogin();
else {
handleLogout();
}
false;
});
}
function onOAuthComplete(token) {
if(token) {
if(token.error) {
var checkbox = $('input[type=checkbox],.secured')
checkbox.each(function(pos){
checkbox[pos].checked = false;
});
alert(token.error);
}
else {
var b = token[window.swaggerUi.tokenName];
if(b){
// if all roles are satisfied
var o = null;
$.each($('.auth #api_information_panel'), function(k, v) {
var children = v;
if(children && children.childNodes) {
var requiredScopes = [];
$.each((children.childNodes), function (k1, v1){
var inner = v1.innerHTML;
if(inner)
requiredScopes.push(inner);
});
var diff = [];
for(var i=0; i < requiredScopes.length; i++) {
var s = requiredScopes[i];
if(window.enabledScopes && window.enabledScopes.indexOf(s) == -1) {
diff.push(s);
}
}
if(diff.length > 0){
o = v.parentNode;
$(o.parentNode).find('.api-ic.ic-on').addClass('ic-off');
$(o.parentNode).find('.api-ic.ic-on').removeClass('ic-on');
// sorry, not all scopes are satisfied
$(o).find('.api-ic').addClass('ic-warning');
$(o).find('.api-ic').removeClass('ic-error');
}
else {
o = v.parentNode;
$(o.parentNode).find('.api-ic.ic-off').addClass('ic-on');
$(o.parentNode).find('.api-ic.ic-off').removeClass('ic-off');
// all scopes are satisfied
$(o).find('.api-ic').addClass('ic-info');
$(o).find('.api-ic').removeClass('ic-warning');
$(o).find('.api-ic').removeClass('ic-error');
}
}
});
window.authorizations.add("oauth2", new ApiKeyAuthorization("Authorization", "Bearer " + b, "header"));
}
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
// Underscore.js 1.3.3
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);

View file

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Matrix Client-Server API Documentation</title>
<link href="./files/css" rel="stylesheet" type="text/css">
<link href="./files/reset.css" media="screen" rel="stylesheet" type="text/css">
<link href="./files/screen.css" media="screen" rel="stylesheet" type="text/css">
<link href="./files/reset.css" media="print" rel="stylesheet" type="text/css">
<link href="./files/screen.css" media="print" rel="stylesheet" type="text/css">
<script type="text/javascript" src="./files/shred.bundle.js"></script>
<script src="./files/jquery-1.8.0.min.js" type="text/javascript"></script>
<script src="./files/jquery.slideto.min.js" type="text/javascript"></script>
<script src="./files/jquery.wiggle.min.js" type="text/javascript"></script>
<script src="./files/jquery.ba-bbq.min.js" type="text/javascript"></script>
<script src="./files/handlebars-1.0.0.js" type="text/javascript"></script>
<script src="./files/underscore-min.js" type="text/javascript"></script>
<script src="./files/backbone-min.js" type="text/javascript"></script>
<script src="./files/swagger.js" type="text/javascript"></script>
<script src="./files/swagger-ui.js" type="text/javascript"></script>
<script src="./files/highlight.7.3.pack.js" type="text/javascript"></script>
<!-- enabling this will enable oauth2 implicit scope support -->
<script src="./files/swagger-oauth.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
window.swaggerUi = new SwaggerUi({
url: "http://localhost:8000/swagger_matrix/api-docs",
dom_id: "swagger-ui-container",
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function(swaggerApi, swaggerUi){
log("Loaded SwaggerUI");
if(typeof initOAuth == "function") {
initOAuth({
clientId: "your-client-id",
realm: "your-realms",
appName: "your-app-name"
});
}
$('pre code').each(function(i, e) {
hljs.highlightBlock(e)
});
},
onFailure: function(data) {
log("Unable to Load SwaggerUI");
},
docExpansion: "none"
});
$('#input_apiKey').change(function() {
var key = $('#input_apiKey')[0].value;
log("key: " + key);
if(key && key.trim() != "") {
log("added key " + key);
window.authorizations.add("key", new ApiKeyAuthorization("access_token", key, "query"));
}
})
window.swaggerUi.load();
});
</script>
</head>
<body class="swagger-section">
<div id="header">
<div class="swagger-ui-wrap">
<a id="logo" href="http://swagger.wordnik.com/">swagger</a>
<form id="api_selector">
<div class="input"><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"></div>
<div class="input"><input placeholder="access_token" id="input_apiKey" name="apiKey" type="text"></div>
</form>
</div>
</div>
<div id="message-bar" class="swagger-ui-wrap message-fail">Can't read from server. It may not have the appropriate access-control-origin settings.</div>
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
</body></html>

View file

@ -168,6 +168,10 @@ Some standard error codes are below:
:``M_NOT_FOUND``:
No resource was found for this request.
:``M_LIMIT_EXCEEDED``:
Too many requests have been sent in a short period of time. Wait a while then
try again.
Some requests have unique error codes:
:``M_USER_IN_USE``:
@ -273,6 +277,7 @@ Example::
}
- TODO: This creates a room creation event which serves as the root of the PDU graph for this room.
- TODO: Keys for speccing a room name / room topic / invite these users?
Modifying aliases
-----------------
@ -284,12 +289,37 @@ Permissions
Joining rooms
-------------
- What is joining? What permissions / access does it give you? How does this affect /initialSync?
- API to hit (``/join/$alias or id``). Explain how alias joining works (auto-resolving). See "Room events" for more info.
- What does the home server have to do?
- Rooms that DON'T need an invite to join. This follows through onto inviting users section.
- Outline invite join dance?
- TODO: What does the home server have to do to join a user to a room?
Users need to join a room in order to send and receive events in that room. A user can join a
room by making a request to ``/join/<room alias or id>`` with::
{}
Alternatively, a user can make a request to ``/rooms/<room id>/join`` with the same request content.
This is only provided for symmetry with the other membership APIs: ``/rooms/<room id>/invite`` and
``/rooms/<room id>/leave``. If a room alias was specified, it will be automatically resolved to
a room ID, which will then be joined. The room ID that was joined will be returned in response::
{
"room_id": "!roomid:domain"
}
The membership state for the joining user can also be modified directly to be ``join``
by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "join"
}
See the "Room events" section for more information on ``m.room.member``.
After the user has joined a room, they will receive subsequent events in that room. This room
will now appear as an entry in the ``/initialSync`` API.
Some rooms enforce that a user is *invited* to a room before they can join that room. Other
rooms will allow anyone to join the room even if they have not received an invite.
Inviting users
--------------
@ -331,31 +361,162 @@ See the "Room events" section for more information on ``m.room.member``.
Leaving rooms
-------------
- API to hit (``$roomid/leave``). See "Room events" for more info.
- Must be joined to leave. How does this affect /initialSync?
- Not ever being in a room is NOT equivalent to have left it (due to membership: leave).
- Need to be re-invited if invite-only room.
- If no more HSes in room, can delete room?
- Is there a dance?
A user can leave a room to stop receiving events for that room. A user must have
joined the room before they are eligible to leave the room. If the room is an
"invite-only" room, they will need to be re-invited before they can re-join the room.
To leave a room, a request should be made to ``/rooms/<room id>/leave`` with::
{}
Alternatively, the membership state for this user in this room can be modified
directly by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "leave"
}
See the "Room events" section for more information on ``m.room.member``.
Once a user has left a room, that room will no longer appear on the ``/initialSync``
API. Be aware that leaving a room is not equivalent to have never been
in that room. A user who has previously left a room still maintains some residual state in
that room. Their membership state will be marked as ``leave``. This contrasts with
a user who has *never been invited or joined to that room* who will not have any
membership state for that room.
If all members in a room leave, that room becomes eligible for deletion.
- TODO: Grace period before deletion?
- TODO: Under what conditions should a room NOT be purged?
Events in a room
----------------
- Split into state and non-state data
- Explain what they are, semantics, give examples of clobbering / not, use cases (msgs vs room names).
Not too much detail on the actual event contents.
- API to hit.
- Extensibility provided by the API for custom events. Examples.
- How this hooks into ``initialSync``.
- See the "Room Events" section for actual spec on each type.
Room events can be split into two categories:
Syncing a room
--------------
- Single room initial sync. API to hit. Why it might be used (lazy loading)
:State Events:
These are events which replace events that came before it, depending on a set of unique keys.
These keys are the event ``type`` and a ``state_key``. Events with the same set of keys will
be overwritten. Typically, state events are used to store state, hence their name.
:Non-state events:
These are events which cannot be overwritten after sending. The list of events continues
to grow as more events are sent. As this list grows, it becomes necessary to
provide a mechanism for navigating this list. Pagination APIs are used to view the list
of historical non-state events. Typically, non-state events are used to send messages.
This specification outlines several events, all with the event type prefix ``m.``. However,
applications may wish to add their own type of event, and this can be achieved using the
REST API detailed in the following sections. If new events are added, the event ``type``
key SHOULD follow the Java package naming convention, e.g. ``com.example.myapp.event``.
This ensures event types are suitably namespaced for each application and reduces the
risk of clashes.
State events
------------
State events can be sent by ``PUT`` ing to ``/rooms/<room id>/state/<event type>/<state key>``.
These events will be overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all match.
If the state event has no ``state_key``, it can be omitted from the path. These requests
**cannot use transaction IDs** like other ``PUT`` paths because they cannot be differentiated
from the ``state_key``. Furthermore, ``POST`` is unsupported on state paths. Valid requests
look like::
PUT /rooms/!roomid:domain/state/m.example.event
{ "key" : "without a state key" }
PUT /rooms/!roomid:domain/state/m.another.example.event/foo
{ "key" : "with 'foo' as the state key" }
In contrast, these requests are invalid::
POST /rooms/!roomid:domain/state/m.example.event/
{ "key" : "cannot use POST here" }
PUT /rooms/!roomid:domain/state/m.another.example.event/foo/11
{ "key" : "txnIds are not supported" }
Care should be taken to avoid setting the wrong ``state key``::
PUT /rooms/!roomid:domain/state/m.another.example.event/11
{ "key" : "with '11' as the state key, but was probably intended to be a txnId" }
The ``state_key`` is often used to store state about individual users, by using the user ID as the
``state_key`` value. For example::
PUT /rooms/!roomid:domain/state/m.favorite.animal.event/%40my_user%3Adomain.com
{ "animal" : "cat", "reason": "fluffy" }
In some cases, there may be no need for a ``state_key``, so it can be omitted::
PUT /rooms/!roomid:domain/state/m.room.bgd.color
{ "color": "red", "hex": "#ff0000" }
See "Room Events" for the ``m.`` event specification.
Non-state events
----------------
Non-state events can be sent by sending a request to ``/rooms/<room id>/send/<event type>``.
These requests *can* use transaction IDs and ``PUT``/``POST`` methods. Non-state events
allow access to historical events and pagination, making it best suited for sending messages.
For example::
POST /rooms/!roomid:domain/send/m.custom.example.message
{ "text": "Hello world!" }
PUT /rooms/!roomid:domain/send/m.custom.example.message/11
{ "text": "Goodbye world!" }
See "Room Events" for the ``m.`` event specification.
Syncing rooms
-------------
When a client logs in, they may have a list of rooms which they have already joined. These rooms
may also have a list of events associated with them. The purpose of 'syncing' is to present the
current room and event information in a convenient, compact manner. The events returned are not
limited to room events; presence events will also be returned. There are two APIs provided:
- ``/initialSync`` : A global sync which will present room and event information for all rooms
the user has joined.
- ``/rooms/<room id>/initialSync`` : A sync scoped to a single room. Presents room and event
information for this room only.
- TODO: JSON response format for both types
- TODO: when would you use global? when would you use scoped?
Getting events for a room
-------------------------
There are several APIs provided to ``GET`` events for a room:
``/rooms/<room id>/state/<event type>/<state key>``
Description:
Get the state event identified.
Response format:
A JSON object representing the state event **content**.
Example:
``/rooms/!room:domain.com/state/m.room.name`` returns ``{ "name": "Room name" }``
``/rooms/<room id>/state``
Description:
Get all state events for a room.
Response format:
``[ { state event }, { state event }, ... ]``
Example:
TODO
``/rooms/<room id>/members``
Description:
Get all ``m.room.member`` state events.
Response format:
``{ "start": "token", "end": "token", "chunk": [ { m.room.member event }, ... ] }``
Example:
TODO
- ``/rooms/<room id>/messages`` : Get all ``m.room.message`` events.
- ``/rooms/<room id>/initialSync`` : Get all relevant events for a room.
Getting grouped state events
----------------------------
- ``/members`` and ``/messages`` and the events they return.
- ``/state`` and it returns ALL THE THINGS.
Room Events
===========
@ -363,23 +524,109 @@ Room Events
This specification outlines several standard event types, all of which are
prefixed with ``m.``
State messages
--------------
- m.room.name
- m.room.topic
- m.room.member
- m.room.config
- m.room.invite_join
``m.room.name``
Summary:
Set the human-readable name for the room.
Type:
State event
JSON format:
``{ "name" : "string" }``
Example:
``{ "name" : "My Room" }``
Description:
A room has an opaque room ID which is not human-friendly to read. A room alias is
human-friendly, but not all rooms have room aliases. The room name is a human-friendly
string designed to be displayed to the end-user. The room name is not *unique*, as
multiple rooms can have the same room name set. The room name can also be set when
creating a room using ``/createRoom`` with the ``name`` key.
What are they, when are they used, what do they contain, how should they be used.
Link back to explanatory sections (e.g. invite/join/leave sections for m.room.member)
``m.room.topic``
Summary:
Set a topic for the room.
Type:
State event
JSON format:
``{ "topic" : "string" }``
Example:
``{ "topic" : "Welcome to the real world." }``
Description:
A topic is a short message detailing what is currently being discussed in the room.
It can also be used as a way to display extra information about the room, which may
not be suitable for the room name.
Non-state messages
------------------
- m.room.message
- m.room.message.feedback (and compressed format)
``m.room.member``
Summary:
The current membership state of a user in the room.
Type:
State event
JSON format:
``{ "membership" : "enum[ invite|join|leave|ban ]" }``
Example:
``{ "membership" : "join" }``
Description:
Adjusts the membership state for a user in a room. It is preferable to use the
membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions
rather than adjusting the state directly as there are a restricted set of valid
transformations. For example, user A cannot force user B to join a room, and trying
to force this state change directly will fail. See the "Rooms" section for how to
use the membership APIs.
What are they, when are they used, what do they contain, how should they be used
``m.room.config``
Summary:
The room config.
Type:
State event
JSON format:
TODO
Example:
TODO
Description:
TODO
``m.room.invite_join``
Summary:
TODO.
Type:
State event
JSON format:
TODO
Example:
TODO
Description:
TODO
``m.room.message``
Summary:
A message.
Type:
Non-state event
JSON format:
``{ "msgtype": "string" }``
Example:
``{ "msgtype": "m.text", "body": "Testing" }``
Description:
This event is used when sending messages in a room. Messages are not limited to be text.
The ``msgtype`` key outlines the type of message, e.g. text, audio, image, video, etc.
Whilst not required, the ``body`` key SHOULD be used with every kind of ``msgtype`` as
a fallback mechanism when a client cannot render the message. For more information on
the types of messages which can be sent, see "m.room.message msgtypes".
``m.room.message.feedback``
Summary:
A receipt for a message.
Type:
Non-state event
JSON format:
``{ "type": "enum [ delivered|read ]", "target_event_id": "string" }``
Example:
``{ "type": "delivered", "target_event_id": "e3b2icys" }``
Description:
Feedback events are events sent to acknowledge a message in some way. There are two
supported acknowledgements: ``delivered`` (sent when the event has been received) and
``read`` (sent when the event has been observed by the end-user). The ``target_event_id``
should reference the ``m.room.message`` event being acknowledged.
- voip?
m.room.message msgtypes
-----------------------
@ -490,7 +737,7 @@ Each user has the concept of presence information. This encodes the
"availability" of that user, suitable for display on other user's clients. This
is transmitted as an ``m.presence`` event and is one of the few events which
are sent *outside the context of a room*. The basic piece of presence information
is represented by the ``state`` key, which is an enum of one of the following:
is represented by the ``presence`` key, which is an enum of one of the following:
- ``online`` : The default state when the user is connected to an event stream.
- ``unavailable`` : The user is not reachable at this time.
@ -500,18 +747,26 @@ is represented by the ``state`` key, which is an enum of one of the following:
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
state anyway and generally interact with client features.
This basic ``state`` field applies to the user as a whole, regardless of how many
This basic ``presence`` field applies to the user as a whole, regardless of how many
client devices they have connected. The home server should synchronise this
status choice among multiple devices to ensure the user gets a consistent
experience.
In addition, the server maintains a timestamp of the last time it saw an active
action from the user; either sending a message to a room, or changing presence
state from a lower to a higher level of availability (thus: changing state from
``unavailable`` to ``online`` will count as an action for being active, whereas
in the other direction will not). This timestamp is presented via a key called
``last_active_ago``, which gives the relative number of miliseconds since the
message is generated/emitted, that the user was last seen active.
Idle Time
---------
As well as the basic ``state`` field, the presence information can also show a sense
of an "idle timer". This should be maintained individually by the user's
clients, and the home server can take the highest reported time as that to
report. When a user is offline, the home server can still report when the user was last
seen online.
As well as the basic ``presence`` field, the presence information can also show
a sense of an "idle timer". This should be maintained individually by the
user's clients, and the home server can take the highest reported time as that
to report. When a user is offline, the home server can still report when the
user was last seen online.
Transmission
------------

View file

@ -76,6 +76,10 @@ class MessageHandler(BaseRoomHandler):
Raises:
SynapseError if something went wrong.
"""
# TODO(paul): Why does 'event' not have a 'user' object?
user = self.hs.parse_userid(event.user_id)
assert(user.is_mine)
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
@ -86,6 +90,10 @@ class MessageHandler(BaseRoomHandler):
yield self._on_new_room_event(event, snapshot)
self.hs.get_handlers().presence_handler.bump_presence_active_time(
user
)
@defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False):

View file

@ -52,6 +52,13 @@ def partitionbool(l, func):
class PresenceHandler(BaseHandler):
STATE_LEVELS = {
PresenceState.OFFLINE: 0,
PresenceState.UNAVAILABLE: 1,
PresenceState.ONLINE: 2,
PresenceState.FREE_FOR_CHAT: 3,
}
def __init__(self, hs):
super(PresenceHandler, self).__init__(hs)
@ -135,7 +142,7 @@ class PresenceHandler(BaseHandler):
return self._user_cachemap[user]
else:
statuscache = UserPresenceCache()
statuscache.update({"state": PresenceState.OFFLINE}, user)
statuscache.update({"presence": PresenceState.OFFLINE}, user)
return statuscache
def registered_user(self, user):
@ -173,19 +180,24 @@ class PresenceHandler(BaseHandler):
observed_user=target_user
)
if visible:
state = yield self.store.get_presence_state(
target_user.localpart
)
else:
if not visible:
raise SynapseError(404, "Presence information not visible")
state = yield self.store.get_presence_state(target_user.localpart)
if "mtime" in state:
del state["mtime"]
state["presence"] = state["state"]
if target_user in self._user_cachemap:
state["last_active"] = (
self._user_cachemap[target_user].get_state()["last_active"]
)
else:
# TODO(paul): Have remote server send us permissions set
state = self._get_or_offline_usercache(target_user).get_state()
if "mtime" in state and (state["mtime"] is not None):
state["mtime_age"] = int(
self.clock.time_msec() - state.pop("mtime")
if "last_active" in state:
state["last_active_ago"] = int(
self.clock.time_msec() - state.pop("last_active")
)
defer.returnValue(state)
@ -202,20 +214,33 @@ class PresenceHandler(BaseHandler):
if target_user != auth_user:
raise AuthError(400, "Cannot set another user's displayname")
# TODO(paul): Sanity-check 'state'
if "status_msg" not in state:
state["status_msg"] = None
for k in state.keys():
if k not in ("state", "status_msg"):
if k not in ("presence", "state", "status_msg"):
raise SynapseError(
400, "Unexpected presence state key '%s'" % (k,)
)
# Handle legacy "state" key for now
if "state" in state:
state["presence"] = state.pop("state")
if state["presence"] not in self.STATE_LEVELS:
raise SynapseError(400, "'%s' is not a valid presence state" %
state["presence"]
)
logger.debug("Updating presence state of %s to %s",
target_user.localpart, state["state"])
target_user.localpart, state["presence"])
state_to_store = dict(state)
state_to_store["state"] = state_to_store.pop("presence")
statuscache=self._get_or_offline_usercache(target_user)
was_level = self.STATE_LEVELS[statuscache.get_state()["presence"]]
now_level = self.STATE_LEVELS[state["presence"]]
yield defer.DeferredList([
self.store.set_presence_state(
@ -226,9 +251,10 @@ class PresenceHandler(BaseHandler):
),
])
state["mtime"] = self.clock.time_msec()
if now_level > was_level:
state["last_active"] = self.clock.time_msec()
now_online = state["state"] != PresenceState.OFFLINE
now_online = state["presence"] != PresenceState.OFFLINE
was_polling = target_user in self._user_cachemap
if now_online and not was_polling:
@ -240,6 +266,12 @@ class PresenceHandler(BaseHandler):
# we don't have to do this all the time
self.changed_presencelike_data(target_user, state)
def bump_presence_active_time(self, user, now=None):
if now is None:
now = self.clock.time_msec()
self.changed_presencelike_data(user, {"last_active": now})
def changed_presencelike_data(self, user, state):
statuscache = self._get_or_make_usercache(user)
@ -251,12 +283,12 @@ class PresenceHandler(BaseHandler):
@log_function
def started_user_eventstream(self, user):
# TODO(paul): Use "last online" state
self.set_state(user, user, {"state": PresenceState.ONLINE})
self.set_state(user, user, {"presence": PresenceState.ONLINE})
@log_function
def stopped_user_eventstream(self, user):
# TODO(paul): Save current state as "last online" state
self.set_state(user, user, {"state": PresenceState.OFFLINE})
self.set_state(user, user, {"presence": PresenceState.OFFLINE})
@defer.inlineCallbacks
def user_joined_room(self, user, room_id):
@ -385,9 +417,9 @@ class PresenceHandler(BaseHandler):
observed_user = self.hs.parse_userid(p.pop("observed_user_id"))
p["observed_user"] = observed_user
p.update(self._get_or_offline_usercache(observed_user).get_state())
if "mtime" in p:
p["mtime_age"] = int(
self.clock.time_msec() - p.pop("mtime")
if "last_active" in p:
p["last_active_ago"] = int(
self.clock.time_msec() - p.pop("last_active")
)
defer.returnValue(presence)
@ -576,21 +608,30 @@ class PresenceHandler(BaseHandler):
def _push_presence_remote(self, user, destination, state=None):
if state is None:
state = yield self.store.get_presence_state(user.localpart)
del state["mtime"]
state["presence"] = state["state"]
if user in self._user_cachemap:
state["last_active"] = (
self._user_cachemap[user].get_state()["last_active"]
)
yield self.distributor.fire(
"collect_presencelike_data", user, state
)
if "mtime" in state:
if "last_active" in state:
state = dict(state)
state["mtime_age"] = int(
self.clock.time_msec() - state.pop("mtime")
state["last_active_ago"] = int(
self.clock.time_msec() - state.pop("last_active")
)
user_state = {
"user_id": user.to_string(),
}
user_state.update(**state)
if "state" in user_state and "presence" not in user_state:
user_state["presence"] = user_state["state"]
yield self.federation.send_edu(
destination=destination,
@ -622,9 +663,14 @@ class PresenceHandler(BaseHandler):
state = dict(push)
del state["user_id"]
if "mtime_age" in state:
state["mtime"] = int(
self.clock.time_msec() - state.pop("mtime_age")
# Legacy handling
if "presence" not in state:
state["presence"] = state["state"]
del state["state"]
if "last_active_ago" in state:
state["last_active"] = int(
self.clock.time_msec() - state.pop("last_active_ago")
)
statuscache = self._get_or_make_usercache(user)
@ -639,7 +685,7 @@ class PresenceHandler(BaseHandler):
statuscache=statuscache,
)
if state["state"] == PresenceState.OFFLINE:
if state["presence"] == PresenceState.OFFLINE:
del self._user_cachemap[user]
for poll in content.get("poll", []):
@ -672,10 +718,9 @@ class PresenceHandler(BaseHandler):
yield defer.DeferredList(deferreds)
@defer.inlineCallbacks
def push_update_to_local_and_remote(self, observed_user,
def push_update_to_local_and_remote(self, observed_user, statuscache,
users_to_push=[], room_ids=[],
remote_domains=[],
statuscache=None):
remote_domains=[]):
localusers, remoteusers = partitionbool(
users_to_push,
@ -804,6 +849,7 @@ class UserPresenceCache(object):
def update(self, state, serial):
assert("mtime_age" not in state)
assert("state" not in state)
self.state.update(state)
# Delete keys that are now 'None'
@ -820,15 +866,21 @@ class UserPresenceCache(object):
def get_state(self):
# clone it so caller can't break our cache
return dict(self.state)
state = dict(self.state)
# Legacy handling
if "presence" in state:
state["state"] = state["presence"]
return state
def make_event(self, user, clock):
content = self.get_state()
content["user_id"] = user.to_string()
if "mtime" in content:
content["mtime_age"] = int(
clock.time_msec() - content.pop("mtime")
if "last_active" in content:
content["last_active_ago"] = int(
clock.time_msec() - content.pop("last_active")
)
return {"type": "m.presence", "content": content}

View file

@ -48,7 +48,11 @@ class PresenceStatusRestServlet(RestServlet):
try:
content = json.loads(request.content.read())
state["state"] = content.pop("state")
# Legacy handling
if "state" in content:
state["presence"] = content.pop("state")
else:
state["presence"] = content.pop("presence")
if "status_msg" in content:
state["status_msg"] = content.pop("status_msg")

View file

@ -35,8 +35,6 @@ ONLINE = PresenceState.ONLINE
logging.getLogger().addHandler(logging.NullHandler())
#logging.getLogger().addHandler(logging.StreamHandler())
#logging.getLogger().setLevel(logging.DEBUG)
def _expect_edu(destination, edu_type, content, origin="test"):
@ -141,7 +139,8 @@ class PresenceStateTestCase(unittest.TestCase):
target_user=self.u_apple, auth_user=self.u_apple
)
self.assertEquals({"state": ONLINE, "status_msg": "Online"},
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
state
)
mocked_get.assert_called_with("apple")
@ -157,7 +156,8 @@ class PresenceStateTestCase(unittest.TestCase):
target_user=self.u_apple, auth_user=self.u_banana
)
self.assertEquals({"state": ONLINE, "status_msg": "Online"},
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
state
)
mocked_get.assert_called_with("apple")
@ -175,7 +175,10 @@ class PresenceStateTestCase(unittest.TestCase):
target_user=self.u_apple, auth_user=self.u_clementine
)
self.assertEquals({"state": ONLINE, "status_msg": "Online"}, state)
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
state
)
@defer.inlineCallbacks
def test_get_disallowed_state(self):
@ -202,20 +205,20 @@ class PresenceStateTestCase(unittest.TestCase):
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": UNAVAILABLE, "status_msg": "Away"})
state={"presence": UNAVAILABLE, "status_msg": "Away"})
mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"})
self.mock_start.assert_called_with(self.u_apple,
state={
"state": UNAVAILABLE,
"presence": UNAVAILABLE,
"status_msg": "Away",
"mtime": 1000000, # MockClock
"last_active": 1000000, # MockClock
})
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": OFFLINE})
state={"presence": OFFLINE})
self.mock_stop.assert_called_with(self.u_apple)
@ -449,28 +452,35 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_get_presence_list(self):
self.datastore.get_presence_list.return_value = defer.succeed(
[{"observed_user_id": "@banana:test"}]
[{"observed_user_id": "@banana:test"}]
)
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple)
self.assertEquals([{"observed_user": self.u_banana,
"state": OFFLINE}], presence)
self.assertEquals([
{"observed_user": self.u_banana,
"presence": OFFLINE,
"state": OFFLINE},
], presence)
self.datastore.get_presence_list.assert_called_with("apple",
accepted=None)
accepted=None
)
self.datastore.get_presence_list.return_value = defer.succeed(
[{"observed_user_id": "@banana:test"}]
[{"observed_user_id": "@banana:test"}]
)
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple, accepted=True)
observer_user=self.u_apple, accepted=True
)
self.assertEquals([{"observed_user": self.u_banana,
"state": OFFLINE}], presence)
self.assertEquals([
{"observed_user": self.u_banana,
"presence": OFFLINE,
"state": OFFLINE},
], presence)
self.datastore.get_presence_list.assert_called_with("apple",
accepted=True)
@ -611,6 +621,9 @@ class PresencePushTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
self.handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handler._local_pushmap.setdefault("apple", set())
apple_set.add(self.u_banana)
apple_set.add(self.u_clementine)
@ -618,7 +631,8 @@ class PresencePushTestCase(unittest.TestCase):
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.handler.set_state(self.u_apple, self.u_apple,
{"state": ONLINE})
{"presence": ONLINE}
)
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(
@ -627,8 +641,9 @@ class PresencePushTestCase(unittest.TestCase):
{"type": "m.presence",
"content": {
"user_id": "@apple:test",
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 0,
"last_active_ago": 0,
}},
],
)
@ -636,13 +651,21 @@ class PresencePushTestCase(unittest.TestCase):
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple, accepted=True)
self.assertEquals([
{"observed_user": self.u_banana, "state": OFFLINE},
{"observed_user": self.u_clementine, "state": OFFLINE}],
presence)
self.assertEquals(
[
{"observed_user": self.u_banana,
"presence": OFFLINE,
"state": OFFLINE},
{"observed_user": self.u_clementine,
"presence": OFFLINE,
"state": OFFLINE},
],
presence
)
yield self.handler.set_state(self.u_banana, self.u_banana,
{"state": ONLINE})
{"presence": ONLINE}
)
self.clock.advance_time(2)
@ -651,9 +674,11 @@ class PresencePushTestCase(unittest.TestCase):
self.assertEquals([
{"observed_user": self.u_banana,
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 2000},
"last_active_ago": 2000},
{"observed_user": self.u_clementine,
"presence": OFFLINE,
"state": OFFLINE},
], presence)
@ -666,8 +691,9 @@ class PresencePushTestCase(unittest.TestCase):
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 2000
"last_active_ago": 2000
}},
]
)
@ -682,8 +708,9 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"presence": u"online",
"state": u"online",
"mtime_age": 0},
"last_active_ago": 0},
],
}
)
@ -699,11 +726,14 @@ class PresencePushTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
self.handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handler._remote_sendmap.setdefault("apple", set())
apple_set.add(self.u_potato.domain)
yield self.handler.set_state(self.u_apple, self.u_apple,
{"state": ONLINE}
{"presence": ONLINE}
)
yield put_json.await_calls()
@ -726,7 +756,7 @@ class PresencePushTestCase(unittest.TestCase):
"push": [
{"user_id": "@potato:remote",
"state": "online",
"mtime_age": 1000},
"last_active_ago": 1000},
],
}
)
@ -741,8 +771,9 @@ class PresencePushTestCase(unittest.TestCase):
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 1000,
"last_active_ago": 1000,
}}
]
)
@ -751,7 +782,10 @@ class PresencePushTestCase(unittest.TestCase):
state = yield self.handler.get_state(self.u_potato, self.u_apple)
self.assertEquals({"state": ONLINE, "mtime_age": 3000}, state)
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "last_active_ago": 3000},
state
)
@defer.inlineCallbacks
def test_join_room_local(self):
@ -763,8 +797,8 @@ class PresencePushTestCase(unittest.TestCase):
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
self.handler._user_cachemap[self.u_clementine].update(
{
"state": PresenceState.ONLINE,
"mtime": self.clock.time_msec(),
"presence": PresenceState.ONLINE,
"last_active": self.clock.time_msec(),
}, self.u_clementine
)
@ -781,8 +815,9 @@ class PresencePushTestCase(unittest.TestCase):
{"type": "m.presence",
"content": {
"user_id": "@clementine:test",
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 0,
"last_active_ago": 0,
}}
]
)
@ -798,7 +833,8 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"state": "online"},
"presence": "online",
"state": "online"},
],
}
),
@ -812,7 +848,8 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@banana:test",
"state": "offline"},
"presence": "offline",
"state": "offline"},
],
}
),
@ -823,7 +860,7 @@ class PresencePushTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
self.handler._user_cachemap[self.u_apple].update(
{"state": PresenceState.ONLINE}, self.u_apple)
{"presence": PresenceState.ONLINE}, self.u_apple)
self.room_members = [self.u_apple, self.u_banana]
yield self.distributor.fire("user_joined_room", self.u_potato,
@ -841,7 +878,8 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@clementine:test",
"state": "online"},
"presence": "online",
"state": "online"},
],
}
),
@ -851,7 +889,7 @@ class PresencePushTestCase(unittest.TestCase):
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
self.handler._user_cachemap[self.u_clementine].update(
{"state": ONLINE}, self.u_clementine)
{"presence": ONLINE}, self.u_clementine)
self.room_members.append(self.u_potato)
yield self.distributor.fire("user_joined_room", self.u_clementine,
@ -935,7 +973,8 @@ class PresencePollingTestCase(unittest.TestCase):
def get_presence_state(user_localpart):
return defer.succeed(
{"state": self.current_user_state[user_localpart],
"status_msg": None}
"status_msg": None,
"mtime": 123456000}
)
self.datastore.get_presence_state = get_presence_state
@ -969,7 +1008,7 @@ class PresencePollingTestCase(unittest.TestCase):
# apple goes online
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": ONLINE}
state={"presence": ONLINE}
)
# apple should see both banana and clementine currently offline
@ -992,8 +1031,9 @@ class PresencePollingTestCase(unittest.TestCase):
# banana goes online
yield self.handler.set_state(
target_user=self.u_banana, auth_user=self.u_banana,
state={"state": ONLINE})
target_user=self.u_banana, auth_user=self.u_banana,
state={"presence": ONLINE}
)
# apple and banana should now both see each other online
self.mock_update_client.assert_has_calls([
@ -1013,8 +1053,9 @@ class PresencePollingTestCase(unittest.TestCase):
# apple goes offline
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": OFFLINE})
target_user=self.u_apple, auth_user=self.u_apple,
state={"presence": OFFLINE}
)
# banana should now be told apple is offline
self.mock_update_client.assert_has_calls([
@ -1027,7 +1068,6 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertFalse("banana" in self.handler._local_pushmap)
self.assertFalse("clementine" in self.handler._local_pushmap)
@defer.inlineCallbacks
def test_remote_poll_send(self):
put_json = self.mock_http_client.put_json
@ -1057,8 +1097,9 @@ class PresencePollingTestCase(unittest.TestCase):
# clementine goes online
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": ONLINE})
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"presence": ONLINE}
)
yield put_json.await_calls()
@ -1085,7 +1126,7 @@ class PresencePollingTestCase(unittest.TestCase):
# fig goes online; shouldn't send a second poll
yield self.handler.set_state(
target_user=self.u_fig, auth_user=self.u_fig,
state={"state": ONLINE}
state={"presence": ONLINE}
)
# reactor.iterate(delay=0)
@ -1095,7 +1136,7 @@ class PresencePollingTestCase(unittest.TestCase):
# fig goes offline
yield self.handler.set_state(
target_user=self.u_fig, auth_user=self.u_fig,
state={"state": OFFLINE}
state={"presence": OFFLINE}
)
reactor.iterate(delay=0)
@ -1116,8 +1157,9 @@ class PresencePollingTestCase(unittest.TestCase):
# clementine goes offline
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": OFFLINE})
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"presence": OFFLINE}
)
yield put_json.await_calls()
@ -1135,6 +1177,7 @@ class PresencePollingTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@banana:test",
"presence": "offline",
"state": "offline",
"status_msg": None},
],

View file

@ -166,7 +166,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
from synapse.handlers.presence import UserPresenceCache
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
UserPresenceCache())
UserPresenceCache()
)
self.handlers.presence_handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handlers.presence_handler._local_pushmap.setdefault(
"apple", set())
apple_set.add(self.u_banana)
@ -182,11 +186,13 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
self.assertEquals([
{"observed_user": self.u_banana,
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 0,
"last_active_ago": 0,
"displayname": "Frank",
"avatar_url": "http://foo"},
{"observed_user": self.u_clementine,
"presence": OFFLINE,
"state": OFFLINE}],
presence)
@ -199,8 +205,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
statuscache = self.mock_update_client.call_args[1]["statuscache"]
self.assertEquals({
"state": ONLINE,
"mtime": 1000000, # MockClock
"presence": ONLINE,
"last_active": 1000000, # MockClock
"displayname": "Frank",
"avatar_url": "http://foo",
}, statuscache.state)
@ -222,8 +228,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
statuscache = self.mock_update_client.call_args[1]["statuscache"]
self.assertEquals({
"state": ONLINE,
"mtime": 1000000, # MockClock
"presence": ONLINE,
"last_active": 1000000, # MockClock
"displayname": "I am an Apple",
"avatar_url": "http://foo",
}, statuscache.state)
@ -241,7 +247,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
from synapse.handlers.presence import UserPresenceCache
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
UserPresenceCache())
UserPresenceCache()
)
self.handlers.presence_handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handlers.presence_handler._remote_sendmap.setdefault(
"apple", set())
apple_set.add(self.u_potato.domain)
@ -255,8 +265,9 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"presence": "online",
"state": "online",
"mtime_age": 0,
"last_active_ago": 0,
"displayname": "Frank",
"avatar_url": "http://foo"},
],
@ -293,14 +304,16 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
statuscache=ANY)
statuscache = self.mock_update_client.call_args[1]["statuscache"]
self.assertEquals({"state": ONLINE,
self.assertEquals({"presence": ONLINE,
"displayname": "Frank",
"avatar_url": "http://foo"}, statuscache.state)
state = yield self.handlers.presence_handler.get_state(self.u_potato,
self.u_apple)
self.assertEquals({"state": ONLINE,
"displayname": "Frank",
"avatar_url": "http://foo"},
state)
self.assertEquals(
{"presence": ONLINE,
"state": ONLINE,
"displayname": "Frank",
"avatar_url": "http://foo"},
state)

View file

@ -98,8 +98,10 @@ class PresenceStateTestCase(unittest.TestCase):
"/presence/%s/status" % (myid), None)
self.assertEquals(200, code)
self.assertEquals({"state": ONLINE, "status_msg": "Available"},
response)
self.assertEquals(
{"presence": ONLINE, "state": ONLINE, "status_msg": "Available"},
response
)
mocked_get.assert_called_with("apple")
@defer.inlineCallbacks
@ -109,7 +111,7 @@ class PresenceStateTestCase(unittest.TestCase):
(code, response) = yield self.mock_resource.trigger("PUT",
"/presence/%s/status" % (myid),
'{"state": "unavailable", "status_msg": "Away"}')
'{"presence": "unavailable", "status_msg": "Away"}')
self.assertEquals(200, code)
mocked_set.assert_called_with("apple",
@ -173,9 +175,9 @@ class PresenceListTestCase(unittest.TestCase):
"/presence/list/%s" % (myid), None)
self.assertEquals(200, code)
self.assertEquals(
[{"user_id": "@banana:test", "state": OFFLINE}], response
)
self.assertEquals([
{"user_id": "@banana:test", "presence": OFFLINE, "state": OFFLINE},
], response)
self.datastore.get_presence_list.assert_called_with(
"apple", accepted=True
@ -314,7 +316,8 @@ class PresenceEventStreamTestCase(unittest.TestCase):
[])
yield self.presence.set_state(self.u_banana, self.u_banana,
state={"state": ONLINE})
state={"presence": ONLINE}
)
(code, response) = yield self.mock_resource.trigger("GET",
"/events?from=0_1_0&timeout=0", None)
@ -324,8 +327,9 @@ class PresenceEventStreamTestCase(unittest.TestCase):
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"presence": ONLINE,
"state": ONLINE,
"displayname": "Frank",
"mtime_age": 0,
"last_active_ago": 0,
}},
]}, response)

View file

@ -51,7 +51,7 @@ class RoomPermissionsTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@ -398,7 +398,7 @@ class RoomsMemberListTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@ -476,7 +476,7 @@ class RoomsCreateTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@ -566,7 +566,7 @@ class RoomTopicTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@ -669,7 +669,7 @@ class RoomMemberStateTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@ -794,7 +794,7 @@ class RoomMessagesTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),

View file

@ -188,8 +188,9 @@ class MemoryDataStore(object):
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
return [
r for r in self.members
if self.members[r].get(user_id).membership in membership_list
self.members[r].get(user_id) for r in self.members
if user_id in self.members[r] and
self.members[r][user_id].membership in membership_list
]
def get_room_events_stream(self, user_id=None, from_key=None, to_key=None,

View file

@ -21,8 +21,8 @@ limitations under the License.
'use strict';
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
// Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path();
@ -36,8 +36,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
eventStreamService.resume();
mPresence.start();
}
$scope.user_id = matrixService.config().user_id;
$scope.user_id;
var config = matrixService.config();
if (config) {
$scope.user_id = matrixService.config().user_id;
}
/**
* Open a given page.
@ -84,7 +88,27 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$scope.updateHeader = function() {
$scope.user_id = matrixService.config().user_id;
};
}]);
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
console.trace("incoming call");
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
$rootScope.currentCall = call;
});
$scope.answerCall = function() {
$scope.currentCall.answer();
};
$scope.hangupCall = function() {
$scope.currentCall.hangup();
$scope.currentCall = undefined;
};
$rootScope.onCallError = function(errStr) {
$scope.feedback = errStr;
}
$rootScope.onCallHangup = function() {
}
}]);

View file

@ -70,7 +70,7 @@ angular.module('matrixWebClient')
});
filtered.sort(function (a, b) {
return ((a["mtime_age"] || 10e10) > (b["mtime_age"] || 10e10) ? 1 : -1);
return ((a["last_active_ago"] || 10e10) > (b["last_active_ago"] || 10e10) ? 1 : -1);
});
return filtered;
};
@ -79,4 +79,43 @@ angular.module('matrixWebClient')
return function(text) {
return $sce.trustAsHtml(text);
};
}])
// Compute the room name according to information we have
.filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
return function(room_id) {
var roomName;
// If there is an alias, use it
// TODO: only one alias is managed for now
var alias = matrixService.getRoomIdToAliasMapping(room_id);
if (alias) {
roomName = alias;
}
if (undefined === roomName) {
// Else, build the name from its users
var room = $rootScope.events.rooms[room_id];
if (room) {
if (room.members) {
// Limit the room renaming to 1:1 room
if (2 === Object.keys(room.members).length) {
for (var i in room.members) {
var member = room.members[i];
if (member.user_id !== matrixService.config().user_id) {
roomName = member.content.displayname ? member.content.displayname : member.user_id;
}
}
}
}
}
}
if (undefined === roomName) {
// By default, use the room ID
roomName = room_id;
}
return roomName;
};
}]);

View file

@ -43,6 +43,10 @@ a:active { color: #000; }
height: 32px;
}
#callBar {
float: left;
}
#headerContent {
color: #ccc;
max-width: 1280px;

View file

@ -44,6 +44,19 @@
<div id="header">
<!-- Do not show buttons on the login page -->
<div id="headerContent" ng-hide="'/login' == location || '/register' == location">
<div id="callBar">
<div ng-show="currentCall.state == 'ringing'">
Incoming call from {{ currentCall.user_id }}
<button ng-click="answerCall()">Answer</button>
<button ng-click="hangupCall()">Reject</button>
</div>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
<span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
<span ng-show="currentCall.state == 'ended'">Call Ended</span>
<span style="display: none; ">{{ currentCall.state }}</span>
</div>
<a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
&nbsp;
<button ng-click='goToPage("/")'>Home</button>
@ -56,7 +69,7 @@
<div id="footer" ng-hide="location.indexOf('/room') == 0">
<div id="footerContent">
&copy 2014 Matrix.org
&copy; 2014 Matrix.org
</div>
</div>
</body>

View file

@ -33,8 +33,7 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
console.log("Invited to room " + event.room_id);
// FIXME push membership to top level key to match /im/sync
event.membership = event.content.membership;
// FIXME bodge a nicer name than the room ID for this invite.
event.room_display_name = event.user_id + "'s room";
$scope.rooms[event.room_id] = event;
}
});
@ -43,6 +42,11 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
$scope.rooms[event.room_id].lastMsg = event;
}
});
$scope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
if (isLive) {
$scope.rooms[event.room_id].lastMsg = event;
}
});
};
@ -83,7 +87,9 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
};
$scope.onInit = function() {
refresh();
eventHandlerService.waitForInitialSyncCompletion().then(function() {
refresh();
});
};
}]);

View file

@ -6,7 +6,7 @@
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
<tr>
<td class="recentsRoomName">
{{ room.room_display_name }}
{{ room.room_id | roomName }}
</td>
<td class="recentsRoomSummaryTS">
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
@ -51,7 +51,9 @@
</div>
<div ng-switch-default>
{{ room.lastMsg }}
<div ng-if="room.lastMsg.type.indexOf('m.call.') == 0">
Call
</div>
</div>
</div>
</td>

View file

@ -82,13 +82,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
updatePresence(event);
});
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
console.trace("incoming call");
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
$scope.currentCall = call;
});
$scope.memberCount = function() {
return Object.keys($scope.members).length;
};
@ -100,15 +93,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
}
};
$scope.answerCall = function() {
$scope.currentCall.answer();
};
$scope.hangupCall = function() {
$scope.currentCall.hangup();
$scope.currentCall = undefined;
};
var paginate = function(numItems) {
// console.log("paginate " + numItems);
if ($scope.state.paginating || !$scope.room_id) {
@ -181,11 +165,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
var isNewMember = !(target_user_id in $scope.members);
if (isNewMember) {
// FIXME: why are we copying these fields around inside chunk?
if ("state" in chunk.content) {
chunk.presenceState = chunk.content.state; // why is this renamed?
if ("presence" in chunk.content) {
chunk.presence = chunk.content.presence;
}
if ("mtime_age" in chunk.content) {
chunk.mtime_age = chunk.content.mtime_age;
if ("last_active_ago" in chunk.content) {
chunk.last_active_ago = chunk.content.last_active_ago;
}
if ("displayname" in chunk.content) {
chunk.displayname = chunk.content.displayname;
@ -201,9 +185,15 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
}
}
else {
// selectively update membership else it will nuke the picture and displayname too :/
// selectively update membership and presence else it will nuke the picture and displayname too :/
var member = $scope.members[target_user_id];
member.content.membership = chunk.content.membership;
member.membership = chunk.content.membership;
if ("presence" in chunk.content) {
member.presence = chunk.content.presence;
}
if ("last_active_ago" in chunk.content) {
member.last_active_ago = chunk.content.last_active_ago;
}
}
};
@ -221,13 +211,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
var member = $scope.members[chunk.content.user_id];
// XXX: why not just pass the chunk straight through?
if ("state" in chunk.content) {
member.presenceState = chunk.content.state;
if ("presence" in chunk.content) {
member.presence = chunk.content.presence;
}
if ("mtime_age" in chunk.content) {
// FIXME: should probably keep updating mtime_age in realtime like FB does
member.mtime_age = chunk.content.mtime_age;
if ("last_active_ago" in chunk.content) {
member.last_active_ago = chunk.content.last_active_ago;
}
// this may also contain a new display name or avatar url, so check.
@ -331,6 +320,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
// Make sure the initialSync has been before going further
eventHandlerService.waitForInitialSyncCompletion().then(
function() {
// Some data has been retrieved from the iniialSync request
// So, the relative time starts here
$scope.now = new Date().getTime();
var needsToJoin = true;
// The room members is available in the data fetched by initialSync
@ -377,10 +371,22 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
// Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id;
// Get the up-to-date the current member list
matrixService.getMemberList($scope.room_id).then(
function(response) {
for (var i = 0; i < response.data.chunk.length; i++) {
var chunk = response.data.chunk[i];
updateMemberList(chunk);
updateMemberListPresenceAge();
}
},
function(error) {
$scope.feedback = "Failed get member list: " + error.data.error;
}
);
paginate(MESSAGES_PER_PAGINATION);
updateMemberListPresenceAge();
};
$scope.inviteUser = function(user_id) {
@ -455,16 +461,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
$scope.startVoiceCall = function() {
var call = new MatrixCall($scope.room_id);
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
call.onError = $rootScope.onCallError;
call.onHangup = $rootScope.onCallHangup;
call.placeCall();
$scope.currentCall = call;
$rootScope.currentCall = call;
}
$scope.onCallError = function(errStr) {
$scope.feedback = errStr;
}
$scope.onCallHangup = function() {
}
}]);

View file

@ -3,7 +3,7 @@
<div id="roomHeader">
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
<div id="roomName">
{{ room_alias || room_id }}
{{ room_id | roomName }}
</div>
</div>
@ -26,8 +26,8 @@
<img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
<div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
</td>
<td class="userPresence" ng-class="(member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
<span ng-show="member.mtime_age">{{ member.mtime_age + (now - member.last_updated) | duration }}<br/>ago</span>
<td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
<span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
</td>
</table>
</div>
@ -100,18 +100,7 @@
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
</span>
<button ng-click="leaveRoom()">Leave</button>
<button ng-click="startVoiceCall()" ng-show="currentCall == undefined && memberCount() == 2">Voice Call</button>
<div ng-show="currentCall.state == 'ringing'">
Incoming call from {{ currentCall.user_id }}
<button ng-click="answerCall()">Answer</button>
<button ng-click="hangupCall()">Reject</button>
</div>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button>
<span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
<span ng-show="currentCall.state == 'ended'">Call Ended</span>
<span style="display: none; ">{{ currentCall.state }}</span>
<button ng-click="startVoiceCall()" ng-show="(currentCall == undefined || currentCall.state == 'ended') && memberCount() == 2">Voice Call</button>
</div>
{{ feedback }}