mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-28 06:47:25 +00:00
Chore: Move presence to package (#25541)
This commit is contained in:
parent
8dc8817d9e
commit
6d3b20d81c
@ -39,9 +39,9 @@ import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKe
|
||||
import { resetTOTP } from '../../../2fa/server/functions/resetTOTP';
|
||||
import { Team } from '../../../../server/sdk';
|
||||
import { isValidQuery } from '../lib/isValidQuery';
|
||||
import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers';
|
||||
import { getURL } from '../../../utils/server';
|
||||
import { getUploadFormData } from '../lib/getUploadFormData';
|
||||
import { api } from '../../../../server/sdk/api';
|
||||
|
||||
API.v1.addRoute(
|
||||
'users.getAvatar',
|
||||
@ -1031,7 +1031,11 @@ API.v1.addRoute(
|
||||
},
|
||||
});
|
||||
|
||||
setUserStatus(user, status);
|
||||
const { _id, username, statusText, roles, name } = user;
|
||||
api.broadcast('presence.status', {
|
||||
user: { status, _id, username, statusText, roles, name },
|
||||
previousStatus: user.status,
|
||||
});
|
||||
} else {
|
||||
throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', {
|
||||
method: 'users.setStatus',
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Random } from 'meteor/random';
|
||||
import { UserPresence } from 'meteor/konecty:user-presence';
|
||||
import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge';
|
||||
import type { IUserCreationOptions, IUser } from '@rocket.chat/apps-engine/definition/users';
|
||||
import { Subscriptions, Users as UsersRaw } from '@rocket.chat/models';
|
||||
@ -84,7 +83,11 @@ export class AppUserBridge extends UserBridge {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async update(user: IUser & { id: string }, fields: Partial<IUser>, appId: string): Promise<boolean> {
|
||||
protected async update(
|
||||
user: IUser & { id: string },
|
||||
fields: Partial<IUser> & { statusDefault: string },
|
||||
appId: string,
|
||||
): Promise<boolean> {
|
||||
this.orch.debugLog(`The App ${appId} is updating a user`);
|
||||
|
||||
if (!user) {
|
||||
@ -98,12 +101,12 @@ export class AppUserBridge extends UserBridge {
|
||||
const { status } = fields;
|
||||
delete fields.status;
|
||||
|
||||
await UsersRaw.update({ _id: user.id }, { $set: fields as any });
|
||||
|
||||
if (status) {
|
||||
UserPresence.setDefaultStatus(user.id, status);
|
||||
fields.statusDefault = status;
|
||||
}
|
||||
|
||||
await UsersRaw.updateOne({ _id: user.id }, { $set: fields as any });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import './cron';
|
||||
import './status.ts';
|
||||
|
||||
export { Apps, AppEvents } from './orchestrator';
|
||||
@ -1,16 +0,0 @@
|
||||
import { UserPresenceMonitor } from 'meteor/konecty:user-presence';
|
||||
|
||||
import { AppEvents, Apps } from './orchestrator';
|
||||
|
||||
UserPresenceMonitor.onSetUserStatus((...args: any) => {
|
||||
const [user, status] = args;
|
||||
|
||||
// App IPostUserStatusChanged event hook
|
||||
Promise.await(
|
||||
Apps.triggerEvent(AppEvents.IPostUserStatusChanged, {
|
||||
user,
|
||||
currentStatus: status,
|
||||
previousStatus: user.status,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -26,7 +26,7 @@ export { saveUserIdentity } from './saveUserIdentity';
|
||||
export { sendMessage } from './sendMessage';
|
||||
export { setEmail } from './setEmail';
|
||||
export { setRealName, _setRealName } from './setRealName';
|
||||
export { setStatusText, _setStatusText, _setStatusTextPromise } from './setStatusText';
|
||||
export { setStatusText } from './setStatusText';
|
||||
export { getStatusText } from './getStatusText';
|
||||
export { setUserAvatar } from './setUserAvatar';
|
||||
export { _setUsername, setUsername } from './setUsername';
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import s from 'underscore.string';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Users as UsersRaw } from '@rocket.chat/models';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { Users } from '../../../models/server';
|
||||
import { hasPermission } from '../../../authorization/server';
|
||||
import { RateLimiter } from '../lib';
|
||||
import { api } from '../../../../server/sdk/api';
|
||||
|
||||
export const _setStatusTextPromise = async function (userId: string, statusText: string): Promise<boolean> {
|
||||
async function _setStatusTextPromise(userId: string, statusText: string): Promise<boolean> {
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
statusText = s.trim(statusText).substr(0, 120);
|
||||
|
||||
const user = await UsersRaw.findOneById(userId);
|
||||
const user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'roles' | 'statusText'>>(userId, {
|
||||
projection: { username: 1, name: 1, status: 1, roles: 1, statusText: 1 },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
@ -25,44 +26,20 @@ export const _setStatusTextPromise = async function (userId: string, statusText:
|
||||
return true;
|
||||
}
|
||||
|
||||
await UsersRaw.updateStatusText(user._id, statusText);
|
||||
await Users.updateStatusText(user._id, statusText);
|
||||
|
||||
const { _id, username, status } = user;
|
||||
const { _id, username, status, name, roles } = user;
|
||||
api.broadcast('presence.status', {
|
||||
user: { _id, username, status, statusText },
|
||||
user: { _id, username, status, statusText, name, roles },
|
||||
previousStatus: status,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export const _setStatusText = function (userId: any, statusText: string): IUser | boolean {
|
||||
statusText = s.trim(statusText);
|
||||
if (statusText.length > 120) {
|
||||
statusText = statusText.substr(0, 120);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = Users.findOneById(userId);
|
||||
|
||||
// User already has desired statusText, return
|
||||
if (user.statusText === statusText) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// Set new statusText
|
||||
Users.updateStatusText(user._id, statusText);
|
||||
user.statusText = statusText;
|
||||
|
||||
const { _id, username, status } = user;
|
||||
api.broadcast('presence.status', {
|
||||
user: { _id, username, status, statusText },
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
function _setStatusText(userId: any, statusText: string): boolean {
|
||||
return Promise.await(_setStatusTextPromise(userId, statusText));
|
||||
}
|
||||
|
||||
export const setStatusText = RateLimiter.limitFunction(_setStatusText, 5, 60000, {
|
||||
0() {
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { UserPresenceMonitor } from 'meteor/konecty:user-presence';
|
||||
|
||||
import { Livechat } from './lib/Livechat';
|
||||
import { hasAnyRole } from '../../authorization/server/functions/hasRole';
|
||||
|
||||
UserPresenceMonitor.onSetUserStatus((user, status) => {
|
||||
if (hasAnyRole(user._id, ['livechat-manager', 'livechat-monitor', 'livechat-agent'])) {
|
||||
Livechat.notifyAgentStatusChanged(user._id, status);
|
||||
}
|
||||
});
|
||||
@ -1,8 +1,6 @@
|
||||
import './livechat';
|
||||
import './config';
|
||||
import './startup';
|
||||
import './visitorStatus';
|
||||
import './agentStatus';
|
||||
import '../lib/messageTypes';
|
||||
import './hooks/beforeCloseRoom';
|
||||
import './hooks/beforeDelegateAgent';
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { UserPresenceEvents } from 'meteor/konecty:user-presence';
|
||||
|
||||
import { Livechat } from './lib/Livechat';
|
||||
|
||||
Meteor.startup(() => {
|
||||
UserPresenceEvents.on('setStatus', (session, status, metadata) => {
|
||||
if (metadata && metadata.visitor) {
|
||||
Livechat.notifyGuestStatusChanged(metadata.visitor, status);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1082,16 +1082,6 @@ export class Users extends Base {
|
||||
return this.update(_id, update);
|
||||
}
|
||||
|
||||
updateStatusById(_id, status) {
|
||||
const update = {
|
||||
$set: {
|
||||
status,
|
||||
},
|
||||
};
|
||||
|
||||
return this.update(_id, update);
|
||||
}
|
||||
|
||||
addPasswordToHistory(_id, password) {
|
||||
const update = {
|
||||
$push: {
|
||||
|
||||
@ -1,17 +1,29 @@
|
||||
import { settings } from '../../settings/server';
|
||||
import { Voip } from '../../../server/sdk';
|
||||
|
||||
settings.watch('VoIP_Enabled', (value: boolean) => {
|
||||
return value ? Voip.init() : Voip.stop();
|
||||
settings.watch('VoIP_Enabled', async function (value: boolean) {
|
||||
try {
|
||||
if (value) {
|
||||
await Voip.init();
|
||||
} else {
|
||||
await Voip.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
|
||||
settings.changeMultiple(
|
||||
['VoIP_Management_Server_Host', 'VoIP_Management_Server_Port', 'VoIP_Management_Server_Username', 'VoIP_Management_Server_Password'],
|
||||
(_values) => {
|
||||
async function (_values) {
|
||||
// Here, if 4 settings are changed at once, we're getting 4 diff callbacks. The good part is that all callbacks are fired almost instantly
|
||||
// So to avoid stopping/starting voip too often, we debounce the call and restart 1 second after the last setting has reached us.
|
||||
if (settings.get('VoIP_Enabled')) {
|
||||
Voip.refresh();
|
||||
try {
|
||||
await Voip.refresh();
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ServiceBroker, Context, ServiceSchema } from 'moleculer';
|
||||
|
||||
import { asyncLocalStorage } from '../../server/sdk';
|
||||
import type { IBroker, IBrokerNode, IServiceMetrics } from '../../server/sdk/types/IBroker';
|
||||
import type { ServiceClass } from '../../server/sdk/types/ServiceClass';
|
||||
import type { IServiceClass } from '../../server/sdk/types/ServiceClass';
|
||||
import type { EventSignatures } from '../../server/sdk/lib/Events';
|
||||
|
||||
const events: { [k: string]: string } = {
|
||||
@ -35,6 +35,7 @@ export class NetworkBroker implements IBroker {
|
||||
|
||||
this.metrics = broker.metrics;
|
||||
|
||||
// TODO move this to a proper startup method?
|
||||
this.started = this.broker.start();
|
||||
}
|
||||
|
||||
@ -73,18 +74,19 @@ export class NetworkBroker implements IBroker {
|
||||
return this.broker.call(method, data);
|
||||
}
|
||||
|
||||
destroyService(instance: ServiceClass): void {
|
||||
destroyService(instance: IServiceClass): void {
|
||||
this.broker.destroyService(instance.getName());
|
||||
}
|
||||
|
||||
createService(instance: ServiceClass): void {
|
||||
createService(instance: IServiceClass): void {
|
||||
const methods = (
|
||||
instance.constructor?.name === 'Object'
|
||||
? Object.getOwnPropertyNames(instance)
|
||||
: Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
|
||||
).filter((name) => name !== 'constructor');
|
||||
|
||||
if (!instance.getEvents() || !methods.length) {
|
||||
const instanceEvents = instance.getEvents();
|
||||
if (!instanceEvents && !methods.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -98,7 +100,7 @@ export class NetworkBroker implements IBroker {
|
||||
this.broker.logger.debug({ msg: 'Not shutting down, different node.', nodeID: this.broker.nodeID });
|
||||
return;
|
||||
}
|
||||
this.broker.logger.info({ msg: 'Received shutdown event, destroying service.', nodeID: this.broker.nodeID });
|
||||
this.broker.logger.warn({ msg: 'Received shutdown event, destroying service.', nodeID: this.broker.nodeID });
|
||||
this.destroyService(instance);
|
||||
});
|
||||
}
|
||||
@ -109,7 +111,7 @@ export class NetworkBroker implements IBroker {
|
||||
name,
|
||||
actions: {},
|
||||
...dependencies,
|
||||
events: instance.getEvents().reduce<Record<string, (ctx: Context) => void>>((map, eventName) => {
|
||||
events: instanceEvents.reduce<Record<string, (ctx: Context) => void>>((map, eventName) => {
|
||||
map[eventName] = /^\$/.test(eventName)
|
||||
? (ctx: Context): void => {
|
||||
// internal events params are not an array
|
||||
|
||||
@ -6,18 +6,18 @@ module.exports = {
|
||||
name: 'authorization',
|
||||
watch: [...watch, '../../../server/services/authorization'],
|
||||
},
|
||||
{
|
||||
name: 'presence',
|
||||
},
|
||||
// {
|
||||
// name: 'presence',
|
||||
// },
|
||||
{
|
||||
name: 'account',
|
||||
},
|
||||
{
|
||||
name: 'stream-hub',
|
||||
},
|
||||
{
|
||||
name: 'ddp-streamer',
|
||||
},
|
||||
// {
|
||||
// name: 'ddp-streamer',
|
||||
// },
|
||||
].map((app) =>
|
||||
Object.assign(app, {
|
||||
script: app.script || `ts-node --files ${app.name}/service.ts`,
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
"scripts": {
|
||||
"dev": "pm2 start ecosystem.config.js",
|
||||
"pm2": "pm2",
|
||||
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} run-p start:account start:authorization start:stream-hub",
|
||||
"start:account": "ts-node --files ./account/service.ts",
|
||||
"start:authorization": "ts-node --files ./authorization/service.ts",
|
||||
"start:presence": "ts-node --files ./presence/service.ts",
|
||||
"start:stream-hub": "ts-node --files ./stream-hub/service.ts",
|
||||
"typecheck": "tsc --noEmit --skipLibCheck",
|
||||
"build": "tsc",
|
||||
@ -62,9 +62,10 @@
|
||||
"@types/fibers": "^3.1.1",
|
||||
"@types/node": "^14.18.21",
|
||||
"@types/ws": "^8.5.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pino-pretty": "^7.6.1",
|
||||
"pm2": "^5.2.0",
|
||||
"ts-node": "^10.8.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.5.5"
|
||||
},
|
||||
"volta": {
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
import type { UserStatus } from '@rocket.chat/core-typings';
|
||||
|
||||
import { newConnection } from './actions/newConnection';
|
||||
import { removeConnection } from './actions/removeConnection';
|
||||
import { removeLostConnections } from './actions/removeLostConnections';
|
||||
import { setStatus, setConnectionStatus } from './actions/setStatus';
|
||||
import { updateUserPresence } from './actions/updateUserPresence';
|
||||
import { ServiceClass } from '../../../../server/sdk/types/ServiceClass';
|
||||
import type { IPresence } from '../../../../server/sdk/types/IPresence';
|
||||
import type { IBrokerNode } from '../../../../server/sdk/types/IBroker';
|
||||
|
||||
export class Presence extends ServiceClass implements IPresence {
|
||||
protected name = 'presence';
|
||||
|
||||
async onNodeDisconnected({ node }: { node: IBrokerNode }): Promise<void> {
|
||||
const affectedUsers = await this.removeLostConnections(node.id);
|
||||
return affectedUsers.forEach((uid) => this.updateUserPresence(uid));
|
||||
}
|
||||
|
||||
async started(): Promise<void> {
|
||||
setTimeout(async () => {
|
||||
const affectedUsers = await this.removeLostConnections();
|
||||
return affectedUsers.forEach((uid) => this.updateUserPresence(uid));
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async newConnection(uid: string, session: string): Promise<{ uid: string; connectionId: string } | undefined> {
|
||||
const result = await newConnection(uid, session, this.context);
|
||||
await updateUserPresence(uid);
|
||||
return result;
|
||||
}
|
||||
|
||||
async removeConnection(uid: string, session: string): Promise<{ uid: string; session: string }> {
|
||||
const result = await removeConnection(uid, session);
|
||||
await updateUserPresence(uid);
|
||||
return result;
|
||||
}
|
||||
|
||||
async removeLostConnections(nodeID?: string): Promise<string[]> {
|
||||
return removeLostConnections(nodeID, this.context);
|
||||
}
|
||||
|
||||
async setStatus(uid: string, status: UserStatus, statusText?: string): Promise<boolean> {
|
||||
return setStatus(uid, status, statusText);
|
||||
}
|
||||
|
||||
async setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean> {
|
||||
const result = await setConnectionStatus(uid, status, session);
|
||||
await updateUserPresence(uid);
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateUserPresence(uid: string): Promise<void> {
|
||||
return updateUserPresence(uid);
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import { getCollection, Collections } from '../../mongo';
|
||||
import type { IServiceContext } from '../../../../../server/sdk/types/ServiceClass';
|
||||
|
||||
const status = 'online';
|
||||
|
||||
export async function newConnection(
|
||||
uid: string,
|
||||
session: string,
|
||||
context?: IServiceContext,
|
||||
): Promise<{ uid: string; connectionId: string } | undefined> {
|
||||
const instanceId = context?.nodeID;
|
||||
|
||||
if (!instanceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = {
|
||||
_id: uid,
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const update = {
|
||||
$push: {
|
||||
connections: {
|
||||
id: session,
|
||||
instanceId,
|
||||
status,
|
||||
_createdAt: now,
|
||||
_updatedAt: now,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// if (metadata) {
|
||||
// update.$set = {
|
||||
// metadata: metadata
|
||||
// };
|
||||
// connection.metadata = metadata;
|
||||
// }
|
||||
|
||||
const UserSession = await getCollection(Collections.UserSession);
|
||||
await UserSession.updateOne(query, update, { upsert: true });
|
||||
|
||||
return {
|
||||
uid,
|
||||
connectionId: session,
|
||||
};
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import { getCollection, Collections } from '../../mongo';
|
||||
|
||||
export async function removeConnection(uid: string, session: string): Promise<{ uid: string; session: string }> {
|
||||
const query = {
|
||||
'connections.id': session,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$pull: {
|
||||
connections: {
|
||||
id: session,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const UserSession = await getCollection(Collections.UserSession);
|
||||
await UserSession.updateMany(query, update);
|
||||
|
||||
return {
|
||||
uid,
|
||||
session,
|
||||
};
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
import type { Collection } from 'mongodb';
|
||||
import type { IUserSession } from '@rocket.chat/core-typings';
|
||||
|
||||
import { getCollection, Collections } from '../../mongo';
|
||||
import type { IServiceContext } from '../../../../../server/sdk/types/ServiceClass';
|
||||
|
||||
async function getAffectedUsers(model: Collection<IUserSession>, query: object): Promise<string[]> {
|
||||
const list = await model.find<{ _id: string }>(query, { projection: { _id: 1 } }).toArray();
|
||||
return list.map(({ _id }) => _id);
|
||||
}
|
||||
|
||||
// TODO: Change this to use find and modify
|
||||
export async function removeLostConnections(nodeID?: string, context?: IServiceContext): Promise<string[]> {
|
||||
const UserSession = await getCollection<IUserSession>(Collections.UserSession);
|
||||
|
||||
if (nodeID) {
|
||||
const query = {
|
||||
'connections.instanceId': nodeID,
|
||||
};
|
||||
const update = {
|
||||
$pull: {
|
||||
connections: {
|
||||
instanceId: nodeID,
|
||||
},
|
||||
},
|
||||
};
|
||||
const affectedUsers = await getAffectedUsers(UserSession, query);
|
||||
|
||||
const { modifiedCount } = await UserSession.updateMany(query, update);
|
||||
|
||||
if (modifiedCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return affectedUsers;
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes = await context.broker.nodeList();
|
||||
|
||||
const ids = nodes.filter((node) => node.available).map(({ id }) => id);
|
||||
|
||||
const affectedUsers = await getAffectedUsers(UserSession, {
|
||||
'connections.instanceId': {
|
||||
$exists: true,
|
||||
$nin: ids,
|
||||
},
|
||||
});
|
||||
|
||||
const update = {
|
||||
$pull: {
|
||||
connections: {
|
||||
instanceId: {
|
||||
$nin: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { modifiedCount } = await UserSession.updateMany({}, update);
|
||||
|
||||
if (modifiedCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return affectedUsers;
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
import type { IUser, IUserSession, UserStatus } from '@rocket.chat/core-typings';
|
||||
|
||||
import { getCollection, Collections } from '../../mongo';
|
||||
import { processPresenceAndStatus } from '../lib/processConnectionStatus';
|
||||
import { api } from '../../../../../server/sdk/api';
|
||||
|
||||
export async function setStatus(uid: string, statusDefault: UserStatus, statusText?: string): Promise<boolean> {
|
||||
const query = { _id: uid };
|
||||
|
||||
const UserSession = await getCollection<IUserSession>(Collections.UserSession);
|
||||
const userSessions = (await UserSession.findOne(query)) || { connections: [] };
|
||||
|
||||
const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault);
|
||||
|
||||
const update = {
|
||||
statusDefault,
|
||||
status,
|
||||
statusConnection,
|
||||
...(typeof statusText !== 'undefined'
|
||||
? {
|
||||
// TODO logic duplicated from Rocket.Chat core
|
||||
statusText: String(statusText || '')
|
||||
.trim()
|
||||
.substr(0, 120),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const User = await getCollection(Collections.User);
|
||||
const result = await User.updateOne(query, {
|
||||
$set: update,
|
||||
});
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
const user = await User.findOne<IUser>(query, { projection: { username: 1 } });
|
||||
api.broadcast('presence.status', {
|
||||
user: { _id: uid, username: user?.username, status, statusText },
|
||||
});
|
||||
}
|
||||
|
||||
return !!result.modifiedCount;
|
||||
}
|
||||
|
||||
export async function setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean> {
|
||||
const query = {
|
||||
'_id': uid,
|
||||
'connections.id': session,
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
'connections.$.status': status,
|
||||
'connections.$._updatedAt': now,
|
||||
},
|
||||
};
|
||||
|
||||
const UserSession = await getCollection(Collections.UserSession);
|
||||
const result = await UserSession.updateOne(query, update);
|
||||
|
||||
return !!result.modifiedCount;
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
// import { afterAll } from '../hooks';
|
||||
import type { IUserSession, IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
import { processPresenceAndStatus } from '../lib/processConnectionStatus';
|
||||
import { getCollection, Collections } from '../../mongo';
|
||||
import { api } from '../../../../../server/sdk/api';
|
||||
|
||||
const projection = {
|
||||
projection: {
|
||||
username: 1,
|
||||
statusDefault: 1,
|
||||
statusText: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export async function updateUserPresence(uid: string): Promise<void> {
|
||||
const query = { _id: uid };
|
||||
|
||||
const UserSession = await getCollection<IUserSession>(Collections.UserSession);
|
||||
const User = await getCollection<IUser>(Collections.User);
|
||||
|
||||
const user = await User.findOne<IUser>(query, projection);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userSessions = (await UserSession.findOne(query)) || { connections: [] };
|
||||
|
||||
const { statusDefault } = user;
|
||||
|
||||
const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault);
|
||||
|
||||
const result = await User.updateOne(query, {
|
||||
$set: { status, statusConnection },
|
||||
});
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
api.broadcast('presence.status', {
|
||||
user: { _id: uid, username: user.username, status, statusText: user.statusText },
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import '../../startup/broker';
|
||||
|
||||
import { api } from '../../../../server/sdk/api';
|
||||
import { Presence } from './Presence';
|
||||
|
||||
api.registerService(new Presence());
|
||||
@ -1,3 +1,2 @@
|
||||
import '../../message-read-receipt/server';
|
||||
import '../../personal-access-tokens/server';
|
||||
import '../../users-presence/server';
|
||||
@ -1,36 +0,0 @@
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import { UserPresenceEvents } from 'meteor/konecty:user-presence';
|
||||
|
||||
import { settings } from '../../../app/settings/server';
|
||||
import { api } from '../../../server/sdk/api';
|
||||
|
||||
export const STATUS_MAP = {
|
||||
[UserStatus.OFFLINE]: 0,
|
||||
[UserStatus.ONLINE]: 1,
|
||||
[UserStatus.AWAY]: 2,
|
||||
[UserStatus.BUSY]: 3,
|
||||
};
|
||||
|
||||
export const setUserStatus = (user, status /* , statusConnection*/) => {
|
||||
const { _id, username, statusText } = user;
|
||||
|
||||
// since this callback can be called by only one instance in the cluster
|
||||
// we need to broadcast the change to all instances
|
||||
api.broadcast('presence.status', {
|
||||
user: { status, _id, username, statusText }, // TODO remove username
|
||||
});
|
||||
};
|
||||
|
||||
let TroubleshootDisablePresenceBroadcast;
|
||||
settings.watch('Troubleshoot_Disable_Presence_Broadcast', (value) => {
|
||||
if (TroubleshootDisablePresenceBroadcast === value) {
|
||||
return;
|
||||
}
|
||||
TroubleshootDisablePresenceBroadcast = value;
|
||||
|
||||
if (value) {
|
||||
return UserPresenceEvents.removeListener('setUserStatus', setUserStatus);
|
||||
}
|
||||
|
||||
UserPresenceEvents.on('setUserStatus', setUserStatus);
|
||||
});
|
||||
@ -1 +0,0 @@
|
||||
import './activeUsers';
|
||||
@ -16,7 +16,7 @@
|
||||
"start": "meteor",
|
||||
"build:ci": "METEOR_DISABLE_OPTIMISTIC_CACHING=1 meteor build --server-only",
|
||||
"dev": "meteor --exclude-archs \"web.browser.legacy, web.cordova\"",
|
||||
"dsv": "meteor --exclude-archs \"web.browser.legacy, web.cordova\"",
|
||||
"dsv": "meteor npm run dev",
|
||||
"ha": "meteor npm run ha:start",
|
||||
"ha:start": "ts-node .scripts/run-ha.ts main",
|
||||
"ha:add": "ts-node .scripts/run-ha.ts instance",
|
||||
@ -183,7 +183,7 @@
|
||||
"stylelint-order": "^5.0.0",
|
||||
"supertest": "^6.2.3",
|
||||
"template-file": "^6.0.1",
|
||||
"ts-node": "^10.8.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.5.5",
|
||||
"webpack": "^4.46.0"
|
||||
},
|
||||
@ -223,6 +223,7 @@
|
||||
"@rocket.chat/mp3-encoder": "0.24.0",
|
||||
"@rocket.chat/onboarding-ui": "next",
|
||||
"@rocket.chat/poplib": "workspace:^",
|
||||
"@rocket.chat/presence": "workspace:^",
|
||||
"@rocket.chat/rest-typings": "workspace:^",
|
||||
"@rocket.chat/string-helpers": "next",
|
||||
"@rocket.chat/ui-client": "workspace:^",
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
LivechatInquiry,
|
||||
LivechatDepartmentAgents,
|
||||
Rooms,
|
||||
UsersSessions,
|
||||
Roles,
|
||||
LoginServiceConfiguration,
|
||||
InstanceStatus,
|
||||
@ -23,7 +22,6 @@ export const watchCollections = [
|
||||
Subscriptions.getCollectionName(),
|
||||
LivechatInquiry.getCollectionName(),
|
||||
LivechatDepartmentAgents.getCollectionName(),
|
||||
UsersSessions.getCollectionName(),
|
||||
Permissions.getCollectionName(),
|
||||
Roles.getCollectionName(),
|
||||
Rooms.getCollectionName(),
|
||||
|
||||
@ -66,6 +66,7 @@ import './methods/setUserActiveStatus';
|
||||
import './methods/setUserPassword';
|
||||
import './methods/toogleFavorite';
|
||||
import './methods/unmuteUserInRoom';
|
||||
import './methods/userPresence';
|
||||
import './methods/userSetUtcOffset';
|
||||
import './publications/messages';
|
||||
import './publications/room';
|
||||
|
||||
28
apps/meteor/server/methods/userPresence.ts
Normal file
28
apps/meteor/server/methods/userPresence.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { Presence } from '../sdk';
|
||||
|
||||
Meteor.methods({
|
||||
'UserPresence:setDefaultStatus'(status): Promise<boolean> | undefined {
|
||||
const { userId } = this;
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
return Presence.setStatus(userId, status);
|
||||
},
|
||||
'UserPresence:online'(): Promise<boolean> | undefined {
|
||||
const { userId, connection } = this;
|
||||
if (!userId || !connection) {
|
||||
return;
|
||||
}
|
||||
return Presence.setConnectionStatus(userId, UserStatus.ONLINE, connection.id);
|
||||
},
|
||||
'UserPresence:away'(): Promise<boolean> | undefined {
|
||||
const { userId, connection } = this;
|
||||
if (!userId || !connection) {
|
||||
return;
|
||||
}
|
||||
return Presence.setConnectionStatus(userId, UserStatus.AWAY, connection.id);
|
||||
},
|
||||
});
|
||||
@ -736,6 +736,35 @@ export class UsersRaw extends BaseRaw {
|
||||
return this.update(query, update, { multi: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {object} status
|
||||
* @param {string} status.status
|
||||
* @param {string} status.statusConnection
|
||||
* @param {string} [status.statusDefault]
|
||||
* @param {string} [status.statusText]
|
||||
*/
|
||||
updateStatusById(userId, { statusDefault, status, statusConnection, statusText }) {
|
||||
const query = {
|
||||
_id: userId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
status,
|
||||
statusConnection,
|
||||
...(statusDefault && { statusDefault }),
|
||||
...(statusText && {
|
||||
statusText: String(statusText).trim().substr(0, 120),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// We don't want to update the _updatedAt field on this operation,
|
||||
// so we can check if the status update triggered a change
|
||||
return this.col.updateOne(query, update);
|
||||
}
|
||||
|
||||
openAgentsBusinessHoursByBusinessHourId(businessHourIds) {
|
||||
const query = {
|
||||
roles: 'livechat-agent',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { IUserSession, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IUserSession, IUserSessionConnection, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IUsersSessionsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db } from 'mongodb';
|
||||
import type { FindCursor, Collection, Db, FindOptions } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
@ -28,4 +28,104 @@ export class UsersSessionsRaw extends BaseRaw<IUserSession> implements IUsersSes
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateConnectionStatusById(uid: string, connectionId: string, status: string): ReturnType<BaseRaw<IUserSession>['updateOne']> {
|
||||
const query = {
|
||||
'_id': uid,
|
||||
'connections.id': connectionId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
'connections.$.status': status,
|
||||
'connections.$._updatedAt': new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
async removeConnectionsFromInstanceId(instanceId: string): ReturnType<BaseRaw<IUserSession>['updateMany']> {
|
||||
return this.updateMany(
|
||||
{
|
||||
'connections.instanceId': instanceId,
|
||||
},
|
||||
{
|
||||
$pull: {
|
||||
connections: {
|
||||
instanceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
removeConnectionsFromOtherInstanceIds(instanceIds: string[]): ReturnType<BaseRaw<IUserSession>['updateMany']> {
|
||||
return this.updateMany(
|
||||
{},
|
||||
{
|
||||
$pull: {
|
||||
connections: {
|
||||
instanceId: {
|
||||
$nin: instanceIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async removeConnectionByConnectionId(connectionId: string): ReturnType<BaseRaw<IUserSession>['updateMany']> {
|
||||
return this.updateMany(
|
||||
{
|
||||
'connections.id': connectionId,
|
||||
},
|
||||
{
|
||||
$pull: {
|
||||
connections: {
|
||||
id: connectionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findByInstanceId(instanceId: string): FindCursor<IUserSession> {
|
||||
return this.find({
|
||||
'connections.instanceId': instanceId,
|
||||
});
|
||||
}
|
||||
|
||||
addConnectionById(
|
||||
userId: string,
|
||||
{ id, instanceId, status }: Pick<IUserSessionConnection, 'id' | 'instanceId' | 'status'>,
|
||||
): ReturnType<BaseRaw<IUserSession>['updateOne']> {
|
||||
const now = new Date();
|
||||
|
||||
const update = {
|
||||
$push: {
|
||||
connections: {
|
||||
id,
|
||||
instanceId,
|
||||
status,
|
||||
_createdAt: now,
|
||||
_updatedAt: now,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id: userId }, update, { upsert: true });
|
||||
}
|
||||
|
||||
findByOtherInstanceIds(instanceIds: string[], options?: FindOptions<IUserSession>): FindCursor<IUserSession> {
|
||||
return this.find(
|
||||
{
|
||||
'connections.instanceId': {
|
||||
$exists: true,
|
||||
$nin: instanceIds,
|
||||
},
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ import type {
|
||||
SettingValue,
|
||||
ILivechatInquiryRecord,
|
||||
IRole,
|
||||
IUserSession,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import {
|
||||
Subscriptions,
|
||||
@ -24,7 +23,6 @@ import {
|
||||
Users,
|
||||
Settings,
|
||||
Roles,
|
||||
UsersSessions,
|
||||
LivechatInquiry,
|
||||
LivechatDepartmentAgents,
|
||||
Rooms,
|
||||
@ -39,7 +37,6 @@ import {
|
||||
|
||||
import { subscriptionFields, roomFields } from './publishFields';
|
||||
import type { EventSignatures } from '../../sdk/lib/Events';
|
||||
import { isPresenceMonitorEnabled } from '../../lib/isPresenceMonitorEnabled';
|
||||
import type { DatabaseWatcher } from '../../database/DatabaseWatcher';
|
||||
|
||||
type BroadcastCallback = <T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>) => Promise<void>;
|
||||
@ -160,25 +157,6 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb
|
||||
});
|
||||
});
|
||||
|
||||
if (isPresenceMonitorEnabled()) {
|
||||
watcher.on<IUserSession>(UsersSessions.getCollectionName(), async ({ clientAction, id, data: eventData }) => {
|
||||
switch (clientAction) {
|
||||
case 'inserted':
|
||||
case 'updated':
|
||||
const data = eventData ?? (await UsersSessions.findOneById(id));
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast('watch.userSessions', { clientAction, userSession: data });
|
||||
break;
|
||||
case 'removed':
|
||||
broadcast('watch.userSessions', { clientAction, userSession: { _id: id } });
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watcher.on<ILivechatInquiryRecord>(LivechatInquiry.getCollectionName(), async ({ clientAction, id, data, diff }) => {
|
||||
switch (clientAction) {
|
||||
case 'inserted':
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import { isRunningMs } from '../lib/isRunningMs';
|
||||
import { Api } from './lib/Api';
|
||||
import { LocalBroker } from './lib/LocalBroker';
|
||||
|
||||
export const api = new Api();
|
||||
|
||||
if (!isRunningMs()) {
|
||||
api.setBroker(new LocalBroker());
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
// import { BaseBroker } from './BaseBroker';
|
||||
import type { IBroker } from '../types/IBroker';
|
||||
import type { ServiceClass } from '../types/ServiceClass';
|
||||
import type { IApiService } from '../types/IApiService';
|
||||
import type { IBroker, IBrokerNode } from '../types/IBroker';
|
||||
import type { IServiceClass } from '../types/ServiceClass';
|
||||
import type { EventSignatures } from './Events';
|
||||
import { LocalBroker } from './LocalBroker';
|
||||
|
||||
export class Api {
|
||||
private services = new Set<ServiceClass>();
|
||||
export class Api implements IApiService {
|
||||
private services: Set<IServiceClass> = new Set<IServiceClass>();
|
||||
|
||||
private broker: IBroker = new LocalBroker();
|
||||
private broker: IBroker;
|
||||
|
||||
// set a broker for the API and registers all services in the broker
|
||||
setBroker(broker: IBroker): void {
|
||||
@ -16,7 +15,7 @@ export class Api {
|
||||
this.services.forEach((service) => this.broker.createService(service));
|
||||
}
|
||||
|
||||
destroyService(instance: ServiceClass): void {
|
||||
destroyService(instance: IServiceClass): void {
|
||||
if (!this.services.has(instance)) {
|
||||
return;
|
||||
}
|
||||
@ -28,9 +27,11 @@ export class Api {
|
||||
this.services.delete(instance);
|
||||
}
|
||||
|
||||
registerService(instance: ServiceClass): void {
|
||||
registerService(instance: IServiceClass): void {
|
||||
this.services.add(instance);
|
||||
|
||||
instance.setApi(this);
|
||||
|
||||
if (this.broker) {
|
||||
this.broker.createService(instance);
|
||||
}
|
||||
@ -59,4 +60,8 @@ export class Api {
|
||||
async broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
|
||||
return this.broker.broadcastLocal(event, ...args);
|
||||
}
|
||||
|
||||
nodeList(): Promise<IBrokerNode[]> {
|
||||
return this.broker.nodeList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,12 +17,12 @@ import type {
|
||||
ISocketConnection,
|
||||
ISubscription,
|
||||
IUser,
|
||||
IUserSession,
|
||||
IUserStatus,
|
||||
IInvite,
|
||||
IWebdavAccount,
|
||||
ICustomSound,
|
||||
VoipEventDataSignature,
|
||||
UserStatus,
|
||||
} from '@rocket.chat/core-typings';
|
||||
|
||||
import type { AutoUpdateRecord } from '../types/IMeteor';
|
||||
@ -88,12 +88,14 @@ export type EventSignatures = {
|
||||
'user.nameChanged'(user: Partial<IUser>): void;
|
||||
'user.roleUpdate'(update: Record<string, any>): void;
|
||||
'user.updateCustomStatus'(userStatus: IUserStatus): void;
|
||||
'presence.status'(data: { user: Partial<IUser> }): void;
|
||||
'presence.status'(data: {
|
||||
user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'name' | 'roles'>;
|
||||
previousStatus: UserStatus | undefined;
|
||||
}): void;
|
||||
'watch.messages'(data: { clientAction: ClientAction; message: Partial<IMessage> }): void;
|
||||
'watch.roles'(data: { clientAction: ClientAction; role: Partial<IRole> }): void;
|
||||
'watch.rooms'(data: { clientAction: ClientAction; room: Pick<IRoom, '_id'> & Partial<IRoom> }): void;
|
||||
'watch.subscriptions'(data: { clientAction: ClientAction; subscription: Partial<ISubscription> }): void;
|
||||
'watch.userSessions'(data: { clientAction: ClientAction; userSession: Partial<IUserSession> }): void;
|
||||
'watch.inquiries'(data: { clientAction: ClientAction; inquiry: IInquiry; diff?: undefined | Record<string, any> }): void;
|
||||
'watch.settings'(data: { clientAction: ClientAction; setting: ISetting }): void;
|
||||
'watch.users'(data: {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { InstanceStatus } from '@rocket.chat/models';
|
||||
|
||||
import type { IBroker, IBrokerNode } from '../types/IBroker';
|
||||
import type { ServiceClass } from '../types/ServiceClass';
|
||||
import type { ServiceClass, IServiceClass } from '../types/ServiceClass';
|
||||
import { asyncLocalStorage } from '..';
|
||||
import type { EventSignatures } from './Events';
|
||||
import { StreamerCentral } from '../../modules/streamer/streamer.module';
|
||||
@ -49,9 +51,11 @@ export class LocalBroker implements IBroker {
|
||||
}
|
||||
}
|
||||
|
||||
createService(instance: ServiceClass): void {
|
||||
createService(instance: IServiceClass): void {
|
||||
const namespace = instance.getName();
|
||||
|
||||
instance.created();
|
||||
|
||||
instance.getEvents().forEach((eventName) => {
|
||||
this.events.on(eventName, (...args) => {
|
||||
instance.emit(eventName, ...(args as Parameters<EventSignatures[typeof eventName]>));
|
||||
@ -70,6 +74,8 @@ export class LocalBroker implements IBroker {
|
||||
|
||||
this.methods.set(`${namespace}.${method}`, i[method].bind(i));
|
||||
}
|
||||
|
||||
instance.started();
|
||||
}
|
||||
|
||||
async broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
|
||||
@ -91,6 +97,9 @@ export class LocalBroker implements IBroker {
|
||||
}
|
||||
|
||||
async nodeList(): Promise<IBrokerNode[]> {
|
||||
return [];
|
||||
// TODO models should not be called form here. we should create an abstraction to an internal service to perform this query
|
||||
const instances = await InstanceStatus.find({}, { projection: { _id: 1 } }).toArray();
|
||||
|
||||
return instances.map(({ _id }) => ({ id: _id, available: true }));
|
||||
}
|
||||
}
|
||||
|
||||
27
apps/meteor/server/sdk/types/IApiService.ts
Normal file
27
apps/meteor/server/sdk/types/IApiService.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { IBroker, IBrokerNode } from './IBroker';
|
||||
import type { IServiceClass } from './ServiceClass';
|
||||
import type { EventSignatures } from '../lib/Events';
|
||||
|
||||
export interface IApiService {
|
||||
setBroker(broker: IBroker): void;
|
||||
|
||||
destroyService(instance: IServiceClass): void;
|
||||
|
||||
registerService(instance: IServiceClass): void;
|
||||
|
||||
call(method: string, data?: unknown): Promise<any>;
|
||||
|
||||
waitAndCall(method: string, data: any): Promise<any>;
|
||||
|
||||
broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
|
||||
|
||||
broadcastToServices<T extends keyof EventSignatures>(
|
||||
services: string[],
|
||||
event: T,
|
||||
...args: Parameters<EventSignatures[T]>
|
||||
): Promise<void>;
|
||||
|
||||
broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
|
||||
|
||||
nodeList(): Promise<IBrokerNode[]>;
|
||||
}
|
||||
3
apps/meteor/server/sdk/types/IAppsEngineService.ts
Normal file
3
apps/meteor/server/sdk/types/IAppsEngineService.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { IServiceClass } from './ServiceClass';
|
||||
|
||||
export type IAppsEngineService = IServiceClass;
|
||||
@ -1,18 +1,18 @@
|
||||
import type { ServiceClass } from './ServiceClass';
|
||||
import type { IServiceClass } from './ServiceClass';
|
||||
import type { EventSignatures } from '../lib/Events';
|
||||
|
||||
export interface IBrokerNode {
|
||||
id: string;
|
||||
instanceID: string;
|
||||
instanceID?: string;
|
||||
available: boolean;
|
||||
local: boolean;
|
||||
local?: boolean;
|
||||
// lastHeartbeatTime: 16,
|
||||
// config: {},
|
||||
// client: { type: 'nodejs', version: '0.14.10', langVersion: 'v12.18.3' },
|
||||
// metadata: {},
|
||||
// ipList: [ '192.168.0.100', '192.168.1.25' ],
|
||||
// port: 59989,
|
||||
// hostname: 'RocketChats-MacBook-Pro-Rodrigo-Nascimento.local',
|
||||
// hostname: 'service.local-1',
|
||||
// udpAddress: null,
|
||||
// cpu: 25,
|
||||
// cpuSeq: 1,
|
||||
@ -47,8 +47,8 @@ export interface IServiceMetrics {
|
||||
|
||||
export interface IBroker {
|
||||
metrics?: IServiceMetrics;
|
||||
destroyService(service: ServiceClass): void;
|
||||
createService(service: ServiceClass): void;
|
||||
destroyService(service: IServiceClass): void;
|
||||
createService(service: IServiceClass): void;
|
||||
call(method: string, data: any): Promise<any>;
|
||||
waitAndCall(method: string, data: any): Promise<any>;
|
||||
broadcastToServices<T extends keyof EventSignatures>(
|
||||
|
||||
3
apps/meteor/server/sdk/types/IOmnichannelService.ts
Normal file
3
apps/meteor/server/sdk/types/IOmnichannelService.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { IServiceClass } from './ServiceClass';
|
||||
|
||||
export type IOmnichannelService = IServiceClass;
|
||||
@ -3,10 +3,19 @@ import type { UserStatus } from '@rocket.chat/core-typings';
|
||||
import type { IServiceClass } from './ServiceClass';
|
||||
|
||||
export interface IPresence extends IServiceClass {
|
||||
newConnection(uid: string, session: string): Promise<{ uid: string; connectionId: string } | undefined>;
|
||||
removeConnection(uid: string, session: string): Promise<{ uid: string; session: string }>;
|
||||
newConnection(
|
||||
uid: string | undefined,
|
||||
session: string | undefined,
|
||||
nodeId: string,
|
||||
): Promise<{ uid: string; connectionId: string } | undefined>;
|
||||
removeConnection(
|
||||
uid: string | undefined,
|
||||
session: string | undefined,
|
||||
nodeId: string,
|
||||
): Promise<{ uid: string; session: string } | undefined>;
|
||||
removeLostConnections(nodeID: string): Promise<string[]>;
|
||||
setStatus(uid: string, status: UserStatus, statusText?: string): Promise<boolean>;
|
||||
setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean>;
|
||||
updateUserPresence(uid: string): Promise<void>;
|
||||
toggleBroadcast(enabled: boolean): void;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
|
||||
import { asyncLocalStorage } from '..';
|
||||
import type { IBroker, IBrokerNode } from './IBroker';
|
||||
import type { EventSignatures } from '../lib/Events';
|
||||
import type { IApiService } from './IApiService';
|
||||
|
||||
export interface IServiceContext {
|
||||
id: string; // Context ID
|
||||
@ -29,12 +30,18 @@ export interface IServiceClass {
|
||||
onNodeConnected?({ node, reconnected }: { node: IBrokerNode; reconnected: boolean }): void;
|
||||
onNodeUpdated?({ node }: { node: IBrokerNode }): void;
|
||||
onNodeDisconnected?({ node, unexpected }: { node: IBrokerNode; unexpected: boolean }): Promise<void>;
|
||||
getEvents(): Array<keyof EventSignatures>;
|
||||
|
||||
setApi(api: IApiService): void;
|
||||
|
||||
onEvent<T extends keyof EventSignatures>(event: T, handler: EventSignatures[T]): void;
|
||||
emit<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): void;
|
||||
|
||||
created?(): Promise<void>;
|
||||
started?(): Promise<void>;
|
||||
stopped?(): Promise<void>;
|
||||
isInternal(): boolean;
|
||||
|
||||
created(): Promise<void>;
|
||||
started(): Promise<void>;
|
||||
stopped(): Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class ServiceClass implements IServiceClass {
|
||||
@ -44,10 +51,16 @@ export abstract class ServiceClass implements IServiceClass {
|
||||
|
||||
protected internal = false;
|
||||
|
||||
protected api: IApiService;
|
||||
|
||||
constructor() {
|
||||
this.emit = this.emit.bind(this);
|
||||
}
|
||||
|
||||
setApi(api: IApiService): void {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
getEvents(): Array<keyof EventSignatures> {
|
||||
return this.events.eventNames() as unknown as Array<keyof EventSignatures>;
|
||||
}
|
||||
@ -71,6 +84,18 @@ export abstract class ServiceClass implements IServiceClass {
|
||||
public emit<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): void {
|
||||
this.events.emit(event, ...args);
|
||||
}
|
||||
|
||||
async created(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
|
||||
async started(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
|
||||
async stopped(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
17
apps/meteor/server/services/apps-engine/service.ts
Normal file
17
apps/meteor/server/services/apps-engine/service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ServiceClassInternal } from '../../sdk/types/ServiceClass';
|
||||
import type { IAppsEngineService } from '../../sdk/types/IAppsEngineService';
|
||||
import { Apps, AppEvents } from '../../../app/apps/server/orchestrator';
|
||||
|
||||
export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService {
|
||||
protected name = 'apps-engine';
|
||||
|
||||
async created() {
|
||||
this.onEvent('presence.status', async ({ user, previousStatus }): Promise<void> => {
|
||||
Apps.triggerEvent(AppEvents.IPostUserStatusChanged, {
|
||||
user,
|
||||
currentStatus: user.status,
|
||||
previousStatus,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ServiceConfiguration } from 'meteor/service-configuration';
|
||||
import { UserPresenceMonitor, UserPresence } from 'meteor/konecty:user-presence';
|
||||
import { MongoInternals } from 'meteor/mongo';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
@ -16,10 +15,9 @@ import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'
|
||||
import { onlineAgents, monitorAgents } from '../../../app/livechat/server/lib/stream/agentStatus';
|
||||
import { matrixBroadCastActions } from '../../stream/streamBroadcast';
|
||||
import { triggerHandler } from '../../../app/integrations/server/lib/triggerHandler';
|
||||
import { ListenersModule, minimongoChangeMap } from '../../modules/listeners/listeners.module';
|
||||
import { ListenersModule } from '../../modules/listeners/listeners.module';
|
||||
import notifications from '../../../app/notifications/server/lib/Notifications';
|
||||
import { configureEmailInboxes } from '../../features/EmailInbox/EmailInbox';
|
||||
import { isPresenceMonitorEnabled } from '../../lib/isPresenceMonitorEnabled';
|
||||
import { use } from '../../../app/settings/server/Middleware';
|
||||
import type { IRoutingManagerConfig } from '../../../definition/IRoutingManagerConfig';
|
||||
|
||||
@ -148,38 +146,14 @@ export class MeteorService extends ServiceClassInternal implements IMeteor {
|
||||
setValue(setting._id, undefined);
|
||||
});
|
||||
|
||||
// TODO: May need to merge with https://github.com/RocketChat/Rocket.Chat/blob/0ddc2831baf8340cbbbc432f88fc2cb97be70e9b/ee/server/services/Presence/Presence.ts#L28
|
||||
if (isPresenceMonitorEnabled()) {
|
||||
this.onEvent('watch.userSessions', async ({ clientAction, userSession }): Promise<void> => {
|
||||
if (clientAction === 'removed') {
|
||||
UserPresenceMonitor.processUserSession(
|
||||
{
|
||||
_id: userSession._id,
|
||||
connections: [
|
||||
{
|
||||
fake: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
'removed',
|
||||
);
|
||||
}
|
||||
|
||||
UserPresenceMonitor.processUserSession(userSession, minimongoChangeMap[clientAction]);
|
||||
});
|
||||
}
|
||||
|
||||
this.onEvent('watch.instanceStatus', async ({ clientAction, id, data }): Promise<void> => {
|
||||
if (clientAction === 'removed') {
|
||||
UserPresence.removeConnectionsByInstanceId(id);
|
||||
matrixBroadCastActions?.removed?.(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientAction === 'inserted') {
|
||||
if (data?.extraInformation?.port) {
|
||||
matrixBroadCastActions?.added?.(data);
|
||||
}
|
||||
if (clientAction === 'inserted' && data?.extraInformation?.port) {
|
||||
matrixBroadCastActions?.added?.(data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
20
apps/meteor/server/services/omnichannel/service.ts
Normal file
20
apps/meteor/server/services/omnichannel/service.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ServiceClassInternal } from '../../sdk/types/ServiceClass';
|
||||
import type { IOmnichannelService } from '../../sdk/types/IOmnichannelService';
|
||||
import { Livechat } from '../../../app/livechat/server';
|
||||
|
||||
export class OmnichannelService extends ServiceClassInternal implements IOmnichannelService {
|
||||
protected name = 'omnichannel';
|
||||
|
||||
async created() {
|
||||
this.onEvent('presence.status', async ({ user }): Promise<void> => {
|
||||
if (!user?._id) {
|
||||
return;
|
||||
}
|
||||
const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role));
|
||||
if (hasRole) {
|
||||
// TODO change `Livechat.notifyAgentStatusChanged` to a service call
|
||||
Livechat.notifyAgentStatusChanged(user._id, user.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { MongoInternals } from 'meteor/mongo';
|
||||
|
||||
import { AnalyticsService } from './analytics/service';
|
||||
import { api } from '../sdk/api';
|
||||
import { AppsEngineService } from './apps-engine/service';
|
||||
import { AuthorizationLivechat } from '../../app/livechat/server/roomAccessValidator.internalService';
|
||||
import { BannerService } from './banner/service';
|
||||
import { LDAPService } from './ldap/service';
|
||||
@ -12,6 +13,7 @@ import { RoomService } from './room/service';
|
||||
import { SAUMonitorService } from './sauMonitor/service';
|
||||
import { TeamService } from './team/service';
|
||||
import { UiKitCoreApp } from './uikit-core-app/service';
|
||||
import { OmnichannelService } from './omnichannel/service';
|
||||
import { OmnichannelVoipService } from './omnichannel-voip/service';
|
||||
import { VoipService } from './voip/service';
|
||||
import { VideoConfService } from './video-conference/service';
|
||||
@ -21,6 +23,7 @@ import { DeviceManagementService } from './device-management/service';
|
||||
|
||||
const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;
|
||||
|
||||
api.registerService(new AppsEngineService());
|
||||
api.registerService(new AnalyticsService());
|
||||
api.registerService(new AuthorizationLivechat());
|
||||
api.registerService(new BannerService());
|
||||
@ -31,6 +34,7 @@ api.registerService(new NPSService());
|
||||
api.registerService(new RoomService());
|
||||
api.registerService(new SAUMonitorService());
|
||||
api.registerService(new VoipService(db));
|
||||
api.registerService(new OmnichannelService());
|
||||
api.registerService(new OmnichannelVoipService());
|
||||
api.registerService(new TeamService());
|
||||
api.registerService(new UiKitCoreApp());
|
||||
@ -41,8 +45,11 @@ api.registerService(new VideoConfService());
|
||||
// if the process is running in micro services mode we don't need to register services that will run separately
|
||||
if (!isRunningMs()) {
|
||||
(async (): Promise<void> => {
|
||||
const { Presence } = await import('@rocket.chat/presence');
|
||||
|
||||
const { Authorization } = await import('./authorization/service');
|
||||
|
||||
api.registerService(new Presence());
|
||||
api.registerService(new Authorization(db));
|
||||
})();
|
||||
}
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import './migrations';
|
||||
import './watchDb';
|
||||
import './appcache';
|
||||
import './callbacks';
|
||||
import './cron';
|
||||
import './initialData';
|
||||
import './instance';
|
||||
import './presence';
|
||||
import './serverRunning';
|
||||
import './coreApps';
|
||||
import './presenceTroubleshoot';
|
||||
import '../hooks';
|
||||
import '../lib/rooms/roomTypes';
|
||||
import { isRunningMs } from '../lib/isRunningMs';
|
||||
|
||||
// only starts network broker if running in micro services mode
|
||||
if (!isRunningMs()) {
|
||||
require('./watchDb');
|
||||
require('./presence');
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { UserPresence } from 'meteor/konecty:user-presence';
|
||||
import { InstanceStatus, UsersSessions } from '@rocket.chat/models';
|
||||
|
||||
import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled';
|
||||
|
||||
Meteor.startup(function () {
|
||||
UserPresence.start();
|
||||
|
||||
if (!isPresenceMonitorEnabled()) {
|
||||
return;
|
||||
}
|
||||
// UserPresenceMonitor.start();
|
||||
|
||||
// Remove lost connections
|
||||
const ids = Promise.await(InstanceStatus.find({}, { projection: { _id: 1 } }).toArray()).map((id) => id._id);
|
||||
|
||||
Promise.await(UsersSessions.clearConnectionsFromInstanceId(ids));
|
||||
});
|
||||
35
apps/meteor/server/startup/presence.ts
Normal file
35
apps/meteor/server/startup/presence.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
|
||||
|
||||
import { Presence } from '../sdk';
|
||||
|
||||
Meteor.startup(function () {
|
||||
const nodeId = InstanceStatus.id();
|
||||
Meteor.onConnection(function (connection) {
|
||||
const session = Meteor.server.sessions.get(connection.id);
|
||||
|
||||
connection.onClose(function () {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
Presence.removeConnection(session.userId, connection.id, nodeId);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('exit', function () {
|
||||
Presence.removeLostConnections(nodeId);
|
||||
});
|
||||
|
||||
Accounts.onLogin(function (login: any): void {
|
||||
if (login.type !== 'resume') {
|
||||
return;
|
||||
}
|
||||
Presence.newConnection(login.user._id, login.connection.id, nodeId);
|
||||
});
|
||||
|
||||
Accounts.onLogout(function (login: any): void {
|
||||
Presence.removeConnection(login.user._id, login.connection.id, nodeId);
|
||||
});
|
||||
});
|
||||
11
apps/meteor/server/startup/presenceTroubleshoot.ts
Normal file
11
apps/meteor/server/startup/presenceTroubleshoot.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { settings } from '../../app/settings/server';
|
||||
import { Presence } from '../sdk';
|
||||
|
||||
// maybe this setting should disable the listener to 'presence.status' event on listerners.module.ts
|
||||
settings.watch('Troubleshoot_Disable_Presence_Broadcast', async function (value) {
|
||||
try {
|
||||
await Presence.toggleBroadcast(!value);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
@ -2,17 +2,14 @@ import { MongoInternals } from 'meteor/mongo';
|
||||
|
||||
import { DatabaseWatcher } from '../database/DatabaseWatcher';
|
||||
import { db } from '../database/utils';
|
||||
import { isRunningMs } from '../lib/isRunningMs';
|
||||
import { initWatchers } from '../modules/watchers/watchers.module';
|
||||
import { api } from '../sdk/api';
|
||||
import { metrics } from '../../app/metrics/server/lib/metrics';
|
||||
|
||||
if (!isRunningMs()) {
|
||||
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
|
||||
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
|
||||
|
||||
const watcher = new DatabaseWatcher({ db, _oplogHandle: (mongo as any)._oplogHandle, metrics });
|
||||
const watcher = new DatabaseWatcher({ db, _oplogHandle: (mongo as any)._oplogHandle, metrics });
|
||||
|
||||
initWatchers(watcher, api.broadcastLocal.bind(api));
|
||||
initWatchers(watcher, api.broadcastLocal.bind(api));
|
||||
|
||||
watcher.watch();
|
||||
}
|
||||
watcher.watch();
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { UserPresence } from 'meteor/konecty:user-presence';
|
||||
import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
|
||||
import { check } from 'meteor/check';
|
||||
import { DDP } from 'meteor/ddp';
|
||||
@ -10,9 +9,7 @@ import { Logger } from '../lib/logger/Logger';
|
||||
import { hasPermission } from '../../app/authorization/server';
|
||||
import { settings } from '../../app/settings/server';
|
||||
import { isDocker, getURL } from '../../app/utils/server';
|
||||
import { Users } from '../../app/models/server';
|
||||
import { StreamerCentral } from '../modules/streamer/streamer.module';
|
||||
import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled';
|
||||
|
||||
process.env.PORT = String(process.env.PORT).trim();
|
||||
process.env.INSTANCE_IP = String(process.env.INSTANCE_IP).trim();
|
||||
@ -57,13 +54,8 @@ function authorizeConnection(instance) {
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
const originalSetDefaultStatus = UserPresence.setDefaultStatus;
|
||||
export let matrixBroadCastActions;
|
||||
function startMatrixBroadcast() {
|
||||
if (!isPresenceMonitorEnabled()) {
|
||||
UserPresence.setDefaultStatus = originalSetDefaultStatus;
|
||||
}
|
||||
|
||||
matrixBroadCastActions = {
|
||||
added: Meteor.bindEnvironment((record) => {
|
||||
cache.set(record._id, record);
|
||||
@ -156,12 +148,6 @@ function startStreamCastBroadcast(value) {
|
||||
|
||||
connLogger.info({ msg: 'connecting in', instance, value });
|
||||
|
||||
if (!isPresenceMonitorEnabled()) {
|
||||
UserPresence.setDefaultStatus = (id, status) => {
|
||||
Users.updateDefaultStatus(id, status);
|
||||
};
|
||||
}
|
||||
|
||||
const connection = DDP.connect(value, {
|
||||
_dontPrintErrors: settings.get('Log_Level') !== '2',
|
||||
});
|
||||
|
||||
@ -56,9 +56,9 @@ services:
|
||||
|
||||
presence-service:
|
||||
build:
|
||||
dockerfile: apps/meteor/ee/server/services/Dockerfile
|
||||
dockerfile: ee/apps/presence-service/Dockerfile
|
||||
args:
|
||||
SERVICE: presence
|
||||
SERVICE: presence-service
|
||||
image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG}
|
||||
environment:
|
||||
- MONGO_URL=${MONGO_URL}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"description": "Rocket.Chat DDP-Streamer service",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
@ -47,6 +48,7 @@
|
||||
"@types/ws": "^8.5.3",
|
||||
"eslint": "^8.21.0",
|
||||
"pino-pretty": "^7.6.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.5.5"
|
||||
},
|
||||
"main": "./dist/service.js",
|
||||
|
||||
@ -6,8 +6,9 @@ import WebSocket from 'ws';
|
||||
|
||||
import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module';
|
||||
import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module';
|
||||
import { MeteorService } from '../../../../apps/meteor/server/sdk';
|
||||
import { MeteorService, Presence } from '../../../../apps/meteor/server/sdk';
|
||||
import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass';
|
||||
import { api } from '../../../../apps/meteor/server/sdk/api';
|
||||
import { Client } from './Client';
|
||||
import { events, server } from './configureServer';
|
||||
import { DDP_EVENTS } from './constants';
|
||||
@ -104,7 +105,7 @@ export class DDPStreamer extends ServiceClass {
|
||||
}
|
||||
|
||||
const { broker, nodeID } = this.context;
|
||||
if (!broker) {
|
||||
if (!broker || !nodeID) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -141,5 +142,38 @@ export class DDPStreamer extends ServiceClass {
|
||||
metrics.decrement('users_logged', { nodeID }, 1);
|
||||
}
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.LOGGED, (info) => {
|
||||
const { userId, connection } = info;
|
||||
|
||||
Presence.newConnection(userId, connection.id, nodeID);
|
||||
api.broadcast('accounts.login', { userId, connection });
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.LOGGEDOUT, (info) => {
|
||||
const { userId, connection } = info;
|
||||
|
||||
api.broadcast('accounts.logout', { userId, connection });
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
Presence.removeConnection(userId, connection.id, nodeID);
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.DISCONNECTED, (info) => {
|
||||
const { userId, connection } = info;
|
||||
|
||||
api.broadcast('socket.disconnected', connection);
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
Presence.removeConnection(userId, connection.id, nodeID);
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.CONNECTED, ({ connection }) => {
|
||||
api.broadcast('socket.connected', connection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import { DDP_EVENTS, WS_ERRORS } from './constants';
|
||||
import { Account, Presence, MeteorService } from '../../../../apps/meteor/server/sdk';
|
||||
import { Server } from './Server';
|
||||
import { api } from '../../../../apps/meteor/server/sdk/api';
|
||||
import { MeteorError } from '../../../../apps/meteor/server/sdk/errors';
|
||||
import { Autoupdate } from './lib/Autoupdate';
|
||||
|
||||
@ -74,6 +73,7 @@ server.methods({
|
||||
|
||||
this.userId = result.uid;
|
||||
this.userToken = result.hashedToken;
|
||||
this.connection.loginToken = result.hashedToken;
|
||||
|
||||
this.emit(DDP_EVENTS.LOGGED);
|
||||
|
||||
@ -152,36 +152,3 @@ server.methods({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.LOGGED, (info) => {
|
||||
const { userId, connection } = info;
|
||||
|
||||
Presence.newConnection(userId, connection.id);
|
||||
api.broadcast('accounts.login', { userId, connection });
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.LOGGEDOUT, (info) => {
|
||||
const { userId, connection } = info;
|
||||
|
||||
api.broadcast('accounts.logout', { userId, connection });
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
Presence.removeConnection(userId, connection.id);
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.DISCONNECTED, (info) => {
|
||||
const { userId, connection } = info;
|
||||
|
||||
api.broadcast('socket.disconnected', connection);
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
Presence.removeConnection(userId, connection.id);
|
||||
});
|
||||
|
||||
server.on(DDP_EVENTS.CONNECTED, ({ connection }) => {
|
||||
api.broadcast('socket.connected', connection);
|
||||
});
|
||||
|
||||
16
ee/apps/presence-service/.eslintrc
Normal file
16
ee/apps/presence-service/.eslintrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": ["@rocket.chat/eslint-config"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.spec.js", "**/*.spec.jsx"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["**/dist"],
|
||||
"plugins": ["jest"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
||||
38
ee/apps/presence-service/Dockerfile
Normal file
38
ee/apps/presence-service/Dockerfile
Normal file
@ -0,0 +1,38 @@
|
||||
FROM node:14.19.3-alpine
|
||||
|
||||
ARG SERVICE
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./packages/presence/package.json packages/presence/package.json
|
||||
COPY ./packages/presence/dist packages/presence/dist
|
||||
COPY ./packages/core-typings/package.json packages/core-typings/package.json
|
||||
COPY ./packages/core-typings/dist packages/core-typings/dist
|
||||
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json
|
||||
COPY ./packages/rest-typings/dist packages/rest-typings/dist
|
||||
COPY ./packages/model-typings/package.json packages/model-typings/package.json
|
||||
COPY ./packages/model-typings/dist packages/model-typings/dist
|
||||
COPY ./packages/models/package.json packages/models/package.json
|
||||
COPY ./packages/models/dist packages/models/dist
|
||||
COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json
|
||||
COPY ./packages/ui-contexts/dist packages/ui-contexts/dist
|
||||
|
||||
COPY ./ee/apps/${SERVICE}/dist .
|
||||
|
||||
COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./.yarn/plugins .yarn/plugins
|
||||
COPY ./.yarn/releases .yarn/releases
|
||||
COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
PORT=3000
|
||||
|
||||
WORKDIR /app/ee/apps/${SERVICE}
|
||||
|
||||
RUN yarn workspaces focus --production
|
||||
|
||||
EXPOSE 3000 9458
|
||||
|
||||
CMD ["node", "src/service.js"]
|
||||
43
ee/apps/presence-service/package.json
Normal file
43
ee/apps/presence-service/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@rocket.chat/presence-service",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Rocket.Chat Presence service",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"keywords": [
|
||||
"rocketchat"
|
||||
],
|
||||
"author": "Rocket.Chat",
|
||||
"dependencies": {
|
||||
"@rocket.chat/emitter": "next",
|
||||
"@rocket.chat/presence": "workspace:^",
|
||||
"@rocket.chat/string-helpers": "next",
|
||||
"@types/node": "^14.18.21",
|
||||
"ejson": "^2.2.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"fibers": "^5.0.1",
|
||||
"moleculer": "^0.14.21",
|
||||
"mongodb": "^4.3.1",
|
||||
"nats": "^2.4.0",
|
||||
"pino": "^8.4.2",
|
||||
"polka": "^0.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rocket.chat/eslint-config": "workspace:^",
|
||||
"@types/eslint": "^8",
|
||||
"@types/polka": "^0.5.4",
|
||||
"eslint": "^8.21.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.5.5"
|
||||
},
|
||||
"main": "./dist/presence/src/Presence.js",
|
||||
"files": [
|
||||
"/dist"
|
||||
]
|
||||
}
|
||||
28
ee/apps/presence-service/src/service.ts
Executable file
28
ee/apps/presence-service/src/service.ts
Executable file
@ -0,0 +1,28 @@
|
||||
import type { Document } from 'mongodb';
|
||||
import polka from 'polka';
|
||||
|
||||
import '../../../../apps/meteor/ee/server/startup/broker';
|
||||
|
||||
import { api } from '../../../../apps/meteor/server/sdk/api';
|
||||
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
|
||||
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
|
||||
|
||||
const PORT = process.env.PORT || 3031;
|
||||
|
||||
getConnection().then(async (db) => {
|
||||
const trash = await getCollection<Document>(Collections.Trash);
|
||||
|
||||
registerServiceModels(db, trash);
|
||||
|
||||
// need to import Presence service after models are registered
|
||||
const { Presence } = await import('@rocket.chat/presence');
|
||||
|
||||
api.registerService(new Presence());
|
||||
|
||||
polka()
|
||||
.get('/health', async function (_req, res) {
|
||||
await api.nodeList();
|
||||
res.end('ok');
|
||||
})
|
||||
.listen(PORT);
|
||||
});
|
||||
31
ee/apps/presence-service/tsconfig.json
Normal file
31
ee/apps/presence-service/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"lib": ["esnext", "dom"],
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"incremental": true,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictFunctionTypes": false,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"outDir": "./dist",
|
||||
"importsNotUsedAsValues": "preserve",
|
||||
"declaration": false,
|
||||
"declarationMap": false
|
||||
},
|
||||
"files": ["./src/service.ts"],
|
||||
"include": ["../../../apps/meteor/definition"],
|
||||
"exclude": ["./dist"]
|
||||
}
|
||||
@ -3,6 +3,7 @@ import type { IncomingHttpHeaders } from 'http';
|
||||
export interface ISocketConnection {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
loginToken?: string;
|
||||
livechatToken?: string;
|
||||
onClose(fn: (...args: any[]) => void): void;
|
||||
clientAddress: string | undefined;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Document, UpdateResult, FindCursor, FindOptions } from 'mongodb';
|
||||
import type { IUser, IRole, IRoom, ILivechatAgent } from '@rocket.chat/core-typings';
|
||||
import type { IUser, IRole, IRoom, ILivechatAgent, UserStatus } from '@rocket.chat/core-typings';
|
||||
|
||||
import type { FindPaginated, IBaseModel } from './IBaseModel';
|
||||
|
||||
@ -153,4 +153,14 @@ export interface IUsersModel extends IBaseModel<IUser> {
|
||||
removeRoomByRoomId(rid: any): any;
|
||||
|
||||
findOneByResetToken(token: string, options: FindOptions<IUser>): Promise<IUser | null>;
|
||||
|
||||
updateStatusById(
|
||||
userId: string,
|
||||
{
|
||||
statusDefault,
|
||||
status,
|
||||
statusConnection,
|
||||
statusText,
|
||||
}: { statusDefault?: string; status: UserStatus; statusConnection: UserStatus; statusText?: string },
|
||||
): Promise<UpdateResult>;
|
||||
}
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
import type { IUserSession } from '@rocket.chat/core-typings';
|
||||
import type { FindCursor, FindOptions } from 'mongodb';
|
||||
import type { IUserSession, IUserSessionConnection } from '@rocket.chat/core-typings';
|
||||
|
||||
import type { IBaseModel } from './IBaseModel';
|
||||
|
||||
export interface IUsersSessionsModel extends IBaseModel<IUserSession> {
|
||||
clearConnectionsFromInstanceId(instanceId: string[]): ReturnType<IBaseModel<IUserSession>['updateMany']>;
|
||||
updateConnectionStatusById(uid: string, connectionId: string, status: string): ReturnType<IBaseModel<IUserSession>['updateOne']>;
|
||||
removeConnectionsFromInstanceId(instanceId: string): ReturnType<IBaseModel<IUserSession>['updateMany']>;
|
||||
removeConnectionByConnectionId(connectionId: string): ReturnType<IBaseModel<IUserSession>['updateMany']>;
|
||||
findByInstanceId(instanceId: string): FindCursor<IUserSession>;
|
||||
addConnectionById(
|
||||
userId: string,
|
||||
{ id, instanceId, status }: Pick<IUserSessionConnection, 'id' | 'instanceId' | 'status'>,
|
||||
): ReturnType<IBaseModel<IUserSession>['updateOne']>;
|
||||
findByOtherInstanceIds(instanceIds: string[], options?: FindOptions<IUserSession>): FindCursor<IUserSession>;
|
||||
removeConnectionsFromOtherInstanceIds(instanceIds: string[]): ReturnType<IBaseModel<IUserSession>['updateMany']>;
|
||||
}
|
||||
|
||||
@ -7,7 +7,10 @@ function handler<T extends object>(namespace: string): ProxyHandler<T> {
|
||||
return {
|
||||
get: (_target: T, prop: string): any => {
|
||||
if (!models.has(namespace) && lazyModels.has(namespace)) {
|
||||
models.set(namespace, (lazyModels.get(namespace) as () => IBaseModel<any>)());
|
||||
const getModel = lazyModels.get(namespace);
|
||||
if (getModel) {
|
||||
models.set(namespace, getModel());
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
12
packages/presence/.eslintrc
Normal file
12
packages/presence/.eslintrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": ["@rocket.chat/eslint-config"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.spec.js", "**/*.spec.jsx"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["**/dist"]
|
||||
}
|
||||
1
packages/presence/.gitignore
vendored
Normal file
1
packages/presence/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.nyc_output
|
||||
3
packages/presence/babel.config.js
Normal file
3
packages/presence/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
|
||||
};
|
||||
37
packages/presence/package.json
Normal file
37
packages/presence/package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@rocket.chat/presence",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.1",
|
||||
"@babel/preset-env": "^7.19.1",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@rocket.chat/apps-engine": "^1.32.0",
|
||||
"@rocket.chat/eslint-config": "workspace:^",
|
||||
"@rocket.chat/rest-typings": "workspace:^",
|
||||
"@rocket.chat/ui-contexts": "workspace:^",
|
||||
"@types/node": "^14.18.21",
|
||||
"babel-jest": "^29.0.3",
|
||||
"eslint": "^8.21.0",
|
||||
"jest": "^29.0.3",
|
||||
"typescript": "~4.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"jest": "jest",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"testunit": "jest tests/**/*.test.ts",
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"main": "./dist/packages/presence/src/Presence.js",
|
||||
"typings": "./dist/packages/presence/src/Presence.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@rocket.chat/core-typings": "workspace:^",
|
||||
"@rocket.chat/models": "workspace:^",
|
||||
"mongodb": "^4.3.1"
|
||||
}
|
||||
}
|
||||
189
packages/presence/src/Presence.ts
Executable file
189
packages/presence/src/Presence.ts
Executable file
@ -0,0 +1,189 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import { Users, UsersSessions } from '@rocket.chat/models';
|
||||
|
||||
import { processPresenceAndStatus } from './lib/processConnectionStatus';
|
||||
import type { IPresence } from '../../../apps/meteor/server/sdk/types/IPresence';
|
||||
import type { IBrokerNode } from '../../../apps/meteor/server/sdk/types/IBroker';
|
||||
import { ServiceClass } from '../../../apps/meteor/server/sdk/types/ServiceClass';
|
||||
|
||||
export class Presence extends ServiceClass implements IPresence {
|
||||
protected name = 'presence';
|
||||
|
||||
private broadcastEnabled = true;
|
||||
|
||||
private lostConTimeout?: NodeJS.Timeout;
|
||||
|
||||
async onNodeDisconnected({ node }: { node: IBrokerNode }): Promise<void> {
|
||||
const affectedUsers = await this.removeLostConnections(node.id);
|
||||
return affectedUsers.forEach((uid) => this.updateUserPresence(uid));
|
||||
}
|
||||
|
||||
async created(): Promise<void> {
|
||||
this.onEvent('watch.instanceStatus', async ({ clientAction, id }): Promise<void> => {
|
||||
if (clientAction !== 'removed') {
|
||||
return;
|
||||
}
|
||||
|
||||
const affectedUsers = await this.removeLostConnections(id);
|
||||
return affectedUsers.forEach((uid) => this.updateUserPresence(uid));
|
||||
});
|
||||
}
|
||||
|
||||
async started(): Promise<void> {
|
||||
this.lostConTimeout = setTimeout(async () => {
|
||||
const affectedUsers = await this.removeLostConnections();
|
||||
return affectedUsers.forEach((uid) => this.updateUserPresence(uid));
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async stopped(): Promise<void> {
|
||||
if (!this.lostConTimeout) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.lostConTimeout);
|
||||
}
|
||||
|
||||
toggleBroadcast(enabled: boolean): void {
|
||||
this.broadcastEnabled = enabled;
|
||||
}
|
||||
|
||||
async newConnection(
|
||||
uid: string | undefined,
|
||||
session: string | undefined,
|
||||
nodeId: string,
|
||||
): Promise<{ uid: string; connectionId: string } | undefined> {
|
||||
if (!uid || !session) {
|
||||
return;
|
||||
}
|
||||
|
||||
await UsersSessions.addConnectionById(uid, {
|
||||
id: session,
|
||||
instanceId: nodeId,
|
||||
status: UserStatus.ONLINE,
|
||||
});
|
||||
|
||||
await this.updateUserPresence(uid);
|
||||
return {
|
||||
uid,
|
||||
connectionId: session,
|
||||
};
|
||||
}
|
||||
|
||||
async removeConnection(uid: string | undefined, session: string | undefined): Promise<{ uid: string; session: string } | undefined> {
|
||||
if (!uid || !session) {
|
||||
return;
|
||||
}
|
||||
await UsersSessions.removeConnectionByConnectionId(session);
|
||||
|
||||
await this.updateUserPresence(uid);
|
||||
|
||||
return {
|
||||
uid,
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
async removeLostConnections(nodeID?: string): Promise<string[]> {
|
||||
if (nodeID) {
|
||||
const affectedUsers = await UsersSessions.findByInstanceId(nodeID).toArray();
|
||||
|
||||
const { modifiedCount } = await UsersSessions.removeConnectionsFromInstanceId(nodeID);
|
||||
if (modifiedCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return affectedUsers.map(({ _id }) => _id);
|
||||
}
|
||||
|
||||
const nodes = await this.api.nodeList();
|
||||
|
||||
const ids = nodes.filter((node) => node.available).map(({ id }) => id);
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const affectedUsers = await UsersSessions.findByOtherInstanceIds(ids, { projection: { _id: 1 } }).toArray();
|
||||
|
||||
const { modifiedCount } = await UsersSessions.removeConnectionsFromOtherInstanceIds(ids);
|
||||
if (modifiedCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return affectedUsers.map(({ _id }) => _id);
|
||||
}
|
||||
|
||||
async setStatus(uid: string, statusDefault: UserStatus, statusText?: string): Promise<boolean> {
|
||||
const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] };
|
||||
|
||||
const user = await Users.findOneById<Pick<IUser, 'username' | 'roles' | 'status'>>(uid, {
|
||||
projection: { username: 1, roles: 1, status: 1 },
|
||||
});
|
||||
|
||||
const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault);
|
||||
|
||||
const result = await Users.updateStatusById(uid, {
|
||||
statusDefault,
|
||||
status,
|
||||
statusConnection,
|
||||
statusText,
|
||||
});
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
this.broadcast({ _id: uid, username: user?.username, status, statusText, roles: user?.roles || [] }, user?.status);
|
||||
}
|
||||
|
||||
return !!result.modifiedCount;
|
||||
}
|
||||
|
||||
async setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean> {
|
||||
const result = await UsersSessions.updateConnectionStatusById(uid, session, status);
|
||||
|
||||
await this.updateUserPresence(uid);
|
||||
|
||||
return !!result.modifiedCount;
|
||||
}
|
||||
|
||||
async updateUserPresence(uid: string): Promise<void> {
|
||||
const user = await Users.findOneById<Pick<IUser, 'username' | 'statusDefault' | 'statusText' | 'roles' | 'status'>>(uid, {
|
||||
projection: {
|
||||
username: 1,
|
||||
statusDefault: 1,
|
||||
statusText: 1,
|
||||
roles: 1,
|
||||
status: 1,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] };
|
||||
|
||||
const { statusDefault } = user;
|
||||
|
||||
const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault);
|
||||
|
||||
const result = await Users.updateStatusById(uid, {
|
||||
status,
|
||||
statusConnection,
|
||||
});
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
this.broadcast({ _id: uid, username: user.username, status, statusText: user.statusText, roles: user.roles }, user.status);
|
||||
}
|
||||
}
|
||||
|
||||
private broadcast(
|
||||
user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'roles'>,
|
||||
previousStatus: UserStatus | undefined,
|
||||
): void {
|
||||
if (!this.broadcastEnabled) {
|
||||
return;
|
||||
}
|
||||
this.api.broadcast('presence.status', {
|
||||
user,
|
||||
previousStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,51 +1,47 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, expect, test } from '@jest/globals';
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
|
||||
import {
|
||||
processConnectionStatus,
|
||||
processStatus,
|
||||
processPresenceAndStatus,
|
||||
} from '../../../../../../server/services/presence/lib/processConnectionStatus';
|
||||
import { processConnectionStatus, processStatus, processPresenceAndStatus } from '../../src/lib/processConnectionStatus';
|
||||
|
||||
describe('Presence micro service', () => {
|
||||
it('should return connection as online when there is a connection online', () => {
|
||||
expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.BUSY, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.AWAY, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE);
|
||||
test('should return connection as online when there is a connection online', () => {
|
||||
expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.BUSY, UserStatus.ONLINE)).toBe(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.AWAY, UserStatus.ONLINE)).toBe(UserStatus.ONLINE);
|
||||
});
|
||||
|
||||
it('should return the connections status if the other connection is offline', () => {
|
||||
expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE);
|
||||
expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).to.equal(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.AWAY, UserStatus.OFFLINE)).to.equal(UserStatus.AWAY);
|
||||
test('should return the connections status if the other connection is offline', () => {
|
||||
expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE);
|
||||
expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).toBe(UserStatus.ONLINE);
|
||||
expect(processConnectionStatus(UserStatus.AWAY, UserStatus.OFFLINE)).toBe(UserStatus.AWAY);
|
||||
});
|
||||
|
||||
it('should return the connection status when the default status is online', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.ONLINE)).to.equal(UserStatus.AWAY);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).to.equal(UserStatus.OFFLINE);
|
||||
test('should return the connection status when the default status is online', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.ONLINE)).toBe(UserStatus.AWAY);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).toBe(UserStatus.OFFLINE);
|
||||
});
|
||||
|
||||
it('should return status busy when the default status is busy', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.BUSY)).to.equal(UserStatus.BUSY);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.BUSY)).to.equal(UserStatus.BUSY);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.BUSY)).to.equal(UserStatus.OFFLINE);
|
||||
test('should return status busy when the default status is busy', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.BUSY)).toBe(UserStatus.BUSY);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.BUSY)).toBe(UserStatus.BUSY);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.BUSY)).toBe(UserStatus.OFFLINE);
|
||||
});
|
||||
|
||||
it('should return status away when the default status is away', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.AWAY)).to.equal(UserStatus.AWAY);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.AWAY)).to.equal(UserStatus.AWAY);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.AWAY)).to.equal(UserStatus.OFFLINE);
|
||||
test('should return status away when the default status is away', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.AWAY)).toBe(UserStatus.AWAY);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.AWAY)).toBe(UserStatus.AWAY);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.AWAY)).toBe(UserStatus.OFFLINE);
|
||||
});
|
||||
|
||||
it('should return status offline when the default status is offline', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE);
|
||||
test('should return status offline when the default status is offline', () => {
|
||||
expect(processStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE);
|
||||
expect(processStatus(UserStatus.AWAY, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE);
|
||||
expect(processStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE);
|
||||
});
|
||||
|
||||
it('should return correct status and statusConnection when connected once', () => {
|
||||
test('should return correct status and statusConnection when connected once', () => {
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
[
|
||||
@ -59,7 +55,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.ONLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE });
|
||||
).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -74,7 +70,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.ONLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY });
|
||||
).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -89,7 +85,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.BUSY,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.BUSY, statusConnection: UserStatus.ONLINE });
|
||||
).toStrictEqual({ status: UserStatus.BUSY, statusConnection: UserStatus.ONLINE });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -104,7 +100,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.AWAY,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.AWAY, statusConnection: UserStatus.ONLINE });
|
||||
).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.ONLINE });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -119,7 +115,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.BUSY,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.BUSY, statusConnection: UserStatus.AWAY });
|
||||
).toStrictEqual({ status: UserStatus.BUSY, statusConnection: UserStatus.AWAY });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -134,7 +130,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.OFFLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.OFFLINE, statusConnection: UserStatus.ONLINE });
|
||||
).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.ONLINE });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -149,10 +145,10 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.OFFLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.OFFLINE, statusConnection: UserStatus.AWAY });
|
||||
).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.AWAY });
|
||||
});
|
||||
|
||||
it('should return correct status and statusConnection when connected twice', () => {
|
||||
test('should return correct status and statusConnection when connected twice', () => {
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
[
|
||||
@ -173,7 +169,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.ONLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE });
|
||||
).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -195,7 +191,7 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.ONLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE });
|
||||
).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE });
|
||||
|
||||
expect(
|
||||
processPresenceAndStatus(
|
||||
@ -217,26 +213,26 @@ describe('Presence micro service', () => {
|
||||
],
|
||||
UserStatus.ONLINE,
|
||||
),
|
||||
).to.deep.equal({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY });
|
||||
).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY });
|
||||
});
|
||||
|
||||
it('should return correct status and statusConnection when not connected', () => {
|
||||
expect(processPresenceAndStatus([], UserStatus.ONLINE)).to.deep.equal({
|
||||
test('should return correct status and statusConnection when not connected', () => {
|
||||
expect(processPresenceAndStatus([], UserStatus.ONLINE)).toStrictEqual({
|
||||
status: UserStatus.OFFLINE,
|
||||
statusConnection: UserStatus.OFFLINE,
|
||||
});
|
||||
|
||||
expect(processPresenceAndStatus([], UserStatus.BUSY)).to.deep.equal({
|
||||
expect(processPresenceAndStatus([], UserStatus.BUSY)).toStrictEqual({
|
||||
status: UserStatus.OFFLINE,
|
||||
statusConnection: UserStatus.OFFLINE,
|
||||
});
|
||||
|
||||
expect(processPresenceAndStatus([], UserStatus.AWAY)).to.deep.equal({
|
||||
expect(processPresenceAndStatus([], UserStatus.AWAY)).toStrictEqual({
|
||||
status: UserStatus.OFFLINE,
|
||||
statusConnection: UserStatus.OFFLINE,
|
||||
});
|
||||
|
||||
expect(processPresenceAndStatus([], UserStatus.OFFLINE)).to.deep.equal({
|
||||
expect(processPresenceAndStatus([], UserStatus.OFFLINE)).toStrictEqual({
|
||||
status: UserStatus.OFFLINE,
|
||||
statusConnection: UserStatus.OFFLINE,
|
||||
});
|
||||
30
packages/presence/tsconfig.json
Normal file
30
packages/presence/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"lib": ["esnext", "dom"],
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"incremental": true,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"outDir": "./dist",
|
||||
"importsNotUsedAsValues": "preserve",
|
||||
// "declaration": false,
|
||||
"declarationMap": false
|
||||
},
|
||||
"include": ["../../apps/meteor/definition/externals/meteor/rocketchat-streamer.d.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"files": ["./src/Presence.ts"]
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToken } from '@rocket.chat/core-typings';
|
||||
import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToken, UserStatus } from '@rocket.chat/core-typings';
|
||||
import Ajv from 'ajv';
|
||||
|
||||
import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST';
|
||||
@ -268,7 +268,7 @@ export type UsersEndpoints = {
|
||||
};
|
||||
|
||||
'/v1/users.setStatus': {
|
||||
POST: (params: { message?: string; status?: 'online' | 'offline' | 'away' | 'busy' }) => void;
|
||||
POST: (params: { message?: string; status?: UserStatus }) => void;
|
||||
};
|
||||
|
||||
'/v1/users.getStatus': {
|
||||
|
||||
@ -36,6 +36,9 @@
|
||||
"dependsOn": ["build"],
|
||||
"cache": false
|
||||
},
|
||||
"ms": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"@rocket.chat/ui-contexts#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false
|
||||
|
||||
Loading…
Reference in New Issue
Block a user