0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-11-14 05:51:10 +01:00
construct/modules/static/charybdis/room/state.js

478 lines
10 KiB
JavaScript
Raw Normal View History

/*
* 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)
{
};
*/