/**
* @namespace FlashphonerSFUExtended
*/
const {v4: uuidv4} = require("uuid");
const ws = require("./ws");
const messaging = require("./messaging");
const roomApi = require("./room");
const promises = require("./promise");
const constants = require("./constants");
const SFU_STATE = constants.SFU_STATE;
const SFU_EVENT = constants.SFU_EVENT;
const SFU_INTERNAL = constants.SFU_INTERNAL_API;
const sfu = {};
let rooms = {};
let connection = ws.createConnection();
let server;
const callbacks = {};
let state = SFU_STATE.NEW;
let connectionConfig;
let user;
let im;
function setupConnection(connection) {
connection.onMessage = function(name, msg){
switch (name) {
case SFU_INTERNAL.DEFAULT_METHOD:
//filter messages
if (msg[0].type === SFU_INTERNAL.MESSAGE) {
notify(SFU_EVENT.MESSAGE, msg[0].message);
} else if (msg[0].type === SFU_INTERNAL.MESSAGE_STATE) {
im.onMessageState(msg);
notify(SFU_EVENT.MESSAGE_STATE, msg[0].status);
} else if (msg[0].type === SFU_INTERNAL.USER_LIST) {
promises.resolve(msg[0].internalMessageId, msg[0].list);
notify(SFU_EVENT.USER_LIST, msg[0].list);
} else if (msg[0].type === SFU_INTERNAL.USER_CALENDAR) {
promises.resolve(msg[0].internalMessageId, msg[0].calendar);
notify(SFU_EVENT.USER_CALENDAR, msg[0].calendar);
} else if (msg[0].type === SFU_INTERNAL.NEW_CHAT) {
if(!promises.resolve(msg[0].internalMessageId, msg[0].info)) {
notify(SFU_EVENT.NEW_CHAT, msg[0].info);
}
} else if (msg[0].type === SFU_INTERNAL.CHAT_DELETED) {
notify(SFU_EVENT.CHAT_DELETED, msg[0].info);
} else if (msg[0].type === SFU_INTERNAL.CHAT_UPDATED) {
promises.resolve(msg[0].internalMessageId, msg[0].info);
notify(SFU_EVENT.CHAT_UPDATED, msg[0].info);
} else if (msg[0].type === SFU_INTERNAL.USER_CHATS) {
promises.resolve(msg[0].internalMessageId, msg[0].chats);
notify(SFU_EVENT.USER_CHATS, msg[0].chats);
} else if (msg[0].type === SFU_INTERNAL.CHAT_LOADED) {
promises.resolve(msg[0].internalMessageId, msg[0].chat);
notify(SFU_EVENT.CHAT_LOADED, msg[0].chat);
} else if (msg[0].type === constants.SFU_ROOM_EVENT.OPERATION_FAILED && promises.promised(msg[0].internalMessageId)) {
promises.reject(msg[0].internalMessageId, msg[0]);
} else if (msg[0].type === constants.SFU_EVENT.ACK && promises.promised(msg[0].internalMessageId)) {
promises.resolve(msg[0].internalMessageId);
} else if (msg[0].roomName && msg[0].roomName.length > 0) {
//room event
const room = rooms[msg[0].roomName];
if (room) {
room.processEvent(msg[0]);
}
} else {
notify(msg[0].type, msg[0]);
}
break;
case "failed":
notify(constants.SFU_EVENT.FAILED, msg[0]);
break;
}
};
connection.onClose = function(e) {
state = SFU_STATE.DISCONNECTED;
disconnect();
notify(SFU_EVENT.DISCONNECTED, e);
};
connection.onError = function(e) {
state = SFU_STATE.FAILED;
notify(SFU_STATE.FAILED, e);
};
im = messaging.create({
connection: connection
});
}
/**
* Connect to server.
*
* @param {Object} options SFU options
* @param {String} options.url Server url
* @param {String} options.username Username
* @param {String} options.password Password
* @param {String=} options.nickname Participant's nickname
* @returns {Promise<void|Error>} Promise which resolves upon connect
* @throws {TypeError} Error if no options provided
* @memberof FlashphonerSFUExtended
*/
const connect = function(options) {
if (!options) {
throw new TypeError("No options provided");
}
setupConnection(connection);
server = new URL(options.url).hostname;
connectionConfig = {
url: options.url,
appName: SFU_INTERNAL.Z_APP,
custom: {
username: options.username,
password: options.password,
nickname: options.nickname
}
};
return new Promise(function(resolve, reject) {
if (state !== SFU_STATE.NEW && state !== SFU_STATE.DISCONNECTED && state !== SFU_STATE.FAILED) {
reject(new Error("Can't connect with the state " + state));
return;
}
connection.connect(connectionConfig).then(function (connectionConfig) {
user = Object.freeze({
username: connectionConfig[0].sipLogin,
nickname: connectionConfig[0].sipVisibleName
});
state = SFU_STATE.AUTHENTICATED;
notify(SFU_EVENT.CONNECTED);
resolve(user);
}, function (e) {
state = SFU_STATE.FAILED;
notify(SFU_EVENT.FAILED, e);
reject(e);
});
});
};
/**
* Send message
* @param {Object} msg Message
* @param {String} msg.to Recipient's id (deprecated, use chatId instead)
* @param {String} msg.chatId Indicates chat this message belongs to
* @param {String} msg.body Message body
* @returns {Promise} Promise will resolve upon message delivery and reject if delivery was unsuccessful
* @throws {Error} error if api isn't connected
* @memberOf FlashphonerSFUExtended
*/
const sendMessage = function(msg) {
if (state !== SFU_STATE.AUTHENTICATED) {
throw new Error("Can't send message while in " + state + " state");
}
return im.sendMessage(msg);
};
/**
* Fetch available user list from server
* @returns {Promise<Array<FlashphonerSFUExtended.UserListEntry>>}
* @memberOf FlashphonerSFUExtended
*/
const getUserList = function() {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't get user list while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.GET_USER_LIST, {internalMessageId: id});
});
};
/**
* Fetch available calendar from server
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const getUserCalendar = function() {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't get user calendar while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.GET_USER_CALENDAR, {internalMessageId: id});
});
};
/**
* Add event to the calendar
* @param event {FlashphonerSFUExtended.UserCalendarEvent}
* @returns {Promise<void|error>}
* @memberOf FlashphonerSFUExtended
*/
const addCalendarEvent = function(event) {
return new Promise(function(resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't add calendar event while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.ADD_CALENDAR_EVENT, {
internalMessageId: id,
event: event
});
});
};
/**
* Remove event from the calendar
* @param event {FlashphonerSFUExtended.UserCalendarEvent}
* @returns {Promise<void|error>}
* @memberOf FlashphonerSFUExtended
*/
const removeCalendarEvent = function(event) {
return new Promise(function(resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't remove calendar event while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.REMOVE_CALENDAR_EVENT, {
internalMessageId: id,
event: event
});
});
};
/**
* Fetch available chats from server
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const getUserChats = function() {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't get user chats while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.GET_USER_CHATS, {internalMessageId: id});
});
};
/**
* Fetch chat data from server
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const loadChat = function(chat) {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't load chats while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.LOAD_CHAT, {id: chat.id, internalMessageId: id});
});
};
/**
* Create chat
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const createChat = function(chat) {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't create chats while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.CREATE_CHAT, {id: chat.id, name: chat.name, members: chat.members, internalMessageId: id});
});
};
/**
* Delete chat
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const deleteChat = function(chat) {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't delete chats while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.DELETE_CHAT, {id: chat.id, internalMessageId: id});
});
};
/**
* Rename chat
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const renameChat = function(chat) {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't rename chats while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.RENAME_CHAT, {id: chat.id, name: chat.name, internalMessageId: id});
});
};
/**
* Add member to chat
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const addMemberToChat = function(chat) {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't add member to chat while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.ADD_MEMBER_TO_CHAT, {id: chat.id, member: chat.member, internalMessageId: id});
});
};
/**
* Remove member from chat
* @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
* @memberOf FlashphonerSFUExtended
*/
const removeMemberFromChat = function(chat) {
return new Promise(function (resolve, reject){
if (state !== SFU_STATE.AUTHENTICATED) {
reject(new Error("Can't remove member to chat while in " + state + " state"));
return;
}
const id = uuidv4();
promises.add(id, resolve, reject);
connection.send(constants.SFU_INTERNAL_API.REMOVE_MEMBER_FROM_CHAT, {id: chat.id, member: chat.member, internalMessageId: id});
});
};
/**
* Create room
*
* @param {Object} options Room options
* @param {String} options.name Room name
* @param {String} options.pin Room's pin
* @param {Object} options.pc Peer connection
* @returns {FlashphonerSFU.Room} Room
* @throws {TypeError} Error if no options provided
* @throws {Error} error if api isn't connected
* @memberof FlashphonerSFUExtended
*/
const room = function(options) {
if (!options) {
throw new TypeError("No options provided");
}
if (state !== SFU_STATE.AUTHENTICATED) {
throw new Error("Can't create room while in " + state + " state");
}
const opt = {
connection: connection,
name: options.name,
pin: options.pin,
pc: options.pc
};
const exports = roomApi.room(opt);
rooms[options.name] = exports;
const cleanup = function() {
rooms[options.name].pc().close();
rooms[options.name].pc().dispatchEvent(new Event("connectionstatechange"));
delete rooms[options.name];
};
exports.on(constants.SFU_ROOM_EVENT.LEFT, function(participant){
if (participant.name === user.nickname) {
cleanup();
}
}).on(constants.SFU_ROOM_EVENT.EVICTED, function(participant) {
if (participant.name === user.nickname) {
cleanup();
}
}).on(constants.SFU_ROOM_EVENT.FAILED, cleanup
).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function(e) {
if (constants.SFU_OPERATIONS.ROOM_JOIN === e.operation) {
cleanup();
}
});
return exports;
};
/**
* FlashphonerSFUExtended event callback.
*
* @callback FlashphonerSFUExtended~eventCallback
* @param {FlashphonerSFUExtended} sdk instance FlashphonerSFUExtended
*/
/**
* Add session event callback.
*
* @param {String} event One of {@link FlashphonerSFU.SFU_EVENT} events
* @param {FlashphonerSFUExtended~eventCallback} callback Callback function
* @returns {FlashphonerSFUExtended} SDK instance callback was attached to
* @throws {TypeError} Error if event is not specified
* @throws {Error} Error if callback is not a valid function
* @memberOf FlashphonerSFUExtended
*/
const on = function (event, callback) {
if (!event) {
throw new TypeError("Event can't be null");
}
if (!callback || typeof callback !== "function") {
throw new Error("Callback needs to be a valid function");
}
if (!callbacks[event]) {
callbacks[event] = [];
}
callbacks[event].push(callback);
return sfu;
};
const notify = function(event, msg) {
if (callbacks[event]) {
for (const callback of callbacks[event]) {
callback(msg);
}
}
};
/**
* Disconnect sfu from the server
* @memberOf FlashphonerSFUExtended
*/
const disconnect = function() {
for (const [key, value] of Object.entries(rooms)) {
value.leaveRoom();
}
user = undefined;
connection.close();
connection = ws.createConnection();
state = SFU_STATE.DISCONNECTED;
rooms = {};
};
sfu.on = on;
sfu.connect = connect;
sfu.sendMessage = sendMessage;
sfu.getUserList = getUserList;
sfu.getUserCalendar = getUserCalendar;
sfu.addCalendarEvent = addCalendarEvent;
sfu.removeCalendarEvent = removeCalendarEvent;
sfu.getUserChats = getUserChats;
sfu.loadChat = loadChat;
sfu.createChat = createChat;
sfu.deleteChat = deleteChat;
sfu.renameChat = renameChat;
sfu.addMemberToChat = addMemberToChat;
sfu.removeMemberFromChat = removeMemberFromChat;
sfu.room = room;
/**
* @typedef {Object} SFUUser
* @property username {String} username
* @property nickname {String} nickname
* @memberOf FlashphonerSFUExtended
*/
/**
* Get current user
* @returns {FlashphonerSFUExtended.SFUUser} Returns current logged in user
* @memberOf FlashphonerSFUExtended
*/
sfu.user = function(){
return user;
};
/**
* Get hostname of the server in use
* @returns {String} Current server's hostname
* @memberOf FlashphonerSFUExtended
*/
sfu.server = function() {
return server;
};
sfu.disconnect = disconnect;
/**
* Get sfu state
* @returns {FlashphonerSFU.SFU_STATE} Current sfu state
*/
sfu.state = function() {
return state;
};
sfu.constants = constants;
module.exports = sfu;