mirror of
https://github.com/matrix-construct/construct
synced 2024-11-17 23:40:57 +01:00
59e05b44be
Client Alpha (Chlamydia Client)
477 lines
10 KiB
JavaScript
477 lines
10 KiB
JavaScript
/*
|
|
* IRCd Charybdis 5/Matrix
|
|
*
|
|
* Copyright (C) 2017 Charybdis Development Team
|
|
* Copyright (C) 2017 Jason Volk (jason@zemos.net)
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice is present in all copies.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
|
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
*******************************************************************************
|
|
* State Function
|
|
*
|
|
* Dictionary of events considered "state events." The state provides context for the
|
|
* event horizon of a timeline (the termination point at the start) to prevent the
|
|
* need for querying indeterminately over the horizon.
|
|
*
|
|
* This class allows the timeline to be the single source of
|
|
* authority for events in the room. So long as the timeline is
|
|
* maintained in order, the state object will always provide
|
|
* an accurate result as to the state of a room. This proxy
|
|
* prevents any race between the timeline and the state object.
|
|
*/
|
|
room.state = class
|
|
{
|
|
constructor(timeline)
|
|
{
|
|
// The state function holds an internal reference to the timeline to make queries
|
|
this.timeline = timeline;
|
|
|
|
// The results of queries to the timeline are cached in the backing-object here.
|
|
this.cache = Object.copy(room.state.defaults);
|
|
|
|
// Cache timestamps (type => timestamp) is used to find out if the cache data is stale with
|
|
// respect to when the timeline was last modified. This is partitioned by type even though
|
|
// the timeline only has one TS value for now so all types are invalidated at the same time.
|
|
this.ts = {};
|
|
|
|
this.content = new room.state.content(this);
|
|
}
|
|
};
|
|
|
|
room.state.prototype.set = function(state, type, val)
|
|
{
|
|
mc.abort({message: "illegal"});
|
|
return false;
|
|
};
|
|
|
|
room.state.prototype.get = function(state, type)
|
|
{
|
|
if(this.valid(type))
|
|
if((type in state))
|
|
return state[type];
|
|
|
|
let query = mc.event.is_type.bind(null, type);
|
|
let idx = this.timeline.query(query);
|
|
let event = this.timeline[idx[0]];
|
|
if(!event || event.state_key === undefined)
|
|
return state[type];
|
|
|
|
this.update(type);
|
|
return event.state_key.length? this._get_aggreg(state, type, idx):
|
|
this._get_single(state, type, idx);
|
|
};
|
|
|
|
room.state.prototype._get_single = function(state, type, idx)
|
|
{
|
|
state[type] = this.timeline[idx[0]];
|
|
return state[type];
|
|
};
|
|
|
|
room.state.prototype._get_aggreg = function(state, type, idx)
|
|
{
|
|
if(typeof(state[type]) != "object")
|
|
state[type] = {};
|
|
|
|
// Ensure true chronological order here so events that happen
|
|
// after other events can simply overwrite them in this iteration.
|
|
let present = this.timeline[0];
|
|
idx.reverse().forEach((i) =>
|
|
{
|
|
let event = this.timeline[i];
|
|
state[type][event.state_key] = event;
|
|
});
|
|
|
|
return state[type];
|
|
};
|
|
|
|
/** Determine if the state function has an up to date cache.
|
|
*/
|
|
room.state.prototype.valid = function(type)
|
|
{
|
|
let time = this.ts[type];
|
|
return time && time >= this.timeline.modified;
|
|
};
|
|
|
|
/** Updates the cache timestamp for type
|
|
*/
|
|
room.state.prototype.update = function(type)
|
|
{
|
|
return this.ts[type] = mc.now();
|
|
};
|
|
|
|
/** Clears the cache for type
|
|
*
|
|
* After this, access to state will require new queries into the timeline.
|
|
*/
|
|
room.state.prototype.invalidate = function(type)
|
|
{
|
|
Object.clear(this.cache[type]);
|
|
Object.defaults(this.cache[type], room.state.defaults[type]);
|
|
this.ts[type] = 0;
|
|
};
|
|
|
|
/**
|
|
**************************************
|
|
*
|
|
* State content tree
|
|
*
|
|
*/
|
|
room.state.content = class
|
|
{
|
|
constructor(state, depth = 0)
|
|
{
|
|
this.state = state;
|
|
this.depth = depth;
|
|
}
|
|
};
|
|
|
|
room.state.content.prototype.set = function(state, key, val)
|
|
{
|
|
mc.abort({message: "unimplemented"});
|
|
return false;
|
|
};
|
|
|
|
room.state.content.prototype.get = function(state, key)
|
|
{
|
|
for(let full in state)
|
|
{
|
|
let type = full.split(".");
|
|
for(let i = 0; i < this.depth; i++)
|
|
type.shift();
|
|
|
|
let part = type.shift();
|
|
if(part != key)
|
|
continue;
|
|
|
|
if(type.length > 0)
|
|
{
|
|
let handler = new room.state.content(this.state, this.depth + 1);
|
|
return new Proxy(state, handler);
|
|
}
|
|
|
|
if(state[full].content)
|
|
return state[full].content;
|
|
else
|
|
return Object.map(state[full], (key, event) => event.content);
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(room.state, 'defaults', {
|
|
writable: false,
|
|
enumerable: false,
|
|
configurable: false,
|
|
value:
|
|
{
|
|
// 10.5.1: m.room.aliases - state_key'ed dictionary of aliases
|
|
"m.room.aliases":
|
|
{
|
|
// "server_hostname": content.aliases
|
|
},
|
|
|
|
// 10.5.2: m.room.canonical_alias - main event
|
|
"m.room.canonical_alias":
|
|
{
|
|
content:
|
|
{
|
|
alias: undefined,
|
|
},
|
|
},
|
|
|
|
// 10.5.3: m.room.create - main event
|
|
"m.room.create":
|
|
{
|
|
content:
|
|
{
|
|
creator: undefined,
|
|
},
|
|
},
|
|
|
|
// 10.5.4: m.room.join_rules - main event
|
|
"m.room.join_rules":
|
|
{
|
|
content:
|
|
{
|
|
join_rule: undefined,
|
|
},
|
|
},
|
|
|
|
// 10.5.5: m.room.member - state_key'ed dictionary of members
|
|
"m.room.member":
|
|
{
|
|
// "mxid": { membership: "join", displayname: "foo" },
|
|
},
|
|
|
|
// 10.5.6: m.room.power_levels - main event
|
|
"m.room.power_levels":
|
|
{
|
|
content:
|
|
{
|
|
// Defaults
|
|
events_default: 0,
|
|
state_default: 0,
|
|
users_default: 0,
|
|
|
|
// Actions (10.5.6: defaults to 50 if unspecified)
|
|
ban: 50,
|
|
invite: 50,
|
|
kick: 50,
|
|
redact: 50,
|
|
|
|
// Access maps
|
|
events: {}, // Power level required for event type; { "m.room.type": power }
|
|
users: {}, // Power level available to user; { mxid: power }
|
|
},
|
|
},
|
|
|
|
// 11.2.1.3: m.room.name - main event
|
|
"m.room.name":
|
|
{
|
|
content:
|
|
{
|
|
name: undefined,
|
|
},
|
|
},
|
|
|
|
// 11.2.1.4: m.room.topic - main event
|
|
"m.room.topic":
|
|
{
|
|
content:
|
|
{
|
|
topic: undefined,
|
|
},
|
|
},
|
|
|
|
// 11.2.1.5: m.room.avatar - main event
|
|
"m.room.avatar":
|
|
{
|
|
content:
|
|
{
|
|
info: {},
|
|
url: undefined,
|
|
},
|
|
},
|
|
|
|
// 11.9.1.1: m.room.history_visibility - main event
|
|
"m.room.history_visibility":
|
|
{
|
|
content:
|
|
{
|
|
history_visibility: "shared", // 11.9.3: defaults to 'shared'
|
|
},
|
|
},
|
|
|
|
// 11.13.1.1: m.room.guest_access - main event
|
|
"m.room.guest_access":
|
|
{
|
|
content:
|
|
{
|
|
guest_access: "forbidden",
|
|
}
|
|
},
|
|
}});
|
|
|
|
|
|
/**
|
|
* State -> Summary
|
|
*
|
|
* Digests a room's state into the format of the matrix rooms
|
|
* directory (i.e. public state).
|
|
*
|
|
* Usage: room.state.summary(myroom.state)
|
|
*
|
|
* This is a static function, as the room's state object proxies the
|
|
* timeline and state.public() will then query the timeline.
|
|
*/
|
|
room.state.summary = function(state)
|
|
{
|
|
let members = state['m.room.member'];
|
|
let is_join = (mxid) => maybe(() => members[mxid].content.membership == "join");
|
|
|
|
let aliases = [];
|
|
Object.each(state['m.room.aliases'], (state_key, event) =>
|
|
{
|
|
aliases = aliases.concat(event.content.aliases);
|
|
});
|
|
|
|
return {
|
|
room_id: state['m.room.create'].room_id,
|
|
name: state['m.room.name'].content.name,
|
|
topic: state['m.room.topic'].content.topic,
|
|
canonical_alias: state['m.room.canonical_alias'].content.alias,
|
|
world_readable: state['m.room.history_visibility'].content.history_visibility == "world_readable",
|
|
guest_can_join: state['m.room.guest_access'].content.guest_access == "can_join",
|
|
num_joined_members: Array.count(Object.keys(members), is_join),
|
|
aliases: aliases,
|
|
};
|
|
};
|
|
|
|
/** Summary -> State (events)
|
|
*
|
|
* Translate summary data into an array of events. The events will not
|
|
* have authentic data other than the essential content but we specify
|
|
* an origin_server_ts of 0 indicating they should be replaced by any
|
|
* timeline algorithm.
|
|
*/
|
|
room.state.summary.parse = function(summary)
|
|
{
|
|
let events = [];
|
|
Object.each(summary, (key, val) =>
|
|
{
|
|
let handler = room.state.summary.parse.on[key];
|
|
if(typeof(handler) == "function") try
|
|
{
|
|
let ret = handler(val);
|
|
if(typeof(ret) != "object")
|
|
return;
|
|
|
|
if(Array.isArray(ret))
|
|
events = events.concat(ret);
|
|
else
|
|
events.push(ret);
|
|
}
|
|
catch(error)
|
|
{
|
|
console.error("Room summary parse error: key[" + key + "] " + error);
|
|
}
|
|
else
|
|
{
|
|
console.warn("Unhandled room summary key [" + key + "]");
|
|
}
|
|
});
|
|
|
|
return events;
|
|
};
|
|
|
|
// Collection of handlers.
|
|
room.state.summary.parse.on = {};
|
|
|
|
room.state.summary.parse.on["room_id"] = function(room_id)
|
|
{
|
|
return;
|
|
};
|
|
|
|
room.state.summary.parse.on["name"] = function(name)
|
|
{
|
|
return {
|
|
type: "m.room.name",
|
|
origin_server_ts: 0,
|
|
state_key: "",
|
|
content:
|
|
{
|
|
name: name,
|
|
},
|
|
};
|
|
};
|
|
|
|
room.state.summary.parse.on["topic"] = function(topic)
|
|
{
|
|
return {
|
|
type: "m.room.topic",
|
|
origin_server_ts: 0,
|
|
state_key: "",
|
|
content:
|
|
{
|
|
alias: topic,
|
|
}
|
|
};
|
|
};
|
|
|
|
room.state.summary.parse.on["canonical_alias"] = function(canonical_alias)
|
|
{
|
|
return {
|
|
type: "m.room.canonical_alias",
|
|
origin_server_ts: 0,
|
|
state_key: "",
|
|
content:
|
|
{
|
|
alias: canonical_alias,
|
|
}
|
|
};
|
|
};
|
|
|
|
room.state.summary.parse.on["aliases"] = function(aliases)
|
|
{
|
|
let contents = {};
|
|
aliases.forEach((alias) =>
|
|
{
|
|
let [local, domain] = alias.split(":");
|
|
if(!(domain in contents))
|
|
contents[domain] = [];
|
|
|
|
contents[domain].push(alias);
|
|
});
|
|
|
|
let events = [];
|
|
Object.each(contents, (domain, aliases) =>
|
|
{
|
|
events.push(
|
|
{
|
|
type: "m.room.aliases",
|
|
origin_server_ts: 0,
|
|
state_key: domain,
|
|
content:
|
|
{
|
|
aliases: aliases,
|
|
},
|
|
})
|
|
});
|
|
|
|
return events;
|
|
};
|
|
|
|
room.state.summary.parse.on["guest_can_join"] = function(bool)
|
|
{
|
|
return {
|
|
type: "m.room.guest_access",
|
|
origin_server_ts: 0,
|
|
state_key: "",
|
|
content:
|
|
{
|
|
guest_access: bool? "can_join" : "forbidden",
|
|
},
|
|
};
|
|
};
|
|
|
|
room.state.summary.parse.on["world_readable"] = function(bool)
|
|
{
|
|
return {
|
|
type: "m.room.history_visibility",
|
|
origin_server_ts: 0,
|
|
state_key: "",
|
|
content:
|
|
{
|
|
history_visibility: bool? "world_readable" : undefined,
|
|
},
|
|
};
|
|
};
|
|
|
|
room.state.summary.parse.on["num_joined_members"] = function(num_joined_members)
|
|
{
|
|
// TODO: XXX
|
|
return
|
|
};
|
|
|
|
/* TODO: XXX
|
|
room.state.summary.parse.on["avatar_url"] = function(avatar_url)
|
|
{
|
|
};
|
|
*/
|