@laboralphy/o876-txat
Version:
An embeddable chat library, multi channels, moderation, event based
493 lines (459 loc) • 15.6 kB
JavaScript
const Channel = require('./Channel');
const User = require('./User');
const USER_RANKS = require('./data/user-ranks.json');
const Events = require('events');
const StdDateProvider = require('./date-provider/StdDateProvider');
const EVENT_TYPES = require('./data/event-types.json');
const CAPABILITIES = require('./data/capabilities.json');
const Message = require('./Message');
const userRanks = require('./user-ranks');
class TxatManager {
constructor () {
/**
* @type {Map<string, Channel>}
* @private
*/
this._channels = new Map();
/**
* @type {Map<string, User>}
* @private
*/
this._users = new Map();
/**
* @type {Map<string, User>}
* @private
*/
this._disconnectedUser = new Map();
this._events = new Events();
/**
* @type {IDateProvider}
* @private
*/
this._dateProvider = new StdDateProvider();
}
get events () {
return this._events;
}
/**
* Returns true if channel exists
* @param idChannel {string}
* @returns {boolean}
*/
channelExists (idChannel) {
return this._channels.has(idChannel);
}
/**
* Returns true if user exists and is connected
* @param idUser {string}
* @returns {boolean}
*/
userExists (idUser) {
return this._users.has(idUser);
}
/**
* return a list of currently create channels
* @returns {Channel[]}
*/
getChannelList () {
return [...this._channels.values()];
}
/**
* re emit incoming event from a specified channel to this event emitter, with a reference of original channel
* @param aEvents {string[]}
* @param channel {Channel}
* @private
*/
_reemitChannelEvent (aEvents, channel) {
for (const sEvent of aEvents) {
channel.events.on(sEvent, (payload) => {
this._events.emit(sEvent, {
...payload,
channel
});
});
}
}
/**
* re emit incoming event from a specified user to this event emitter, with a reference of original user
* @param aEvents {string[]}
* @param user {User}
* @private
*/
_reemitUserEvent (aEvents, user) {
for (const sEvent of aEvents) {
user.events.on(sEvent, (payload) => {
this._events.emit(sEvent, {
...payload,
recipient: user.id
});
});
}
}
/**
* @param channel {Channel}
* @private
*/
_dispatchMessageEvent (channel) {
/**
* @param message {Message}
*/
channel.events.on(EVENT_TYPES.EVENT_CHANNEL_MESSAGE, ({ message }) => {
for (const [, user] of channel.users) {
if (!user.isIgnoring(message.sender)) {
this._events.emit(EVENT_TYPES.EVENT_CHANNEL_MESSAGE, {
message,
channel,
recipient: user.id
});
}
}
});
}
/**
* Specifically relevant for channels
* Dispatches an incoming event to all users in the channel
* @param aEvents {string[]}
* @param channel {Channel}
* @private
*/
_dispatchChannelEvent (aEvents, channel) {
for (const sEvent of aEvents) {
channel.events.on(sEvent, (payload) => {
for (const [, user] of channel.users) {
this._events.emit(sEvent, {
...payload,
channel,
recipient: user.id
});
}
});
}
}
/**
* Creates a new channel
* @param id {string}
* @returns {Channel}
*/
createChannel (id) {
if (this.channelExists(id)) {
throw new Error(`channel ${id} already exists`);
}
const channel = new Channel(id);
channel.inject({ dateProvider: this._dateProvider });
this._dispatchMessageEvent(channel);
this._dispatchChannelEvent([
EVENT_TYPES.EVENT_USER_JOINED_CHANNEL,
EVENT_TYPES.EVENT_USER_LEFT_CHANNEL,
EVENT_TYPES.EVENT_USER_BANNED,
EVENT_TYPES.EVENT_USER_UNBANNED,
EVENT_TYPES.EVENT_USER_INVITED
], channel);
this._reemitChannelEvent([
EVENT_TYPES.EVENT_USER_INSUFFICIENT_CAPABILITY
], channel);
this._channels.set(channel.id, channel);
this._events.emit(EVENT_TYPES.EVENT_CHANNEL_CREATED, { channel });
return channel;
}
/**
* Destroy channel after kicking all users out
* @param id {string}
*/
destroyChannel (id) {
if (this.channelExists(id)) {
const channel = this.getChannel(id);
channel.close();
this._channels.delete(id);
this._events.emit(EVENT_TYPES.EVENT_CHANNEL_DESTROYED, { channel });
} else {
throw new Error(`this channel ${id} does not exist`);
}
}
/**
* Creates or reuse a user
* @param id {string}
* @returns {User}
*/
createUser (id) {
const user = new User(id);
this._reemitUserEvent([
EVENT_TYPES.EVENT_USER_RECEIVE_PRIVATE_MESSAGE
], user);
return user;
}
/**
* Returns instance of channel identified by specified parameter
* @param idChannel {string}
* @returns {Channel}
*/
getChannel (idChannel) {
if (this._channels.has(idChannel)) {
return this._channels.get(idChannel);
} else {
throw new Error(`channel ${idChannel} does not exist`);
}
}
/**
* Return instance of user identified by specifed parameter
* @param idUser
* @returns {User}
*/
getUser (idUser) {
if (this._users.has(idUser)) {
return this._users.get(idUser);
} else if (this._disconnectedUser.has(idUser)) {
return this._disconnectedUser.get(idUser);
} else {
throw new Error(`user ${idUser} does not exist`);
}
}
/**
* A new user is connected, will reuse user instance if they have visited before.
* @param idUser {string}
* @return {User}
*/
connectUser (idUser) {
const user = this._disconnectedUser.has(idUser)
? this._disconnectedUser.get(idUser)
: this.createUser(idUser);
this._users.set(idUser, user);
this._disconnectedUser.delete(idUser);
user.connected = true;
this._events.emit(EVENT_TYPES.EVENT_USER_CONNECTED, { user });
return user;
}
/**
* Disconnects a user from chat
* however the user is not fully deleted, it is kept in disconnectedUsers map for future reuse
* @param user {User}
*/
disconnectUser (user) {
const oFullState = this.getUserFullState(user);
for (const channel of this.getChannelList()) {
if (channel.users.has(user.id)) {
channel.removeUser(user);
}
}
this._disconnectedUser.set(user.id, user);
this._users.delete(user.id);
user.connected = false;
this._events.emit(EVENT_TYPES.EVENT_USER_DISCONNECTED, { user, state: oFullState });
}
/**
* @typedef TxatUserFullState {object}
* @property user {TxatUserState}
* @property bans {TxatBanState}
*
* @param user {User}
* @returns {TxatUserFullState}
*/
getUserFullState (user) {
const oUserState = user.state;
const aBanState = [];
for (const channel of this.getChannelList()) {
const ban = channel._bans.get(user.id);
if (ban) {
aBanState.push(ban.state);
}
}
return {
user: oUserState,
bans: aBanState
};
}
/**
* Checks if user has the specified capability
* @param user
* @param channel
* @param capability
*/
checkChannelUserCapability (user, channel, capability) {
const userRank = channel.getUserRank(user);
if (!userRank.capabilities.has(capability)) {
throw new Error(`user ${user.id} can't perform this operation on channel ${channel.id} ; insufficient capabilities ; need ${capability}`);
}
}
/**
* Return the list of channel the specified user is connected to
* @param user {User}
* @return {Channel[]}
*/
getUserConnectedChannels (user) {
return Array.from(this._channels.values())
.filter(channel => channel.users.has(user.id));
}
/** ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ***** */
/** ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ***** */
/** ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ****** USE CASES ***** */
/**
* /join <channel>
*
* Makes a user join a channel,
* if channel does not exist : create it and make user channel host
* @param user {User}
* @param channelOrId {string|Channel}
* @return {boolean} true if users join channel, false if user cannot join because banned, or private channel
*/
userJoinsChannel (user, channelOrId) {
let channel;
if (typeof channelOrId === 'string') {
const idChannel = channelOrId;
if (this.channelExists(idChannel)) {
channel = this.getChannel(idChannel);
} else {
const channel = this.createChannel(idChannel);
channel.addUser(user);
channel.setUserRank(user, USER_RANKS.USER_RANK_HOST);
return true;
}
} else if (channelOrId instanceof Channel) {
channel = channelOrId;
} else {
throw new TypeError('parameter should be either Channel instance or string');
}
if (channel.public || channel.getUserRank(user).capabilities.has(CAPABILITIES.CAPABILITY_JOIN_PRIVATE)) {
if (!channel.addUser(user)) {
throw new Error(`user ${user.id} could not join channel ${channel.id} because banned`);
}
} else {
throw new Error(`user ${user.id} could not join channel ${channel.id} because channel is private, must ask for invitation`);
}
}
/**
* /leave <channel>
*
* Makes a user leaving a channel
* @param user {User}
* @param channel {Channel}
*/
userLeavesChannel (user, channel) {
channel.removeUser(user);
if (channel.users.size === 0 && !channel.permanent) {
this._channels.delete(channel.id);
}
}
/**
* /say <message>
*
* Makes a user send a public message to a channel
* @param user {User}
* @param channel {Channel}
* @param text {string}
*/
userSendsChannelMessage (user, channel, text) {
return channel.postMessage(user, text);
}
/**
* Makes a user sens a private message to another user
* @param user {User}
* @param userDest {User}
* @param text {string}
*/
userSendsPrivateMessage (user, userDest, text) {
const message = new Message(text, user, this._dateProvider.now().getTime());
userDest.receivePrivateMessage(message);
}
/**
* A promoter changes a user rank within a channel
* @param promotedUser {User} the user whose rank is changed
* @param channel {Channel} the channel where user and promoter are chatting
* @param rank {string} new rank
* @param user {User} the user who is promoting
*/
setUserRank (user, channel, promotedUser, rank, ) {
this.checkChannelUserCapability(user, channel, CAPABILITIES.CAPABILITY_PROMOTE);
const newRank = userRanks[rank];
if (!newRank) {
throw new Error(`unknown rank id ${rank}`);
}
const promoterRank = channel.getUserRank(user);
const userCurrentRank = channel.getUserRank(promotedUser);
if (userCurrentRank.index < promoterRank.index && newRank.index <= promoterRank.index) {
channel.setUserRank(promotedUser, rank);
return true;
}
if (userCurrentRank.index >= promoterRank.index) {
throw new Error(`user rank is greater or equal than promoter rank ; user ${userCurrentRank.id} - promoter ${promoterRank.id}`);
}
if (newRank.index > promoterRank.index) {
throw new Error(`new rank would be higher than promoter rank ; promoter ${promoterRank.id} - target rank ${newRank.id}`);
}
}
/**
* Bans a user for a specified duration, reason
* @param bannedUser {User} user being banned
* @param channel {Channel}
* @param duration {string}
* @param reason {string} reason why user is banned
* @param user {User}
*/
banUser (user, channel, bannedUser, duration, reason) {
this.checkChannelUserCapability(user, channel, CAPABILITIES.CAPABILITY_BAN);
if (duration === 'forever' || duration === 'permanent' || duration === 'perm') {
channel.banUser(bannedUser, reason, { permanent: true });
} else if (duration.match(/^[0-9]+\s+[a-zA-Z]+$/)) {
channel.banUser(bannedUser, reason, { duration: duration });
} else {
channel.banUser(bannedUser, reason, { date: duration.trim() });
}
}
/**
* Unbans a user from a specific channel
* @param bannedUser {User}
* @param channel {Channel}
* @param user {User}
*/
unbanUser (user, channel, bannedUser) {
this.checkChannelUserCapability(user, channel, CAPABILITIES.CAPABILITY_UNBAN);
channel.unbanUser(bannedUser);
}
/**
* Ignores a user
* @param user {User}
* @param ignoredUser {User}
*/
ignoreUser (user, ignoredUser) {
user.addIgnore(ignoredUser);
}
/**
* Unignore user
* @param user {User}
* @param ignoredUser {User}
*/
unignoreUser (user, ignoredUser) {
user.removeIgnore(ignoredUser);
}
/**
* Set a new name for a channel
* @param user {User}
* @param channel {Channel}
* @param sName {string}
*/
setChannelName (user, channel, sName) {
this.checkChannelUserCapability(user, CAPABILITIES.CAPABILITY_MANAGE_CHANNEL);
channel.name = sName;
}
/**
* Set channel public flag
* @param user {User}
* @param channel {Channel}
* @param bPublic {boolean}
*/
setChannelAccess (user, channel, bPublic) {
this.checkChannelUserCapability(user, CAPABILITIES.CAPABILITY_MANAGE_CHANNEL);
channel.public = bPublic;
}
inviteUser (user, channel, guest) {
this.checkChannelUserCapability(user, channel, CAPABILITIES.CAPABILITY_INVITE);
if (!channel.isUserBanned(user) && !channel.isUserBanned(guest)) {
if (channel.addUser(user)) {
channel.events.emit(EVENT_TYPES.EVENT_USER_INVITED, {
user: guest,
channel,
inviter: user
});
}
}
}
}
module.exports = TxatManager;