Skip to content

Commit

Permalink
[NEW] PDF Worker service (#27568)
Browse files Browse the repository at this point in the history
Co-authored-by: Filipe Marins <filipe.marins@rocket.chat>
Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>
Co-authored-by: murtaza98 <murtaza.patrawala@rocket.chat>
  • Loading branch information
4 people authored Jan 23, 2023
1 parent 85f61de commit ef60939
Show file tree
Hide file tree
Showing 71 changed files with 1,697 additions and 256 deletions.
7 changes: 0 additions & 7 deletions _templates/service/new/index.ts.2.ejs.t

This file was deleted.

7 changes: 0 additions & 7 deletions _templates/service/new/index.ts.ejs.t

This file was deleted.

8 changes: 0 additions & 8 deletions _templates/service/new/serviceInterface.ts.ejs.t

This file was deleted.

3 changes: 1 addition & 2 deletions _templates/service/new/servicesClass.ejs.t
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
to: ee/apps/<%= name %>/src/<%= h.changeCase.pascalCase(name) %>.ts
---
import { ServiceClass } from '@rocket.chat/core-services';
import { I<%= h.changeCase.pascalCase(name) %>Service } from '../../../../apps/meteor/server/sdk/types/I<%= h.changeCase.pascalCase(name) %>Service';

export class <%= h.changeCase.pascalCase(name) %> extends ServiceClass implements I<%= h.changeCase.pascalCase(name) %>Service {
export class <%= h.changeCase.pascalCase(name) %> extends ServiceClass {
protected name = '<%= name %>';

constructor() {
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/app/file-upload/server/lib/FileUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,11 @@ export class FileUploadClass {
streamOrBuffer = Promise.await(streamToBuffer(streamOrBuffer));
}

if (streamOrBuffer instanceof Uint8Array) {
// Services compat :)
streamOrBuffer = Buffer.from(streamOrBuffer);
}

// Check if the fileData matches store filter
const filter = this.store.getFilter();
if (filter && filter.check) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,14 @@ export const validators = [
}
return hasPermission(user._id, 'view-livechat-room-closed-same-department');
},
function (room, user) {
// Check if user is rocket.cat
if (!user?._id) {
return false;
}

// This opens the ability for rocketcat to upload files to a livechat room without being included in it :)
// Worst case, someone manages to log in as rocketcat lol
return user._id === 'rocket.cat';
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ export const useQuickActions = (
[closeModal, dispatchToastMessage, requestTranscript, rid, t],
);

const sendTranscriptPDF = useEndpoint('POST', '/v1/omnichannel/:rid/request-transcript', { rid });

const handleSendTranscriptPDF = useCallback(async () => {
try {
await sendTranscriptPDF();
dispatchToastMessage({
type: 'success',
message: t('Livechat_transcript_has_been_requested'),
});
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [dispatchToastMessage, sendTranscriptPDF, t]);

const sendTranscript = useMethod('livechat:sendTranscript');

const handleSendTranscript = useCallback(
Expand Down Expand Up @@ -214,6 +228,9 @@ export const useQuickActions = (
/>,
);
break;
case QuickActionsEnum.TranscriptPDF:
handleSendTranscriptPDF();
break;
case QuickActionsEnum.TranscriptEmail:
const visitorEmail = await getVisitorEmail();

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ import './tags';
import './units';
import './business-hours';
import './rooms';
import './transcript';
37 changes: 37 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { LivechatRooms } from '@rocket.chat/models';
import { OmnichannelTranscript } from '@rocket.chat/core-services';

import { API } from '../../../../../app/api/server';
import { canAccessRoomAsync } from '../../../../../app/authorization/server/functions/canAccessRoom';

API.v1.addRoute(
'omnichannel/:rid/request-transcript',
{ authRequired: true, permissionsRequired: ['request-pdf-transcript'] },
{
async post() {
const room = await LivechatRooms.findOneById(this.urlParams.rid);
if (!room) {
throw new Error('error-invalid-room');
}

if (!(await canAccessRoomAsync(room, { _id: this.userId }))) {
throw new Error('error-not-allowed');
}

// Flow is as follows:
// 1. Call OmnichannelTranscript.requestTranscript()
// 2. OmnichannelTranscript.requestTranscript() calls QueueWorker.queueWork()
// 3. QueueWorker.queueWork() eventually calls OmnichannelTranscript.workOnPdf()
// 4. OmnichannelTranscript.workOnPdf() calls OmnichannelTranscript.pdfComplete() when processing ends
// 5. OmnichannelTranscript.pdfComplete() sends the messages to the user, and updates the room with the flags
await OmnichannelTranscript.requestTranscript({
details: {
userId: this.userId,
rid: this.urlParams.rid,
},
});

return API.v1.success();
},
},
);
2 changes: 2 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const createPermissions = async (): Promise<void> => {
const livechatMonitorRole = 'livechat-monitor';
const livechatManagerRole = 'livechat-manager';
const adminRole = 'admin';
const livechatAgentRole = 'livechat-agent';

const monitorRole = await Roles.findOneById(livechatMonitorRole, { projection: { _id: 1 } });
if (!monitorRole) {
Expand All @@ -22,5 +23,6 @@ export const createPermissions = async (): Promise<void> => {
Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]),
Permissions.create('spy-voip-calls', [adminRole, livechatManagerRole, livechatMonitorRole]),
Permissions.create('outbound-voip-calls', [adminRole, livechatManagerRole]),
Permissions.create('request-pdf-transcript', [adminRole, livechatManagerRole, livechatMonitorRole, livechatAgentRole]),
]);
};
5 changes: 3 additions & 2 deletions apps/meteor/ee/server/NetworkBroker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class NetworkBroker implements IBroker {
this.broker.destroyService(name);
}

createService(instance: IServiceClass): void {
createService(instance: IServiceClass, serviceDependencies?: string[]): void {
const methods = (
instance.constructor?.name === 'Object'
? Object.getOwnPropertyNames(instance)
Expand Down Expand Up @@ -106,7 +106,8 @@ export class NetworkBroker implements IBroker {
return;
}

const dependencies = name !== 'license' ? { dependencies: ['license'] } : {};
// Allow services to depend on other services too
const dependencies = name !== 'license' ? { dependencies: ['license', ...(serviceDependencies || [])] } : {};

const service: ServiceSchema = {
name,
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/ee/server/lib/registerServiceModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import { IntegrationHistoryRaw } from '../../../server/models/raw/IntegrationHis
import { IntegrationsRaw } from '../../../server/models/raw/Integrations';
import { EmailInboxRaw } from '../../../server/models/raw/EmailInbox';
import { PbxEventsRaw } from '../../../server/models/raw/PbxEvents';
import { LivechatRoomsRaw } from '../../../server/models/raw/LivechatRooms';
import { UploadsRaw } from '../../../server/models/raw/Uploads';
import { LivechatVisitorsRaw } from '../../../server/models/raw/LivechatVisitors';

// TODO add trash param to appropiate model instances
export function registerServiceModels(db: Db, trash?: Collection<RocketChatRecordDeleted<any>>): void {
Expand Down Expand Up @@ -55,4 +58,7 @@ export function registerServiceModels(db: Db, trash?: Collection<RocketChatRecor
registerModel('IIntegrationsModel', () => new IntegrationsRaw(db));
registerModel('IEmailInboxModel', () => new EmailInboxRaw(db));
registerModel('IPbxEventsModel', () => new PbxEventsRaw(db));
registerModel('ILivechatRoomsModel', () => new LivechatRoomsRaw(db));
registerModel('IUploadsModel', () => new UploadsRaw(db));
registerModel('ILivechatVisitorsModel', () => new LivechatVisitorsRaw(db));
}
4 changes: 4 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3705,6 +3705,8 @@
"Paid_Apps": "Paid Apps",
"Payload": "Payload",
"PDF": "PDF",
"pdf_success_message": "PDF Transcript successfully generated",
"pdf_error_message": "Error generating PDF Transcript",
"Peer_Password": "Peer Password",
"People": "People",
"Permalink": "Permalink",
Expand Down Expand Up @@ -3993,6 +3995,8 @@
"Request_comment_when_closing_conversation": "Request comment when closing conversation",
"Request_comment_when_closing_conversation_description": "If enabled, the agent will need to set a comment before the conversation is closed.",
"Request_tag_before_closing_chat": "Request tag(s) before closing conversation",
"request-pdf-transcript": "Request PDF Transcript",
"request-pdf-transcript_description": "Permission to request a PDF transcript for a given Omnichannel room",
"Requested_At": "Requested At",
"Requested_By": "Requested By",
"Require": "Require",
Expand Down
35 changes: 35 additions & 0 deletions apps/meteor/server/models/raw/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -1288,4 +1288,39 @@ export class LivechatRoomsRaw extends BaseRaw {
},
]);
}

// These 3 methods shouldn't be here :( but current EE model has a meteor dependency
// And refactoring it could take time
setTranscriptRequestedPdfById(rid) {
return this.updateOne(
{
_id: rid,
},
{
$set: { pdfTranscriptRequested: true },
},
);
}

unsetTranscriptRequestedPdfById(rid) {
return this.updateOne(
{
_id: rid,
},
{
$unset: { pdfTranscriptRequested: 1 },
},
);
}

setPdfTranscriptFileIdById(rid, fileId) {
return this.updateOne(
{
_id: rid,
},
{
$set: { pdfTranscriptFileId: fileId },
},
);
}
}
10 changes: 10 additions & 0 deletions apps/meteor/server/models/raw/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,16 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
);
}

findLivechatMessages(rid: IRoom['_id'], options?: FindOptions<IMessage>): FindCursor<IMessage> {
return this.find(
{
rid,
$or: [{ t: { $exists: false } }, { t: 'livechat-close' }],
},
options,
);
}

async setBlocksById(_id: string, blocks: Required<IMessage>['blocks']): Promise<void> {
await this.updateOne(
{ _id },
Expand Down
27 changes: 27 additions & 0 deletions apps/meteor/server/services/messages/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Users } from '@rocket.chat/models';
import type { IMessage } from '@rocket.chat/core-typings';
import type { IMessageService } from '@rocket.chat/core-services';
import { ServiceClassInternal } from '@rocket.chat/core-services';

import { createDirectMessage } from '../../methods/createDirectMessage';
import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage';

export class MessageService extends ServiceClassInternal implements IMessageService {
protected name = 'message';

async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> {
const [toUser, fromUser] = await Promise.all([
Users.findOneById(to, { projection: { username: 1 } }),
Users.findOneById(from, { projection: { _id: 1 } }),
]);

if (!toUser || !fromUser) {
throw new Error('error-invalid-user');
}
return createDirectMessage([toUser.username], fromUser._id);
}

async sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise<IMessage> {
return executeSendMessage(fromId, { rid, msg });
}
}
12 changes: 12 additions & 0 deletions apps/meteor/server/services/settings/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ISettingsService } from '@rocket.chat/core-services';
import { ServiceClassInternal } from '@rocket.chat/core-services';

import { settings } from '../../../app/settings/server';

export class SettingsService extends ServiceClassInternal implements ISettingsService {
protected name = 'settings';

async get<T>(settingId: string): Promise<T> {
return settings.get<T>(settingId);
}
}
6 changes: 6 additions & 0 deletions apps/meteor/server/services/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { PushService } from './push/service';
import { DeviceManagementService } from './device-management/service';
import { FederationService } from './federation/service';
import { UploadService } from './upload/service';
import { MessageService } from './messages/service';
import { TranslationService } from './translation/service';
import { SettingsService } from './settings/service';

const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;

Expand All @@ -45,6 +48,9 @@ api.registerService(new DeviceManagementService());
api.registerService(new VideoConfService());
api.registerService(new FederationService());
api.registerService(new UploadService());
api.registerService(new MessageService());
api.registerService(new TranslationService());
api.registerService(new SettingsService());

// if the process is running in micro services mode we don't need to register services that will run separately
if (!isRunningMs()) {
Expand Down
35 changes: 35 additions & 0 deletions apps/meteor/server/services/translation/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Settings } from '@rocket.chat/models';
import type { IUser } from '@rocket.chat/core-typings';
import mem from 'mem';
import { ServiceClassInternal } from '@rocket.chat/core-services';
import type { ITranslationService } from '@rocket.chat/core-services';

export class TranslationService extends ServiceClassInternal implements ITranslationService {
protected name = 'translation';

// Cache the server language for 1 hour
private getServerLanguageCached = mem(this.getServerLanguage.bind(this), { maxAge: 1000 * 60 * 60 });

private async getServerLanguage(): Promise<string> {
return ((await Settings.findOneById('Language'))?.value as string) || 'en';
}

// Use translateText when you already know the language, or want to translate to a predefined language
translateText(text: string, targetLanguage: string): Promise<string> {
return Promise.resolve(TAPi18n.__(text, { lng: targetLanguage }));
}

// Use translate when you want to translate to the user's language, or server's as a fallback
async translate(text: string, user: IUser): Promise<string> {
const language = user.language || (await this.getServerLanguageCached());

return this.translateText(text, language);
}

async translateToServerLanguage(text: string): Promise<string> {
const language = await this.getServerLanguageCached();

return this.translateText(text, language);
}
}
Loading

0 comments on commit ef60939

Please sign in to comment.