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 Presence
======== ========
In the following messages, the presence state is an integer enumeration of the In the following messages, the presence state is a presence string as described in
following states: the main specification document.
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.
Getting/Setting your own presence state Getting/Setting your own presence state
--------------------------------------- ---------------------------------------
REST Path: /presence/$user_id/status REST Path: /presence/$user_id/status
Valid methods: GET/PUT Valid methods: GET/PUT
Required keys: Required keys:
state : [0|1|2|3] - The user's new presence state presence : <string> - The user's new presence state
Optional keys: Optional keys:
status_msg : text string provided by the user to explain their status status_msg : text string provided by the user to explain their status
@ -1039,7 +1028,7 @@ Fetching your presence list
following keys: following keys:
{ {
"user_id" : string giving the observed user's ID "user_id" : string giving the observed user's ID
"state" : int giving their status "presence" : int giving their status
"status_msg" : optional text string "status_msg" : optional text string
"displayname" : optional text string from the user's profile "displayname" : optional text string from the user's profile
"avatar_url" : 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", "swaggerVersion": "1.2",
"apis": [ "apis": [
{ {
"path": "/login", "path": "-login",
"description": "Login operations" "description": "Login operations"
}, },
{ {
"path": "/registration", "path": "-registration",
"description": "Registration operations" "description": "Registration operations"
}, },
{ {
"path": "/rooms", "path": "-rooms",
"description": "Room operations" "description": "Room operations"
}, },
{ {
"path": "/profile", "path": "-profile",
"description": "Profile operations" "description": "Profile operations"
}, },
{ {
"path": "/presence", "path": "-presence",
"description": "Presence operations" "description": "Presence operations"
}, },
{ {
"path": "/events", "path": "-events",
"description": "Event operations" "description": "Event operations"
}, },
{ {
"path": "/directory", "path": "-directory",
"description": "Directory operations" "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``: :``M_NOT_FOUND``:
No resource was found for this request. 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: Some requests have unique error codes:
:``M_USER_IN_USE``: :``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: 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 Modifying aliases
----------------- -----------------
@ -284,12 +289,37 @@ Permissions
Joining rooms Joining rooms
------------- -------------
- What is joining? What permissions / access does it give you? How does this affect /initialSync? - TODO: What does the home server have to do to join a user to a room?
- 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?
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 Inviting users
-------------- --------------
@ -331,31 +361,162 @@ See the "Room events" section for more information on ``m.room.member``.
Leaving rooms Leaving rooms
------------- -------------
- API to hit (``$roomid/leave``). See "Room events" for more info. A user can leave a room to stop receiving events for that room. A user must have
- Must be joined to leave. How does this affect /initialSync? joined the room before they are eligible to leave the room. If the room is an
- Not ever being in a room is NOT equivalent to have left it (due to membership: leave). "invite-only" room, they will need to be re-invited before they can re-join the room.
- Need to be re-invited if invite-only room. To leave a room, a request should be made to ``/rooms/<room id>/leave`` with::
- If no more HSes in room, can delete room?
- Is there a dance? {}
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 Events in a room
---------------- ----------------
- Split into state and non-state data Room events can be split into two categories:
- 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.
Syncing a room :State Events:
-------------- These are events which replace events that came before it, depending on a set of unique keys.
- Single room initial sync. API to hit. Why it might be used (lazy loading) 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 Room Events
=========== ===========
@ -363,23 +524,109 @@ Room Events
This specification outlines several standard event types, all of which are This specification outlines several standard event types, all of which are
prefixed with ``m.`` prefixed with ``m.``
State messages ``m.room.name``
-------------- Summary:
- m.room.name Set the human-readable name for the room.
- m.room.topic Type:
- m.room.member State event
- m.room.config JSON format:
- m.room.invite_join ``{ "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. ``m.room.topic``
Link back to explanatory sections (e.g. invite/join/leave sections for m.room.member) 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.member``
------------------ Summary:
- m.room.message The current membership state of a user in the room.
- m.room.message.feedback (and compressed format) 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 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 "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 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 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. - ``online`` : The default state when the user is connected to an event stream.
- ``unavailable`` : The user is not reachable at this time. - ``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 - ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
state anyway and generally interact with client features. 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 client devices they have connected. The home server should synchronise this
status choice among multiple devices to ensure the user gets a consistent status choice among multiple devices to ensure the user gets a consistent
experience. 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 Idle Time
--------- ---------
As well as the basic ``state`` field, the presence information can also show a sense As well as the basic ``presence`` field, the presence information can also show
of an "idle timer". This should be maintained individually by the user's a sense of an "idle timer". This should be maintained individually by the
clients, and the home server can take the highest reported time as that to user's clients, and the home server can take the highest reported time as that
report. When a user is offline, the home server can still report when the user was last to report. When a user is offline, the home server can still report when the
seen online. user was last seen online.
Transmission Transmission
------------ ------------

View File

@ -76,6 +76,10 @@ class MessageHandler(BaseRoomHandler):
Raises: Raises:
SynapseError if something went wrong. 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: if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec()) event.content["hsob_ts"] = int(self.clock.time_msec())
@ -86,6 +90,10 @@ class MessageHandler(BaseRoomHandler):
yield self._on_new_room_event(event, snapshot) yield self._on_new_room_event(event, snapshot)
self.hs.get_handlers().presence_handler.bump_presence_active_time(
user
)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None, def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False): feedback=False):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -188,8 +188,9 @@ class MemoryDataStore(object):
def get_rooms_for_user_where_membership_is(self, user_id, membership_list): def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
return [ return [
r for r in self.members self.members[r].get(user_id) for r in self.members
if self.members[r].get(user_id).membership in membership_list 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, 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'; 'use strict';
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService']) angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', .controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) { function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
// Check current URL to avoid to display the logout button on the login page // Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path(); $scope.location = $location.path();
@ -36,8 +36,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
eventStreamService.resume(); eventStreamService.resume();
mPresence.start(); 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. * Open a given page.
@ -84,7 +88,27 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$scope.updateHeader = function() { $scope.updateHeader = function() {
$scope.user_id = matrixService.config().user_id; $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) { 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; return filtered;
}; };
@ -79,4 +79,43 @@ angular.module('matrixWebClient')
return function(text) { return function(text) {
return $sce.trustAsHtml(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; height: 32px;
} }
#callBar {
float: left;
}
#headerContent { #headerContent {
color: #ccc; color: #ccc;
max-width: 1280px; max-width: 1280px;

View File

@ -44,6 +44,19 @@
<div id="header"> <div id="header">
<!-- Do not show buttons on the login page --> <!-- Do not show buttons on the login page -->
<div id="headerContent" ng-hide="'/login' == location || '/register' == location"> <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> <a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
&nbsp; &nbsp;
<button ng-click='goToPage("/")'>Home</button> <button ng-click='goToPage("/")'>Home</button>
@ -56,7 +69,7 @@
<div id="footer" ng-hide="location.indexOf('/room') == 0"> <div id="footer" ng-hide="location.indexOf('/room') == 0">
<div id="footerContent"> <div id="footerContent">
&copy 2014 Matrix.org &copy; 2014 Matrix.org
</div> </div>
</div> </div>
</body> </body>

View File

@ -33,8 +33,7 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
console.log("Invited to room " + event.room_id); console.log("Invited to room " + event.room_id);
// FIXME push membership to top level key to match /im/sync // FIXME push membership to top level key to match /im/sync
event.membership = event.content.membership; 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; $scope.rooms[event.room_id] = event;
} }
}); });
@ -43,6 +42,11 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
$scope.rooms[event.room_id].lastMsg = event; $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() { $scope.onInit = function() {
refresh(); eventHandlerService.waitForInitialSyncCompletion().then(function() {
refresh();
});
}; };
}]); }]);

View File

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

View File

@ -82,13 +82,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
updatePresence(event); 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() { $scope.memberCount = function() {
return Object.keys($scope.members).length; 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) { var paginate = function(numItems) {
// console.log("paginate " + numItems); // console.log("paginate " + numItems);
if ($scope.state.paginating || !$scope.room_id) { if ($scope.state.paginating || !$scope.room_id) {
@ -181,11 +165,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
var isNewMember = !(target_user_id in $scope.members); var isNewMember = !(target_user_id in $scope.members);
if (isNewMember) { if (isNewMember) {
// FIXME: why are we copying these fields around inside chunk? // FIXME: why are we copying these fields around inside chunk?
if ("state" in chunk.content) { if ("presence" in chunk.content) {
chunk.presenceState = chunk.content.state; // why is this renamed? chunk.presence = chunk.content.presence;
} }
if ("mtime_age" in chunk.content) { if ("last_active_ago" in chunk.content) {
chunk.mtime_age = chunk.content.mtime_age; chunk.last_active_ago = chunk.content.last_active_ago;
} }
if ("displayname" in chunk.content) { if ("displayname" in chunk.content) {
chunk.displayname = chunk.content.displayname; chunk.displayname = chunk.content.displayname;
@ -201,9 +185,15 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
} }
} }
else { 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]; 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]; var member = $scope.members[chunk.content.user_id];
// XXX: why not just pass the chunk straight through? // XXX: why not just pass the chunk straight through?
if ("state" in chunk.content) { if ("presence" in chunk.content) {
member.presenceState = chunk.content.state; member.presence = chunk.content.presence;
} }
if ("mtime_age" in chunk.content) { if ("last_active_ago" in chunk.content) {
// FIXME: should probably keep updating mtime_age in realtime like FB does member.last_active_ago = chunk.content.last_active_ago;
member.mtime_age = chunk.content.mtime_age;
} }
// this may also contain a new display name or avatar url, so check. // 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 // Make sure the initialSync has been before going further
eventHandlerService.waitForInitialSyncCompletion().then( eventHandlerService.waitForInitialSyncCompletion().then(
function() { function() {
// Some data has been retrieved from the iniialSync request
// So, the relative time starts here
$scope.now = new Date().getTime();
var needsToJoin = true; var needsToJoin = true;
// The room members is available in the data fetched by initialSync // 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 // Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id; $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); paginate(MESSAGES_PER_PAGINATION);
updateMemberListPresenceAge();
}; };
$scope.inviteUser = function(user_id) { $scope.inviteUser = function(user_id) {
@ -455,16 +461,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
$scope.startVoiceCall = function() { $scope.startVoiceCall = function() {
var call = new MatrixCall($scope.room_id); var call = new MatrixCall($scope.room_id);
call.onError = $scope.onCallError; call.onError = $rootScope.onCallError;
call.onHangup = $scope.onCallHangup; call.onHangup = $rootScope.onCallHangup;
call.placeCall(); 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"> <div id="roomHeader">
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
<div id="roomName"> <div id="roomName">
{{ room_alias || room_id }} {{ room_id | roomName }}
</div> </div>
</div> </div>
@ -26,8 +26,8 @@
<img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/> <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> <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
</td> </td>
<td class="userPresence" ng-class="(member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')"> <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
<span ng-show="member.mtime_age">{{ member.mtime_age + (now - member.last_updated) | duration }}<br/>ago</span> <span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
</td> </td>
</table> </table>
</div> </div>
@ -100,18 +100,7 @@
<button ng-click="inviteUser(userIDToInvite)">Invite</button> <button ng-click="inviteUser(userIDToInvite)">Invite</button>
</span> </span>
<button ng-click="leaveRoom()">Leave</button> <button ng-click="leaveRoom()">Leave</button>
<button ng-click="startVoiceCall()" ng-show="currentCall == undefined && memberCount() == 2">Voice Call</button> <button ng-click="startVoiceCall()" ng-show="(currentCall == undefined || currentCall.state == 'ended') && 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>
</div> </div>
{{ feedback }} {{ feedback }}