[NEW][Enterprise] Micro services (#19000)
Co-authored-by: Rodrigo Nascimento <rodrigoknascimento@gmail.com> Co-authored-by: Alan Sikora <alansikora@gmail.com>pull/19338/head
parent
3995eec349
commit
92e02f24c9
@ -0,0 +1 @@ |
||||
ee/server/services |
||||
@ -1,40 +0,0 @@ |
||||
import { hasPermissionAsync } from './hasPermission'; |
||||
import { Subscriptions } from '../../../models/server/raw'; |
||||
import { getValue } from '../../../settings/server/raw'; |
||||
|
||||
export const roomAccessValidators = [ |
||||
async function(room, user = {}) { |
||||
if (room && room.t === 'c') { |
||||
const anonymous = await getValue('Accounts_AllowAnonymousRead'); |
||||
if (!user._id && anonymous === true) { |
||||
return true; |
||||
} |
||||
|
||||
return hasPermissionAsync(user._id, 'view-c-room'); |
||||
} |
||||
}, |
||||
async function(room, user) { |
||||
if (!room || !user) { |
||||
return; |
||||
} |
||||
|
||||
const exists = await Subscriptions.countByRoomIdAndUserId(room._id, user._id); |
||||
if (exists) { |
||||
return true; |
||||
} |
||||
}, |
||||
]; |
||||
|
||||
export const canAccessRoomAsync = async (room, user, extraData) => { |
||||
for (let i = 0, total = roomAccessValidators.length; i < total; i++) { |
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const permitted = await roomAccessValidators[i](room, user, extraData); |
||||
if (permitted) { |
||||
return true; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
export const canAccessRoom = (room, user, extraData) => Promise.await(canAccessRoomAsync(room, user, extraData)); |
||||
|
||||
export const addRoomAccessValidator = (validator) => roomAccessValidators.push(validator.bind(this)); |
||||
@ -0,0 +1,8 @@ |
||||
import { Promise } from 'meteor/promise'; |
||||
|
||||
import { Authorization } from '../../../../server/sdk'; |
||||
import { IAuthorization } from '../../../../server/sdk/types/IAuthorization'; |
||||
|
||||
export const canAccessRoomAsync = Authorization.canAccessRoom; |
||||
|
||||
export const canAccessRoom = (...args: Parameters<IAuthorization['canAccessRoom']>): boolean => Promise.await(canAccessRoomAsync(...args)); |
||||
@ -1,5 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
export const rolesStreamer = new Meteor.Streamer('roles'); |
||||
rolesStreamer.allowWrite('none'); |
||||
rolesStreamer.allowRead('logged'); |
||||
@ -1,45 +0,0 @@ |
||||
import Settings from '../../../../models/server/models/Settings'; |
||||
import { Notifications } from '../../../../notifications/server'; |
||||
import { CONSTANTS } from '../../../lib'; |
||||
import Permissions from '../../../../models/server/models/Permissions'; |
||||
import { clearCache } from '../../functions/hasPermission'; |
||||
|
||||
Permissions.on('change', ({ clientAction, id, data, diff }) => { |
||||
if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { |
||||
// avoid useless changes
|
||||
return; |
||||
} |
||||
switch (clientAction) { |
||||
case 'updated': |
||||
case 'inserted': |
||||
data = data ?? Permissions.findOneById(id); |
||||
break; |
||||
|
||||
case 'removed': |
||||
data = { _id: id }; |
||||
break; |
||||
} |
||||
|
||||
clearCache(); |
||||
|
||||
Notifications.notifyLoggedInThisInstance( |
||||
'permissions-changed', |
||||
clientAction, |
||||
data, |
||||
); |
||||
|
||||
if (data.level && data.level === CONSTANTS.SETTINGS_LEVEL) { |
||||
// if the permission changes, the effect on the visible settings depends on the role affected.
|
||||
// The selected-settings-based consumers have to react accordingly and either add or remove the
|
||||
// setting from the user's collection
|
||||
const setting = Settings.findOneNotHiddenById(data.settingId); |
||||
if (!setting) { |
||||
return; |
||||
} |
||||
Notifications.notifyLoggedInThisInstance( |
||||
'private-settings-changed', |
||||
'updated', |
||||
setting, |
||||
); |
||||
} |
||||
}); |
||||
@ -1,10 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { addRoomAccessValidator, canAccessRoom } from '../../authorization'; |
||||
import { Rooms } from '../../models'; |
||||
|
||||
Meteor.startup(() => { |
||||
addRoomAccessValidator(function(room, user) { |
||||
return room && room.prid && canAccessRoom(Rooms.findOne(room.prid), user); |
||||
}); |
||||
}); |
||||
@ -1,30 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { hasAtLeastOnePermission } from '../../authorization/server'; |
||||
import { IntegrationHistory } from '../../models/server'; |
||||
|
||||
export const integrationHistoryStreamer = new Meteor.Streamer('integrationHistory'); |
||||
integrationHistoryStreamer.allowWrite('none'); |
||||
integrationHistoryStreamer.allowRead(function() { |
||||
return this.userId && hasAtLeastOnePermission(this.userId, [ |
||||
'manage-outgoing-integrations', |
||||
'manage-own-outgoing-integrations', |
||||
]); |
||||
}); |
||||
|
||||
IntegrationHistory.on('change', ({ clientAction, id, data, diff }) => { |
||||
switch (clientAction) { |
||||
case 'updated': { |
||||
const history = IntegrationHistory.findOneById(id, { fields: { 'integration._id': 1 } }); |
||||
if (!history && !history.integration) { |
||||
return; |
||||
} |
||||
integrationHistoryStreamer.emit(history.integration._id, { id, diff, type: clientAction }); |
||||
break; |
||||
} |
||||
case 'inserted': { |
||||
integrationHistoryStreamer.emit(data.integration._id, { data, type: clientAction }); |
||||
break; |
||||
} |
||||
} |
||||
}); |
||||
@ -1,57 +1,3 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { DDPCommon } from 'meteor/ddp-common'; |
||||
import notifications from '../../../notifications/server/lib/Notifications'; |
||||
|
||||
const changedPayload = function(collection, id, fields) { |
||||
return DDPCommon.stringifyDDP({ |
||||
msg: 'changed', |
||||
collection, |
||||
id, |
||||
fields, |
||||
}); |
||||
}; |
||||
|
||||
const send = function(self, msg) { |
||||
if (!self.socket) { |
||||
return; |
||||
} |
||||
self.socket.send(msg); |
||||
}; |
||||
|
||||
class MessageStream extends Meteor.Streamer { |
||||
getSubscriptionByUserIdAndRoomId(userId, rid) { |
||||
return this.subscriptions.find((sub) => sub.eventName === rid && sub.subscription.userId === userId); |
||||
} |
||||
|
||||
_publish(publication, eventName, options) { |
||||
super._publish(publication, eventName, options); |
||||
const uid = Meteor.userId(); |
||||
|
||||
const userEvent = (clientAction, { rid }) => { |
||||
switch (clientAction) { |
||||
case 'removed': |
||||
this.removeListener(uid, userEvent); |
||||
this.removeSubscription(this.getSubscriptionByUserIdAndRoomId(uid, rid), eventName); |
||||
break; |
||||
} |
||||
}; |
||||
this.on(uid, userEvent); |
||||
} |
||||
|
||||
mymessage = (eventName, args) => { |
||||
const subscriptions = this.subscriptionsByEventName[eventName]; |
||||
if (!Array.isArray(subscriptions)) { |
||||
return; |
||||
} |
||||
subscriptions.forEach(({ subscription }) => { |
||||
const options = this.isEmitAllowed(subscription, eventName, args); |
||||
if (options) { |
||||
send(subscription._session, changedPayload(this.subscriptionName, 'id', { |
||||
eventName, |
||||
args: [args, options], |
||||
})); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export const msgStream = new MessageStream('room-messages'); |
||||
export const msgStream = notifications.streamRoomMessage; |
||||
|
||||
@ -1,104 +0,0 @@ |
||||
import { MongoInternals } from 'meteor/mongo'; |
||||
|
||||
import { Users } from '../../../models/server'; |
||||
import { Notifications } from '../../../notifications/server'; |
||||
import loginServiceConfiguration from '../../../models/server/models/LoginServiceConfiguration'; |
||||
|
||||
let processOnChange; |
||||
// eslint-disable-next-line no-undef
|
||||
const disableOplog = Package['disable-oplog']; |
||||
|
||||
if (disableOplog) { |
||||
// Stores the callbacks for the disconnection reactivity bellow
|
||||
const userCallbacks = new Map(); |
||||
const serviceConfigCallbacks = new Set(); |
||||
|
||||
// Overrides the native observe changes to prevent database polling and stores the callbacks
|
||||
// for the users' tokens to re-implement the reactivity based on our database listeners
|
||||
const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); |
||||
MongoInternals.Connection.prototype._observeChanges = function({ collectionName, selector, options = {} }, _ordered, callbacks) { |
||||
// console.error('Connection.Collection.prototype._observeChanges', collectionName, selector, options);
|
||||
let cbs; |
||||
if (callbacks?.added) { |
||||
const records = Promise.await(mongo.rawCollection(collectionName).find(selector, { projection: options.fields }).toArray()); |
||||
for (const { _id, ...fields } of records) { |
||||
callbacks.added(_id, fields); |
||||
} |
||||
|
||||
if (collectionName === 'users' && selector['services.resume.loginTokens.hashedToken']) { |
||||
cbs = userCallbacks.get(selector._id) || new Set(); |
||||
cbs.add({ |
||||
hashedToken: selector['services.resume.loginTokens.hashedToken'], |
||||
callbacks, |
||||
}); |
||||
userCallbacks.set(selector._id, cbs); |
||||
} |
||||
} |
||||
|
||||
if (collectionName === 'meteor_accounts_loginServiceConfiguration') { |
||||
serviceConfigCallbacks.add(callbacks); |
||||
} |
||||
|
||||
return { |
||||
stop() { |
||||
if (cbs) { |
||||
cbs.delete(callbacks); |
||||
} |
||||
serviceConfigCallbacks.delete(callbacks); |
||||
}, |
||||
}; |
||||
}; |
||||
|
||||
// Re-implement meteor's reactivity that uses observe to disconnect sessions when the token
|
||||
// associated was removed
|
||||
processOnChange = (diff, id) => { |
||||
const loginTokens = diff['services.resume.loginTokens']; |
||||
if (loginTokens) { |
||||
const tokens = loginTokens.map(({ hashedToken }) => hashedToken); |
||||
|
||||
const cbs = userCallbacks.get(id); |
||||
if (cbs) { |
||||
[...cbs].filter(({ hashedToken }) => !tokens.includes(hashedToken)).forEach((item) => { |
||||
item.callbacks.removed(id); |
||||
cbs.delete(item); |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
loginServiceConfiguration.on('change', ({ clientAction, id, data, diff }) => { |
||||
switch (clientAction) { |
||||
case 'inserted': |
||||
case 'updated': |
||||
const record = { ...data || diff }; |
||||
delete record.secret; |
||||
serviceConfigCallbacks.forEach((callbacks) => { |
||||
callbacks[clientAction === 'inserted' ? 'added' : 'changed']?.(id, record); |
||||
}); |
||||
break; |
||||
case 'removed': |
||||
serviceConfigCallbacks.forEach((callbacks) => { |
||||
callbacks.removed?.(id); |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
Users.on('change', ({ clientAction, id, data, diff, unset }) => { |
||||
switch (clientAction) { |
||||
case 'updated': |
||||
Notifications.notifyUserInThisInstance(id, 'userData', { diff, unset, type: clientAction }); |
||||
|
||||
if (disableOplog) { |
||||
processOnChange(diff, id); |
||||
} |
||||
|
||||
break; |
||||
case 'inserted': |
||||
Notifications.notifyUserInThisInstance(id, 'userData', { data, type: clientAction }); |
||||
break; |
||||
case 'removed': |
||||
Notifications.notifyUserInThisInstance(id, 'userData', { id, type: clientAction }); |
||||
break; |
||||
} |
||||
}); |
||||
@ -1,29 +0,0 @@ |
||||
import { LivechatDepartmentAgents } from '../../../../models/server'; |
||||
import { Notifications } from '../../../../notifications'; |
||||
|
||||
const fields = { agentId: 1, departmentId: 1 }; |
||||
|
||||
const emitNotification = (action, payload = {}) => { |
||||
const { agentId = null } = payload; |
||||
if (!agentId) { |
||||
return; |
||||
} |
||||
|
||||
Notifications.notifyUserInThisInstance(agentId, 'departmentAgentData', { |
||||
action, |
||||
...payload, |
||||
}); |
||||
}; |
||||
|
||||
LivechatDepartmentAgents.on('change', ({ clientAction, id }) => { |
||||
switch (clientAction) { |
||||
case 'inserted': |
||||
case 'updated': |
||||
emitNotification(clientAction, LivechatDepartmentAgents.findOneById(id, { fields })); |
||||
break; |
||||
|
||||
case 'removed': |
||||
emitNotification(clientAction, LivechatDepartmentAgents.trashFindOneById(id, { fields })); |
||||
break; |
||||
} |
||||
}); |
||||
@ -1,52 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { hasPermission } from '../../../../authorization/server'; |
||||
import { LivechatInquiry } from '../../../../models/server'; |
||||
import { LIVECHAT_INQUIRY_QUEUE_STREAM_OBSERVER } from '../../../lib/stream/constants'; |
||||
import { RoutingManager } from '../RoutingManager'; |
||||
|
||||
const queueDataStreamer = new Meteor.Streamer(LIVECHAT_INQUIRY_QUEUE_STREAM_OBSERVER); |
||||
queueDataStreamer.allowWrite('none'); |
||||
queueDataStreamer.allowRead(function() { |
||||
return this.userId ? hasPermission(this.userId, 'view-l-room') : false; |
||||
}); |
||||
|
||||
const emitQueueDataEvent = (event, data) => queueDataStreamer.emitWithoutBroadcast(event, data); |
||||
const mountDataToEmit = (type, data) => ({ type, ...data }); |
||||
|
||||
LivechatInquiry.on('change', ({ clientAction, id: _id, data: record }) => { |
||||
if (RoutingManager.getConfig().autoAssignAgent) { |
||||
return; |
||||
} |
||||
|
||||
switch (clientAction) { |
||||
case 'inserted': |
||||
emitQueueDataEvent(_id, { ...record, clientAction }); |
||||
if (record && record.department) { |
||||
return emitQueueDataEvent(`department/${ record.department }`, mountDataToEmit('added', record)); |
||||
} |
||||
emitQueueDataEvent('public', mountDataToEmit('added', record)); |
||||
break; |
||||
case 'updated': |
||||
const isUpdatingDepartment = record && record.department; |
||||
const updatedRecord = LivechatInquiry.findOneById(_id); |
||||
emitQueueDataEvent(_id, { ...updatedRecord, clientAction }); |
||||
if (updatedRecord && !updatedRecord.department) { |
||||
return emitQueueDataEvent('public', mountDataToEmit('changed', updatedRecord)); |
||||
} |
||||
if (isUpdatingDepartment) { |
||||
emitQueueDataEvent('public', mountDataToEmit('changed', updatedRecord)); |
||||
} |
||||
emitQueueDataEvent(`department/${ updatedRecord.department }`, mountDataToEmit('changed', updatedRecord)); |
||||
break; |
||||
|
||||
case 'removed': |
||||
const removedRecord = LivechatInquiry.trashFindOneById(_id); |
||||
emitQueueDataEvent(_id, { _id, clientAction }); |
||||
if (removedRecord && removedRecord.department) { |
||||
return emitQueueDataEvent(`department/${ removedRecord.department }`, mountDataToEmit('removed', { _id })); |
||||
} |
||||
emitQueueDataEvent('public', mountDataToEmit('removed', { _id })); |
||||
break; |
||||
} |
||||
}); |
||||
@ -0,0 +1,51 @@ |
||||
import { LivechatRooms } from '../../models'; |
||||
import { hasPermission, hasRole } from '../../authorization'; |
||||
import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry } from '../../models/server'; |
||||
import { RoutingManager } from './lib/RoutingManager'; |
||||
|
||||
export const validators = [ |
||||
function(room, user) { |
||||
return hasPermission(user._id, 'view-livechat-rooms'); |
||||
}, |
||||
function(room, user) { |
||||
const { _id: userId } = user; |
||||
const { servedBy: { _id: agentId } = {} } = room; |
||||
return userId === agentId || (!room.open && hasPermission(user._id, 'view-livechat-room-closed-by-another-agent')); |
||||
}, |
||||
function(room, user, extraData) { |
||||
if (extraData && extraData.rid) { |
||||
room = LivechatRooms.findOneById(extraData.rid); |
||||
} |
||||
return extraData && extraData.visitorToken && room.v && room.v.token === extraData.visitorToken; |
||||
}, |
||||
function(room, user) { |
||||
const { previewRoom } = RoutingManager.getConfig(); |
||||
if (!previewRoom) { |
||||
return; |
||||
} |
||||
|
||||
let departmentIds; |
||||
if (!hasRole(user._id, 'livechat-manager')) { |
||||
const departmentAgents = LivechatDepartmentAgents.findByAgentId(user._id).fetch().map((d) => d.departmentId); |
||||
departmentIds = LivechatDepartment.find({ _id: { $in: departmentAgents }, enabled: true }).fetch().map((d) => d._id); |
||||
} |
||||
|
||||
const filter = { |
||||
rid: room._id, |
||||
...departmentIds && departmentIds.length > 0 && { department: { $in: departmentIds } }, |
||||
}; |
||||
|
||||
const inquiry = LivechatInquiry.findOne(filter, { fields: { status: 1 } }); |
||||
return inquiry && inquiry.status === 'queued'; |
||||
}, |
||||
function(room, user) { |
||||
if (!room.departmentId || room.open) { |
||||
return; |
||||
} |
||||
const agentOfDepartment = LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(user._id, room.departmentId); |
||||
if (!agentOfDepartment) { |
||||
return; |
||||
} |
||||
return hasPermission(user._id, 'view-livechat-room-closed-same-department'); |
||||
}, |
||||
]; |
||||
@ -0,0 +1,22 @@ |
||||
import { ServiceClass } from '../../../server/sdk/types/ServiceClass'; |
||||
import { IAuthorizationLivechat } from '../../../server/sdk/types/IAuthorizationLivechat'; |
||||
import { validators } from './roomAccessValidator.compatibility'; |
||||
import { api } from '../../../server/sdk/api'; |
||||
import { IRoom } from '../../../definition/IRoom'; |
||||
import { IUser } from '../../../definition/IUser'; |
||||
|
||||
class AuthorizationLivechat extends ServiceClass implements IAuthorizationLivechat { |
||||
protected name = 'authorization-livechat'; |
||||
|
||||
async canAccessRoom(room: Partial<IRoom>, user: Pick<IUser, '_id'>, extraData?: object): Promise<boolean> { |
||||
for (const validator of validators) { |
||||
if (validator(room, user, extraData)) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
api.registerService(new AuthorizationLivechat()); |
||||
@ -1,52 +0,0 @@ |
||||
export class BaseRaw { |
||||
constructor(col) { |
||||
this.col = col; |
||||
} |
||||
|
||||
_ensureDefaultFields(options) { |
||||
if (!this.defaultFields) { |
||||
return options; |
||||
} |
||||
|
||||
if (!options) { |
||||
return { projection: this.defaultFields }; |
||||
} |
||||
|
||||
// TODO: change all places using "fields" for raw models and remove the additional condition here
|
||||
if ((options.projection != null && Object.keys(options.projection).length > 0) |
||||
|| (options.fields != null && Object.keys(options.fields).length > 0)) { |
||||
return options; |
||||
} |
||||
|
||||
return { |
||||
...options, |
||||
projection: this.defaultFields, |
||||
}; |
||||
} |
||||
|
||||
findOneById(_id, options = {}) { |
||||
return this.findOne({ _id }, options); |
||||
} |
||||
|
||||
findOne(query = {}, options = {}) { |
||||
const optionsDef = this._ensureDefaultFields(options); |
||||
return this.col.findOne(query, optionsDef); |
||||
} |
||||
|
||||
findUsersInRoles() { |
||||
throw new Error('overwrite-function', 'You must overwrite this function in the extended classes'); |
||||
} |
||||
|
||||
find(query = {}, options = {}) { |
||||
const optionsDef = this._ensureDefaultFields(options); |
||||
return this.col.find(query, optionsDef); |
||||
} |
||||
|
||||
update(...args) { |
||||
return this.col.update(...args); |
||||
} |
||||
|
||||
removeById(_id) { |
||||
return this.col.deleteOne({ _id }); |
||||
} |
||||
} |
||||
@ -0,0 +1,94 @@ |
||||
import { Collection, FindOneOptions, Cursor, WriteOpResult, DeleteWriteOpResultObject, FilterQuery, UpdateQuery, UpdateOneOptions } from 'mongodb'; |
||||
|
||||
interface ITrash { |
||||
__collection__: string; |
||||
} |
||||
|
||||
export interface IBaseRaw<T> { |
||||
col: Collection<T>; |
||||
} |
||||
|
||||
const baseName = 'rocketchat_'; |
||||
|
||||
export class BaseRaw<T> implements IBaseRaw<T> { |
||||
public defaultFields?: Record<string, 1 | 0>; |
||||
|
||||
protected name: string; |
||||
|
||||
constructor( |
||||
public readonly col: Collection<T>, |
||||
public readonly trash?: Collection<T>, |
||||
) { |
||||
this.name = this.col.collectionName.replace(baseName, ''); |
||||
} |
||||
|
||||
_ensureDefaultFields<T>(options: FindOneOptions<T>): FindOneOptions<T> { |
||||
if (!this.defaultFields) { |
||||
return options; |
||||
} |
||||
|
||||
if (!options) { |
||||
return { projection: this.defaultFields }; |
||||
} |
||||
|
||||
// TODO: change all places using "fields" for raw models and remove the additional condition here
|
||||
if ((options.projection != null && Object.keys(options.projection).length > 0) |
||||
|| (options.fields != null && Object.keys(options.fields).length > 0)) { |
||||
return options; |
||||
} |
||||
|
||||
return { |
||||
...options, |
||||
projection: this.defaultFields, |
||||
}; |
||||
} |
||||
|
||||
async findOneById(_id: string, options: FindOneOptions<T> = {}): Promise<T | undefined> { |
||||
return this.findOne({ _id }, options); |
||||
} |
||||
|
||||
async findOne(query = {}, options: FindOneOptions<T> = {}): Promise<T | undefined> { |
||||
const optionsDef = this._ensureDefaultFields<T>(options); |
||||
|
||||
if (typeof query === 'string') { |
||||
return this.findOneById(query, options); |
||||
} |
||||
|
||||
return await this.col.findOne<T>(query, optionsDef) ?? undefined; |
||||
} |
||||
|
||||
findUsersInRoles(): void { |
||||
throw new Error('[overwrite-function] You must overwrite this function in the extended classes'); |
||||
} |
||||
|
||||
find(query = {}, options: FindOneOptions<T> = {}): Cursor<T> { |
||||
const optionsDef = this._ensureDefaultFields(options); |
||||
return this.col.find(query, optionsDef); |
||||
} |
||||
|
||||
update(filter: FilterQuery<T>, update: UpdateQuery<T> | Partial<T>, options?: UpdateOneOptions & { multi?: boolean }): Promise<WriteOpResult> { |
||||
return this.col.update(filter, update, options); |
||||
} |
||||
|
||||
removeById(_id: string): Promise<DeleteWriteOpResultObject> { |
||||
const query: object = { _id }; |
||||
return this.col.deleteOne(query); |
||||
} |
||||
|
||||
// Trash
|
||||
trashFind(query: FilterQuery<T & ITrash>, options: FindOneOptions<T>): Cursor<T> | undefined { |
||||
return this.trash?.find<T>({ |
||||
__collection__: this.name, |
||||
...query, |
||||
}, options); |
||||
} |
||||
|
||||
async trashFindOneById(_id: string, options: FindOneOptions<T> = {}): Promise<T | undefined> { |
||||
const query: object = { |
||||
_id, |
||||
__collection__: this.name, |
||||
}; |
||||
|
||||
return await this.trash?.findOne<T>(query, options) ?? undefined; |
||||
} |
||||
} |
||||
@ -0,0 +1,4 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { IInstanceStatus } from '../../../../definition/IInstanceStatus'; |
||||
|
||||
export class InstanceStatusRaw extends BaseRaw<IInstanceStatus> {} |
||||
@ -0,0 +1,4 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { IIntegrationHistory } from '../../../../definition/IIntegrationHistory'; |
||||
|
||||
export class IntegrationHistoryRaw extends BaseRaw<IIntegrationHistory> {} |
||||
@ -0,0 +1,4 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { ILoginServiceConfiguration } from '../../../../definition/ILoginServiceConfiguration'; |
||||
|
||||
export class LoginServiceConfigurationRaw extends BaseRaw<ILoginServiceConfiguration> {} |
||||
@ -1,4 +0,0 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
|
||||
export class PermissionsRaw extends BaseRaw { |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { IPermission } from '../../../../definition/IPermission'; |
||||
|
||||
export class PermissionsRaw extends BaseRaw<IPermission> { |
||||
} |
||||
@ -1,37 +0,0 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
|
||||
export class SettingsRaw extends BaseRaw { |
||||
async getValueById(_id) { |
||||
const setting = await this.col.findOne({ _id }, { projection: { value: 1 } }); |
||||
|
||||
return setting.value; |
||||
} |
||||
|
||||
findByIds(_id = []) { |
||||
_id = [].concat(_id); |
||||
|
||||
const query = { |
||||
_id: { |
||||
$in: _id, |
||||
}, |
||||
}; |
||||
|
||||
return this.find(query); |
||||
} |
||||
|
||||
updateValueById(_id, value) { |
||||
const query = { |
||||
blocked: { $ne: true }, |
||||
value: { $ne: value }, |
||||
_id, |
||||
}; |
||||
|
||||
const update = { |
||||
$set: { |
||||
value, |
||||
}, |
||||
}; |
||||
|
||||
return this.col.update(query, update); |
||||
} |
||||
} |
||||
@ -0,0 +1,53 @@ |
||||
import { Cursor, WriteOpResult } from 'mongodb'; |
||||
|
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { ISetting } from '../../../../definition/ISetting'; |
||||
|
||||
type T = ISetting; |
||||
|
||||
export class SettingsRaw extends BaseRaw<T> { |
||||
async getValueById(_id: string): Promise<ISetting['value'] | undefined> { |
||||
const setting = await this.findOne({ _id }, { projection: { value: 1 } }); |
||||
|
||||
return setting?.value; |
||||
} |
||||
|
||||
findOneNotHiddenById(_id: string): Promise<T | undefined> { |
||||
const query = { |
||||
_id, |
||||
hidden: { $ne: true }, |
||||
}; |
||||
|
||||
return this.findOne(query); |
||||
} |
||||
|
||||
findByIds(_id: string[] | string = []): Cursor<T> { |
||||
if (typeof _id === 'string') { |
||||
_id = [_id]; |
||||
} |
||||
|
||||
const query = { |
||||
_id: { |
||||
$in: _id, |
||||
}, |
||||
}; |
||||
|
||||
return this.find(query); |
||||
} |
||||
|
||||
updateValueById(_id: string, value: any): Promise<WriteOpResult> { |
||||
const query = { |
||||
blocked: { $ne: true }, |
||||
value: { $ne: value }, |
||||
_id, |
||||
}; |
||||
|
||||
const update = { |
||||
$set: { |
||||
value, |
||||
}, |
||||
}; |
||||
|
||||
return this.update(query, update); |
||||
} |
||||
} |
||||
@ -1,57 +0,0 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
|
||||
export class SubscriptionsRaw extends BaseRaw { |
||||
findOneByRoomIdAndUserId(rid, uid, options) { |
||||
const query = { |
||||
rid, |
||||
'u._id': uid, |
||||
}; |
||||
|
||||
return this.col.findOne(query, options); |
||||
} |
||||
|
||||
countByRoomIdAndUserId(rid, uid) { |
||||
const query = { |
||||
rid, |
||||
'u._id': uid, |
||||
}; |
||||
|
||||
const cursor = this.col.find(query); |
||||
|
||||
return cursor.count(); |
||||
} |
||||
|
||||
isUserInRole(uid, roleName, rid) { |
||||
if (rid == null) { |
||||
return; |
||||
} |
||||
|
||||
const query = { |
||||
'u._id': uid, |
||||
rid, |
||||
roles: roleName, |
||||
}; |
||||
|
||||
return this.findOne(query, { fields: { roles: 1 } }); |
||||
} |
||||
|
||||
setAsReadByRoomIdAndUserId(rid, uid, alert = false) { |
||||
const query = { |
||||
rid, |
||||
'u._id': uid, |
||||
}; |
||||
|
||||
const update = { |
||||
$set: { |
||||
open: true, |
||||
alert, |
||||
unread: 0, |
||||
userMentions: 0, |
||||
groupMentions: 0, |
||||
ls: new Date(), |
||||
}, |
||||
}; |
||||
|
||||
return this.col.update(query, update); |
||||
} |
||||
} |
||||
@ -0,0 +1,72 @@ |
||||
import { FindOneOptions, Cursor, UpdateQuery, FilterQuery } from 'mongodb'; |
||||
|
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { ISubscription } from '../../../../definition/ISubscription'; |
||||
|
||||
type T = ISubscription; |
||||
export class SubscriptionsRaw extends BaseRaw<T> { |
||||
findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOneOptions<T> = {}): Promise<T | undefined> { |
||||
const query = { |
||||
rid, |
||||
'u._id': uid, |
||||
}; |
||||
|
||||
return this.findOne(query, options); |
||||
} |
||||
|
||||
findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions<T> = {}): Cursor<T> { |
||||
const query = { |
||||
rid: roomId, |
||||
'u._id': { |
||||
$ne: userId, |
||||
}, |
||||
}; |
||||
|
||||
return this.find(query, options); |
||||
} |
||||
|
||||
countByRoomIdAndUserId(rid: string, uid: string): Promise<number> { |
||||
const query = { |
||||
rid, |
||||
'u._id': uid, |
||||
}; |
||||
|
||||
const cursor = this.find(query, { projection: { _id: 0 } }); |
||||
|
||||
return cursor.count(); |
||||
} |
||||
|
||||
async isUserInRole(uid: string, roleName: string, rid: string): Promise<T | undefined> { |
||||
if (rid == null) { |
||||
return; |
||||
} |
||||
|
||||
const query = { |
||||
'u._id': uid, |
||||
rid, |
||||
roles: roleName, |
||||
}; |
||||
|
||||
return this.findOne(query, { projection: { roles: 1 } }); |
||||
} |
||||
|
||||
setAsReadByRoomIdAndUserId(rid: string, uid: string, alert = false, options: FindOneOptions<T> = {}): ReturnType<BaseRaw<T>['update']> { |
||||
const query: FilterQuery<T> = { |
||||
rid, |
||||
'u._id': uid, |
||||
}; |
||||
|
||||
const update: UpdateQuery<T> = { |
||||
$set: { |
||||
open: true, |
||||
alert, |
||||
unread: 0, |
||||
userMentions: 0, |
||||
groupMentions: 0, |
||||
ls: new Date(), |
||||
}, |
||||
}; |
||||
|
||||
return this.update(query, update, options); |
||||
} |
||||
} |
||||
@ -0,0 +1,4 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { IUserSession } from '../../../../definition/IUserSession'; |
||||
|
||||
export class UsersSessionsRaw extends BaseRaw<IUserSession> {} |
||||
@ -1,212 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { DDPCommon } from 'meteor/ddp-common'; |
||||
|
||||
import { WEB_RTC_EVENTS } from '../../../webrtc'; |
||||
import { Subscriptions, Rooms } from '../../../models/server'; |
||||
import { settings } from '../../../settings/server'; |
||||
|
||||
const changedPayload = function(collection, id, fields) { |
||||
return DDPCommon.stringifyDDP({ |
||||
msg: 'changed', |
||||
collection, |
||||
id, |
||||
fields, |
||||
}); |
||||
}; |
||||
const send = function(self, msg) { |
||||
if (!self.socket) { |
||||
return; |
||||
} |
||||
self.socket.send(msg); |
||||
}; |
||||
class RoomStreamer extends Meteor.Streamer { |
||||
_publish(publication, eventName, options) { |
||||
super._publish(publication, eventName, options); |
||||
const uid = Meteor.userId(); |
||||
if (/rooms-changed/.test(eventName)) { |
||||
const roomEvent = (...args) => send(publication._session, changedPayload(this.subscriptionName, 'id', { |
||||
eventName: `${ uid }/rooms-changed`, |
||||
args, |
||||
})); |
||||
const rooms = Subscriptions.find({ 'u._id': uid }, { fields: { rid: 1 } }).fetch(); |
||||
rooms.forEach(({ rid }) => { |
||||
this.on(rid, roomEvent); |
||||
}); |
||||
|
||||
const userEvent = (clientAction, { rid }) => { |
||||
switch (clientAction) { |
||||
case 'inserted': |
||||
rooms.push({ rid }); |
||||
this.on(rid, roomEvent); |
||||
|
||||
// after a subscription is added need to emit the room again
|
||||
roomEvent('inserted', Rooms.findOneById(rid)); |
||||
break; |
||||
|
||||
case 'removed': |
||||
this.removeListener(rid, roomEvent); |
||||
break; |
||||
} |
||||
}; |
||||
this.on(uid, userEvent); |
||||
|
||||
publication.onStop(() => { |
||||
this.removeListener(uid, userEvent); |
||||
rooms.forEach(({ rid }) => this.removeListener(rid, roomEvent)); |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
class Notifications { |
||||
constructor() { |
||||
const self = this; |
||||
this.debug = false; |
||||
this.notifyUser = this.notifyUser.bind(this); |
||||
this.streamAll = new Meteor.Streamer('notify-all'); |
||||
this.streamLogged = new Meteor.Streamer('notify-logged'); |
||||
this.streamRoom = new Meteor.Streamer('notify-room'); |
||||
this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); |
||||
this.streamUser = new RoomStreamer('notify-user'); |
||||
this.streamAll.allowWrite('none'); |
||||
this.streamLogged.allowWrite('none'); |
||||
this.streamRoom.allowWrite('none'); |
||||
this.streamRoomUsers.allowWrite(function(eventName, ...args) { |
||||
const [roomId, e] = eventName.split('/'); |
||||
// const user = Meteor.users.findOne(this.userId, {
|
||||
// fields: {
|
||||
// username: 1
|
||||
// }
|
||||
// });
|
||||
if (Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) { |
||||
const subscriptions = Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch(); |
||||
subscriptions.forEach((subscription) => self.notifyUser(subscription.u._id, e, ...args)); |
||||
} |
||||
return false; |
||||
}); |
||||
this.streamUser.allowWrite('logged'); |
||||
this.streamAll.allowRead('all'); |
||||
this.streamLogged.allowRead('logged'); |
||||
this.streamRoom.allowRead(function(eventName, extraData) { |
||||
const [roomId] = eventName.split('/'); |
||||
const room = Rooms.findOneById(roomId); |
||||
if (!room) { |
||||
console.warn(`Invalid streamRoom eventName: "${ eventName }"`); |
||||
return false; |
||||
} |
||||
if (room.t === 'l' && extraData && extraData.token && room.v.token === extraData.token) { |
||||
return true; |
||||
} |
||||
if (this.userId == null) { |
||||
return false; |
||||
} |
||||
const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { fields: { _id: 1 } }); |
||||
return subscription != null; |
||||
}); |
||||
this.streamRoomUsers.allowRead('none'); |
||||
this.streamUser.allowRead(function(eventName) { |
||||
const [userId] = eventName.split('/'); |
||||
return (this.userId != null) && this.userId === userId; |
||||
}); |
||||
} |
||||
|
||||
notifyAll(eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyAll', [eventName, ...args]); |
||||
} |
||||
args.unshift(eventName); |
||||
return this.streamAll.emit.apply(this.streamAll, args); |
||||
} |
||||
|
||||
notifyLogged(eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyLogged', [eventName, ...args]); |
||||
} |
||||
args.unshift(eventName); |
||||
return this.streamLogged.emit.apply(this.streamLogged, args); |
||||
} |
||||
|
||||
notifyRoom(room, eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyRoom', [room, eventName, ...args]); |
||||
} |
||||
args.unshift(`${ room }/${ eventName }`); |
||||
return this.streamRoom.emit.apply(this.streamRoom, args); |
||||
} |
||||
|
||||
notifyUser(userId, eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyUser', [userId, eventName, ...args]); |
||||
} |
||||
args.unshift(`${ userId }/${ eventName }`); |
||||
return this.streamUser.emit.apply(this.streamUser, args); |
||||
} |
||||
|
||||
notifyAllInThisInstance(eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyAll', [eventName, ...args]); |
||||
} |
||||
args.unshift(eventName); |
||||
return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args); |
||||
} |
||||
|
||||
notifyLoggedInThisInstance(eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyLogged', [eventName, ...args]); |
||||
} |
||||
args.unshift(eventName); |
||||
return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args); |
||||
} |
||||
|
||||
notifyRoomInThisInstance(room, eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyRoomAndBroadcast', [room, eventName, ...args]); |
||||
} |
||||
args.unshift(`${ room }/${ eventName }`); |
||||
return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args); |
||||
} |
||||
|
||||
notifyUserInThisInstance(userId, eventName, ...args) { |
||||
if (this.debug === true) { |
||||
console.log('notifyUserAndBroadcast', [userId, eventName, ...args]); |
||||
} |
||||
args.unshift(`${ userId }/${ eventName }`); |
||||
return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args); |
||||
} |
||||
} |
||||
|
||||
const notifications = new Notifications(); |
||||
|
||||
notifications.streamRoom.allowWrite(function(eventName, username, typing, extraData) { |
||||
const [roomId, e] = eventName.split('/'); |
||||
|
||||
if (isNaN(e) ? e === WEB_RTC_EVENTS.WEB_RTC : parseFloat(e) === WEB_RTC_EVENTS.WEB_RTC) { |
||||
return true; |
||||
} |
||||
|
||||
if (e === 'typing') { |
||||
const key = settings.get('UI_Use_Real_Name') ? 'name' : 'username'; |
||||
// typing from livechat widget
|
||||
if (extraData && extraData.token) { |
||||
const room = Rooms.findOneById(roomId); |
||||
if (room && room.t === 'l' && room.v.token === extraData.token) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
const user = Meteor.users.findOne(this.userId, { |
||||
fields: { |
||||
[key]: 1, |
||||
}, |
||||
}); |
||||
|
||||
if (!user) { |
||||
return false; |
||||
} |
||||
|
||||
return user[key] === username; |
||||
} |
||||
return false; |
||||
}); |
||||
|
||||
export default notifications; |
||||
@ -0,0 +1,54 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Promise } from 'meteor/promise'; |
||||
import { DDPCommon } from 'meteor/ddp-common'; |
||||
|
||||
import { NotificationsModule } from '../../../../server/modules/notifications/notifications.module'; |
||||
import { Streamer, StreamerCentral } from '../../../../server/modules/streamer/streamer.module'; |
||||
import { api } from '../../../../server/sdk/api'; |
||||
import { |
||||
Subscriptions as SubscriptionsRaw, |
||||
Rooms as RoomsRaw, |
||||
Users as UsersRaw, |
||||
Settings as SettingsRaw, |
||||
} from '../../../models/server/raw'; |
||||
|
||||
// TODO: Replace this in favor of the api.broadcast
|
||||
StreamerCentral.on('broadcast', (name, eventName, args) => { |
||||
api.broadcast('stream', [ |
||||
name, |
||||
eventName, |
||||
args, |
||||
]); |
||||
}); |
||||
|
||||
export class Stream extends Streamer { |
||||
registerPublication(name: string, fn: (eventName: string, options: boolean | {useCollection?: boolean; args?: any}) => void): void { |
||||
Meteor.publish(name, function(eventName, options) { |
||||
return Promise.await(fn.call(this, eventName, options)); |
||||
}); |
||||
} |
||||
|
||||
registerMethod(methods: Record<string, (eventName: string, ...args: any[]) => any>): void { |
||||
Meteor.methods(methods); |
||||
} |
||||
|
||||
changedPayload(collection: string, id: string, fields: Record<string, any>): string | false { |
||||
return DDPCommon.stringifyDDP({ |
||||
msg: 'changed', |
||||
collection, |
||||
id, |
||||
fields, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
const notifications = new NotificationsModule(Stream); |
||||
|
||||
notifications.configure({ |
||||
Rooms: RoomsRaw, |
||||
Subscriptions: SubscriptionsRaw, |
||||
Users: UsersRaw, |
||||
Settings: SettingsRaw, |
||||
}); |
||||
|
||||
export default notifications; |
||||
@ -0,0 +1,45 @@ |
||||
import _ from 'underscore'; |
||||
|
||||
import { Users } from '../../models/server'; |
||||
import { settings } from '../../settings/server'; |
||||
import { searchProviderService } from './service/providerService'; |
||||
import { ServiceClass } from '../../../server/sdk/types/ServiceClass'; |
||||
import { api } from '../../../server/sdk/api'; |
||||
import { searchEventService } from './events/events'; |
||||
|
||||
class Search extends ServiceClass { |
||||
protected name = 'search'; |
||||
|
||||
constructor() { |
||||
super(); |
||||
|
||||
this.onEvent('watch.users', async ({ clientAction, data, id }) => { |
||||
if (clientAction === 'removed') { |
||||
searchEventService.promoteEvent('user.delete', id, undefined); |
||||
return; |
||||
} |
||||
|
||||
const user = data ?? Users.findOneById(id); |
||||
searchEventService.promoteEvent('user.save', id, user); |
||||
}); |
||||
|
||||
this.onEvent('watch.rooms', async ({ clientAction, room }) => { |
||||
if (clientAction === 'removed') { |
||||
searchEventService.promoteEvent('room.delete', room._id, undefined); |
||||
return; |
||||
} |
||||
|
||||
searchEventService.promoteEvent('room.save', room._id, room); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
const service = new Search(); |
||||
|
||||
settings.get('Search.Provider', _.debounce(() => { |
||||
if (searchProviderService.activeProvider?.on) { |
||||
api.registerService(service); |
||||
} else { |
||||
api.destroyService(service); |
||||
} |
||||
}, 1000)); |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue