Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] PDF Worker service #27568

Merged
merged 39 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f7080d0
Implement queueing with mongo-message-queue
KevLehman Dec 13, 2022
bb02793
Add logging and retries
KevLehman Dec 14, 2022
ab66e36
typing
KevLehman Dec 14, 2022
089a41f
change events for lifecycle hooks
KevLehman Dec 14, 2022
0c19b43
Add queueInfo method
KevLehman Dec 14, 2022
f9441a0
save it
KevLehman Dec 14, 2022
e64041d
test
KevLehman Dec 14, 2022
af74cb0
PDF template processing through queue actions
KevLehman Dec 16, 2022
f18d527
small refactor to queueworker
KevLehman Dec 16, 2022
48a3124
Add testing endpoint
KevLehman Dec 16, 2022
4b4d2a4
waiting for uploads service
KevLehman Dec 19, 2022
1b029bb
PDF upload
KevLehman Dec 20, 2022
5b96d38
PDF sending message works
KevLehman Dec 20, 2022
97c7f52
PDF failure notifications
KevLehman Dec 21, 2022
1435620
somethign
KevLehman Dec 21, 2022
09b4d18
bye bye pdfworker
KevLehman Dec 21, 2022
b15a894
typescript
KevLehman Dec 21, 2022
5c2f44e
more typescript
KevLehman Dec 21, 2022
4fb5bc8
endpoint typings
KevLehman Dec 21, 2022
b084eff
add translation service, and logging
KevLehman Dec 22, 2022
9e1380f
hehehe
KevLehman Dec 22, 2022
d3ebd41
Merge branch 'feat/chat-transcript' into new/pdf-worker-base
KevLehman Dec 22, 2022
41cdd80
oops
KevLehman Dec 22, 2022
0d4e829
Render an actual PDF from messages of a room
KevLehman Dec 22, 2022
9f581c0
oopsie
KevLehman Dec 23, 2022
0a812fd
Retries
KevLehman Dec 23, 2022
d1d8481
CR suggestions
KevLehman Dec 27, 2022
3faf70c
Merge branch 'feat/chat-transcript' into new/pdf-worker-base
KevLehman Dec 27, 2022
5407953
silently ignore the 2 hours you spent to remove node_modules and inst…
KevLehman Dec 27, 2022
541fdc8
fix template for hygen
KevLehman Dec 27, 2022
3321f08
Settings service and utils lib
KevLehman Dec 28, 2022
636045e
pdfworker got fancy
KevLehman Dec 28, 2022
51cbbae
Merge branch 'feat/chat-transcript' into new/pdf-worker-base
Jan 5, 2023
211d79f
Merge branch 'feat/chat-transcript' into new/pdf-worker-base
Jan 6, 2023
c82deb6
[NEW] Add header component and load fonts to pdf react template (#27671)
Jan 9, 2023
6f9dcfc
Merge branch 'feat/chat-transcript' into new/pdf-worker-base
murtaza98 Jan 19, 2023
20337d4
Merge branch 'feat/chat-transcript' into new/pdf-worker-base
murtaza98 Jan 20, 2023
e392449
update lock file
murtaza98 Jan 20, 2023
4b7406c
adapt UI call to new typings
murtaza98 Jan 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`);

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]),
KevLehman marked this conversation as resolved.
Show resolved Hide resolved
]);
};
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) {
KevLehman marked this conversation as resolved.
Show resolved Hide resolved
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';
KevLehman marked this conversation as resolved.
Show resolved Hide resolved
}

// 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