You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-18 05:42:00 +03:00
Added a models directory. Added store, http-api and client files. Slowly transitioning to the architecture outlined in SYJS-5.
294 lines
10 KiB
JavaScript
294 lines
10 KiB
JavaScript
"use strict";
|
|
var MatrixHttpApi = require("./http-api");
|
|
var MatrixEvent = require("./models/event").MatrixEvent;
|
|
|
|
// TODO:
|
|
// Internal: rate limiting
|
|
|
|
/*
|
|
* Construct a Matrix Client.
|
|
* @param {Object} credentials The credentials for this client
|
|
* @param {Object} config The config (if any) for this client.
|
|
* Valid config params include:
|
|
* noUserAgent: true // to avoid warnings whilst setting UA headers
|
|
* debug: true // to use console.err() style debugging from the lib
|
|
* @param {Object} store The data store (if any) for this client.
|
|
* @param {Function} request The request fn to use.
|
|
*/
|
|
function MatrixClient(credentials, config, store, request) {
|
|
if (typeof credentials === "string") {
|
|
credentials = {
|
|
"baseUrl": credentials
|
|
};
|
|
}
|
|
var requiredKeys = [
|
|
"baseUrl"
|
|
];
|
|
for (var i = 0; i < requiredKeys.length; i++) {
|
|
if (!credentials.hasOwnProperty(requiredKeys[i])) {
|
|
throw new Error("Missing required key: " + requiredKeys[i]);
|
|
}
|
|
}
|
|
this.config = config;
|
|
this.credentials = credentials;
|
|
this.store = store;
|
|
|
|
// track our position in the overall eventstream
|
|
this.fromToken = undefined;
|
|
this.clientRunning = false;
|
|
this._http = new MatrixHttpApi(credentials, config, request);
|
|
}
|
|
MatrixClient.prototype = {
|
|
isLoggedIn: function() {
|
|
return this.credentials.accessToken !== undefined &&
|
|
this.credentials.userId !== undefined;
|
|
},
|
|
|
|
// Higher level APIs
|
|
// =================
|
|
|
|
// TODO: stuff to handle:
|
|
// local echo
|
|
// event dup suppression? - apparently we should still be doing this
|
|
// tracking current display name / avatar per-message
|
|
// pagination
|
|
// re-sending (including persisting pending messages to be sent)
|
|
// - Need a nice way to callback the app for arbitrary events like
|
|
// displayname changes
|
|
// due to ambiguity (or should this be on a chat-specific layer)?
|
|
// reconnect after connectivity outages
|
|
|
|
/*
|
|
* Helper method for retrieving the name of a room suitable for display
|
|
* in the UI
|
|
* TODO: in future, this should be being generated serverside.
|
|
* @param {String} roomId ID of room whose name is to be resolved
|
|
* @return {String} human-readable label for room.
|
|
*/
|
|
getFriendlyRoomName: function(roomId) {
|
|
// we need a store to track the inputs for calculating room names
|
|
if (!this.store) {
|
|
return roomId;
|
|
}
|
|
|
|
// check for an alias, if any. for now, assume first alias is the
|
|
// official one.
|
|
var alias;
|
|
var mRoomAliases = this.store.getStateEvents(roomId, 'm.room.aliases')[0];
|
|
if (mRoomAliases) {
|
|
alias = mRoomAliases.event.content.aliases[0];
|
|
}
|
|
|
|
var mRoomName = this.store.getStateEvent(roomId, 'm.room.name', '');
|
|
if (mRoomName) {
|
|
return mRoomName.event.content.name + (alias ? " (" + alias + ")" : "");
|
|
}
|
|
else if (alias) {
|
|
return alias;
|
|
}
|
|
else {
|
|
var userId = this.credentials.userId;
|
|
var members = this.store.getStateEvents(roomId, 'm.room.member')
|
|
.filter(function(event) {
|
|
return event.event.user_id !== userId;
|
|
});
|
|
|
|
if (members.length === 0) {
|
|
return "Unknown";
|
|
}
|
|
else if (members.length == 1) {
|
|
return (
|
|
members[0].event.content.displayname ||
|
|
members[0].event.user_id
|
|
);
|
|
}
|
|
else if (members.length == 2) {
|
|
return (
|
|
(members[0].event.content.displayname ||
|
|
members[0].event.user_id) +
|
|
" and " +
|
|
(members[1].event.content.displayname ||
|
|
members[1].event.user_id)
|
|
);
|
|
}
|
|
else {
|
|
return (
|
|
(members[0].event.content.displayname ||
|
|
members[0].event.user_id) +
|
|
" and " +
|
|
(members.length - 1) + " others"
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Helper method for retrieving the name of a user suitable for display
|
|
* in the UI in the context of a room - i.e. disambiguating from any
|
|
* other users in the room.
|
|
* XXX: This could perhaps also be generated serverside, perhaps by just passing
|
|
* a 'disambiguate' flag down on membership entries which have ambiguous
|
|
* displaynames?
|
|
* @param {String} userId ID of the user whose name is to be resolved
|
|
* @param {String} roomId ID of room to be used as the context for
|
|
* resolving the name.
|
|
* @return {String} human-readable name of the user.
|
|
*/
|
|
getFriendlyDisplayName: function(userId, roomId) {
|
|
// we need a store to track the inputs for calculating display names
|
|
if (!this.store) { return userId; }
|
|
|
|
var displayName;
|
|
var memberEvent = this.store.getStateEvent(roomId, 'm.room.member', userId);
|
|
if (memberEvent && memberEvent.event.content.displayname) {
|
|
displayName = memberEvent.event.content.displayname;
|
|
}
|
|
else {
|
|
return userId;
|
|
}
|
|
|
|
var members = this.store.getStateEvents(roomId, 'm.room.member')
|
|
.filter(function(event) {
|
|
return event.event.content.displayname === displayName;
|
|
});
|
|
|
|
if (members.length > 1) {
|
|
return displayName + " (" + userId + ")";
|
|
}
|
|
else {
|
|
return displayName;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* High level helper method to call initialSync, emit the resulting events,
|
|
* and then start polling the eventStream for new events.
|
|
* @param {function} callback Callback invoked whenever new event are available
|
|
* @param {Number} historyLen amount of historical timeline events to
|
|
* emit during from the initial sync.
|
|
*/
|
|
startClient: function(callback, historyLen) {
|
|
historyLen = historyLen || 12;
|
|
|
|
var self = this;
|
|
if (!this.fromToken) {
|
|
this._http.initialSync(historyLen, function(err, data) {
|
|
var i, j;
|
|
if (err) {
|
|
if (this.config && this.config.debug) {
|
|
console.error(
|
|
"startClient error on initialSync: %s",
|
|
JSON.stringify(err)
|
|
);
|
|
}
|
|
callback(err);
|
|
return;
|
|
}
|
|
if (self.store) {
|
|
var eventMapper = function(event) {
|
|
return new MatrixEvent(event);
|
|
};
|
|
// intercept the results and put them into our store
|
|
self.store.setPresenceEvents(
|
|
map(data.presence, eventMapper)
|
|
);
|
|
for (i = 0; i < data.rooms.length; i++) {
|
|
self.store.setStateEvents(
|
|
map(data.rooms[i].state, eventMapper)
|
|
);
|
|
self.store.setEvents(
|
|
map(data.rooms[i].messages.chunk, eventMapper)
|
|
);
|
|
}
|
|
}
|
|
if (data) {
|
|
self.fromToken = data.end;
|
|
var events = [];
|
|
for (i = 0; i < data.presence.length; i++) {
|
|
events.push(new MatrixEvent(data.presence[i]));
|
|
}
|
|
for (i = 0; i < data.rooms.length; i++) {
|
|
for (j = 0; j < data.rooms[i].state.length; j++) {
|
|
events.push(new MatrixEvent(data.rooms[i].state[j]));
|
|
}
|
|
for (j = 0; j < data.rooms[i].messages.chunk.length; j++) {
|
|
events.push(
|
|
new MatrixEvent(data.rooms[i].messages.chunk[j])
|
|
);
|
|
}
|
|
}
|
|
callback(undefined, events, false);
|
|
}
|
|
|
|
self.clientRunning = true;
|
|
self._pollForEvents(callback);
|
|
});
|
|
}
|
|
else {
|
|
this._pollForEvents(callback);
|
|
}
|
|
},
|
|
|
|
_pollForEvents: function(callback) {
|
|
var self = this;
|
|
if (!this.clientRunning) {
|
|
return;
|
|
}
|
|
this._http.eventStream(this.fromToken, 30000, function(err, data) {
|
|
if (err) {
|
|
if (this.config && this.config.debug) {
|
|
console.error(
|
|
"error polling for events via eventStream: %s",
|
|
JSON.stringify(err)
|
|
);
|
|
}
|
|
callback(err);
|
|
// retry every few seconds
|
|
// FIXME: this should be exponential backoff with an option to nudge
|
|
setTimeout(function() {
|
|
self._pollForEvents(callback);
|
|
}, 2000);
|
|
return;
|
|
}
|
|
|
|
if (self.store) {
|
|
self.store.setEvents(map(data.chunk,
|
|
function(event) {
|
|
return new MatrixEvent(event);
|
|
}
|
|
));
|
|
}
|
|
if (data) {
|
|
self.fromToken = data.end;
|
|
var events = [];
|
|
for (var j = 0; j < data.chunk.length; j++) {
|
|
events.push(new MatrixEvent(data.chunk[j]));
|
|
}
|
|
callback(undefined, events, true);
|
|
}
|
|
self._pollForEvents(callback);
|
|
});
|
|
},
|
|
|
|
/*
|
|
* High level helper method to stop the client from polling and allow a
|
|
* clean shutdown.
|
|
*/
|
|
stopClient: function() {
|
|
this.clientRunning = false;
|
|
},
|
|
};
|
|
|
|
var map = function(array, fn) {
|
|
var results = new Array(array.length);
|
|
for (var i = 0; i < array.length; i++) {
|
|
results[i] = fn(array[i]);
|
|
}
|
|
return results;
|
|
};
|
|
|
|
/**
|
|
* The high-level Matrix Client class.
|
|
*/
|
|
module.exports = MatrixClient; // expose the class
|