1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-18 05:42:00 +03:00
Files
matrix-js-sdk/lib/client.js
Kegan Dougal d1e51de7ec Split out matrix.js into different files. Glue things back.
Added a models directory. Added store, http-api and client files. Slowly
transitioning to the architecture outlined in SYJS-5.
2015-06-03 17:55:12 +01:00

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