[NEW] Matrix Federation events coverage expansion (support for 5 more events) (#26705)
parent
71a4cb7933
commit
ab3ed80a6d
@ -0,0 +1,57 @@ |
||||
import { isMessageFromMatrixFederation } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { IFederationBridge } from '../domain/IFederationBridge'; |
||||
import type { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; |
||||
import type { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; |
||||
import type { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; |
||||
import { FederationService } from './AbstractFederationService'; |
||||
import type { FederationMessageReactionEventDto } from './input/MessageReceiverDto'; |
||||
|
||||
export class FederationMessageServiceListener extends FederationService { |
||||
constructor( |
||||
protected internalRoomAdapter: RocketChatRoomAdapter, |
||||
protected internalUserAdapter: RocketChatUserAdapter, |
||||
protected internalMessageAdapter: RocketChatMessageAdapter, |
||||
protected internalSettingsAdapter: RocketChatSettingsAdapter, |
||||
protected bridge: IFederationBridge, |
||||
) { |
||||
super(bridge, internalUserAdapter, internalSettingsAdapter); |
||||
} |
||||
|
||||
public async onMessageReaction(messageReactionEventInput: FederationMessageReactionEventDto): Promise<void> { |
||||
const { |
||||
externalRoomId, |
||||
emoji, |
||||
externalSenderId, |
||||
externalEventId: externalReactionEventId, |
||||
externalReactedEventId: externalMessageId, |
||||
} = messageReactionEventInput; |
||||
|
||||
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId); |
||||
if (!federatedRoom) { |
||||
return; |
||||
} |
||||
|
||||
const federatedUser = await this.internalUserAdapter.getFederatedUserByExternalId(externalSenderId); |
||||
if (!federatedUser) { |
||||
return; |
||||
} |
||||
const message = await this.internalMessageAdapter.getMessageByFederationId(externalMessageId); |
||||
if (!message) { |
||||
return; |
||||
} |
||||
if (!isMessageFromMatrixFederation(message)) { |
||||
return; |
||||
} |
||||
// TODO: move this to a Message entity in the domain layer
|
||||
const userAlreadyReacted = Boolean( |
||||
federatedUser.getUsername() && message.reactions?.[emoji]?.usernames?.includes(federatedUser.getUsername() as string), |
||||
); |
||||
if (userAlreadyReacted) { |
||||
return; |
||||
} |
||||
|
||||
await this.internalMessageAdapter.reactToMessage(federatedUser, message, emoji, externalReactionEventId); |
||||
} |
||||
} |
||||
@ -0,0 +1,61 @@ |
||||
import type { IMessage } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { FederatedUser } from '../domain/FederatedUser'; |
||||
import { Federation } from '../Federation'; |
||||
import type { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; |
||||
|
||||
export interface IRoomRedactionHandlers { |
||||
handle(): Promise<void>; |
||||
} |
||||
|
||||
class DeleteMessageHandler implements IRoomRedactionHandlers { |
||||
constructor( |
||||
private readonly internalMessageAdapter: RocketChatMessageAdapter, |
||||
private readonly message: IMessage, |
||||
private readonly federatedUser: FederatedUser, |
||||
) {} |
||||
|
||||
public async handle(): Promise<void> { |
||||
await this.internalMessageAdapter.deleteMessage(this.message, this.federatedUser); |
||||
} |
||||
} |
||||
|
||||
class UnreactToMessageHandler implements IRoomRedactionHandlers { |
||||
constructor( |
||||
private readonly internalMessageAdapter: RocketChatMessageAdapter, |
||||
private readonly message: IMessage, |
||||
private readonly federatedUser: FederatedUser, |
||||
private readonly redactsEvents: string, |
||||
) {} |
||||
|
||||
public async handle(): Promise<void> { |
||||
const normalizedEventId = Federation.escapeExternalFederationEventId(this.redactsEvents); |
||||
const reaction = Object.keys(this.message.reactions || {}).find( |
||||
(key) => |
||||
this.message.reactions?.[key]?.federationReactionEventIds?.[normalizedEventId] === this.federatedUser.getUsername() && |
||||
this.message.reactions?.[key]?.usernames?.includes(this.federatedUser.getUsername() || ''), |
||||
); |
||||
if (!reaction) { |
||||
return; |
||||
} |
||||
await this.internalMessageAdapter.unreactToMessage(this.federatedUser, this.message, reaction, this.redactsEvents); |
||||
} |
||||
} |
||||
|
||||
export const getRedactMessageHandler = async ( |
||||
internalMessageAdapter: RocketChatMessageAdapter, |
||||
redactsEvent: string, |
||||
federatedUser: FederatedUser, |
||||
): Promise<IRoomRedactionHandlers | undefined> => { |
||||
const message = await internalMessageAdapter.getMessageByFederationId(redactsEvent); |
||||
const messageWithReaction = await internalMessageAdapter.findOneByFederationIdOnReactions(redactsEvent, federatedUser); |
||||
if (!message && !messageWithReaction) { |
||||
return; |
||||
} |
||||
if (messageWithReaction) { |
||||
return new UnreactToMessageHandler(internalMessageAdapter, messageWithReaction, federatedUser, redactsEvent); |
||||
} |
||||
if (message) { |
||||
return new DeleteMessageHandler(internalMessageAdapter, message, federatedUser); |
||||
} |
||||
}; |
||||
@ -0,0 +1,31 @@ |
||||
import type { IFederationReceiverBaseRoomInputDto } from './RoomReceiverDto'; |
||||
import { FederationBaseRoomInputDto } from './RoomReceiverDto'; |
||||
|
||||
interface IFederationRoomMessageReactionInputDto extends IFederationReceiverBaseRoomInputDto { |
||||
externalSenderId: string; |
||||
externalEventId: string; |
||||
externalReactedEventId: string; |
||||
emoji: string; |
||||
} |
||||
|
||||
export class FederationMessageReactionEventDto extends FederationBaseRoomInputDto { |
||||
constructor({ |
||||
externalRoomId, |
||||
normalizedRoomId, |
||||
externalEventId, |
||||
externalReactedEventId, |
||||
emoji, |
||||
externalSenderId, |
||||
}: IFederationRoomMessageReactionInputDto) { |
||||
super({ externalRoomId, normalizedRoomId, externalEventId }); |
||||
this.emoji = emoji; |
||||
this.externalSenderId = externalSenderId; |
||||
this.externalReactedEventId = externalReactedEventId; |
||||
} |
||||
|
||||
emoji: string; |
||||
|
||||
externalSenderId: string; |
||||
|
||||
externalReactedEventId: string; |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
import type { IMessage } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { IFederationBridge } from '../../domain/IFederationBridge'; |
||||
import type { RocketChatFileAdapter } from '../../infrastructure/rocket-chat/adapters/File'; |
||||
|
||||
export interface IExternalMessageSender { |
||||
sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void>; |
||||
} |
||||
|
||||
class TextExternalMessageSender implements IExternalMessageSender { |
||||
constructor(private readonly bridge: IFederationBridge) {} |
||||
|
||||
public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> { |
||||
await this.bridge.sendMessage(externalRoomId, externalSenderId, message.msg); |
||||
} |
||||
} |
||||
|
||||
class FileExternalMessageSender implements IExternalMessageSender { |
||||
constructor(private readonly bridge: IFederationBridge, private readonly internalFileHelper: RocketChatFileAdapter) {} |
||||
|
||||
public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> { |
||||
const file = await this.internalFileHelper.getFileRecordById((message.files || [])[0]?._id); |
||||
if (!file || !file.size || !file.type) { |
||||
return; |
||||
} |
||||
|
||||
const buffer = await this.internalFileHelper.getBufferFromFileRecord(file); |
||||
const metadata = await this.internalFileHelper.extractMetadataFromFile(file); |
||||
|
||||
await this.bridge.sendMessageFileToRoom(externalRoomId, externalSenderId, buffer, { |
||||
filename: file.name, |
||||
fileSize: file.size, |
||||
mimeType: file.type, |
||||
metadata: { |
||||
width: metadata?.width, |
||||
height: metadata?.height, |
||||
format: metadata?.format, |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export const getExternalMessageSender = ( |
||||
message: IMessage, |
||||
bridge: IFederationBridge, |
||||
internalFileHelper: RocketChatFileAdapter, |
||||
): IExternalMessageSender => { |
||||
return message.files ? new FileExternalMessageSender(bridge, internalFileHelper) : new TextExternalMessageSender(bridge); |
||||
}; |
||||
@ -0,0 +1,98 @@ |
||||
import type { IMessage, IUser } from '@rocket.chat/core-typings'; |
||||
import { isMessageFromMatrixFederation } from '@rocket.chat/core-typings'; |
||||
|
||||
import { FederatedUser } from '../../domain/FederatedUser'; |
||||
import type { IFederationBridge } from '../../domain/IFederationBridge'; |
||||
import { Federation } from '../../Federation'; |
||||
import type { RocketChatMessageAdapter } from '../../infrastructure/rocket-chat/adapters/Message'; |
||||
import type { RocketChatRoomAdapter } from '../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatSettingsAdapter } from '../../infrastructure/rocket-chat/adapters/Settings'; |
||||
import type { RocketChatUserAdapter } from '../../infrastructure/rocket-chat/adapters/User'; |
||||
|
||||
export class FederationMessageServiceSender { |
||||
constructor( |
||||
protected internalRoomAdapter: RocketChatRoomAdapter, |
||||
protected internalUserAdapter: RocketChatUserAdapter, |
||||
protected internalSettingsAdapter: RocketChatSettingsAdapter, |
||||
protected internalMessageAdapter: RocketChatMessageAdapter, |
||||
protected bridge: IFederationBridge, |
||||
) {} |
||||
|
||||
public async sendExternalMessageReaction(internalMessage: IMessage, internalUser: IUser, reaction: string): Promise<void> { |
||||
if (!internalMessage || !internalUser || !internalUser._id || !internalMessage.rid) { |
||||
return; |
||||
} |
||||
const federatedSender = await this.internalUserAdapter.getFederatedUserByInternalId(internalUser._id); |
||||
if (!federatedSender) { |
||||
return; |
||||
} |
||||
|
||||
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalMessage.rid); |
||||
if (!federatedRoom) { |
||||
return; |
||||
} |
||||
|
||||
if (!isMessageFromMatrixFederation(internalMessage)) { |
||||
return; |
||||
} |
||||
|
||||
const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer( |
||||
this.bridge.extractHomeserverOrigin(federatedSender.getExternalId()), |
||||
this.internalSettingsAdapter.getHomeServerDomain(), |
||||
); |
||||
if (!isUserFromTheSameHomeServer) { |
||||
return; |
||||
} |
||||
|
||||
const eventId = await this.bridge.sendMessageReaction( |
||||
federatedRoom.getExternalId(), |
||||
federatedSender.getExternalId(), |
||||
internalMessage.federation?.eventId as string, |
||||
reaction, |
||||
); |
||||
federatedSender.getUsername() && |
||||
(await this.internalMessageAdapter.setExternalFederationEventOnMessage( |
||||
federatedSender.getUsername() as string, |
||||
internalMessage, |
||||
reaction, |
||||
eventId, |
||||
)); |
||||
} |
||||
|
||||
public async sendExternalMessageUnReaction(internalMessage: IMessage, internalUser: IUser, reaction: string): Promise<void> { |
||||
if (!internalMessage || !internalUser || !internalUser._id || !internalMessage.rid) { |
||||
return; |
||||
} |
||||
const federatedSender = await this.internalUserAdapter.getFederatedUserByInternalId(internalUser._id); |
||||
if (!federatedSender) { |
||||
return; |
||||
} |
||||
|
||||
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalMessage.rid); |
||||
if (!federatedRoom) { |
||||
return; |
||||
} |
||||
|
||||
if (!isMessageFromMatrixFederation(internalMessage)) { |
||||
return; |
||||
} |
||||
|
||||
const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer( |
||||
this.bridge.extractHomeserverOrigin(federatedSender.getExternalId()), |
||||
this.internalSettingsAdapter.getHomeServerDomain(), |
||||
); |
||||
if (!isUserFromTheSameHomeServer) { |
||||
return; |
||||
} |
||||
// TODO: leaked business logic, move this to the domain layer
|
||||
const externalEventId = Object.keys(internalMessage.reactions?.[reaction].federationReactionEventIds || {}).find( |
||||
(key) => internalMessage.reactions?.[reaction].federationReactionEventIds?.[key] === internalUser.username, |
||||
); |
||||
if (!externalEventId) { |
||||
return; |
||||
} |
||||
const normalizedEventId = Federation.unescapeExternalFederationEventId(externalEventId); |
||||
await this.bridge.redactEvent(federatedRoom.getExternalId(), federatedSender.getExternalId(), normalizedEventId); |
||||
await this.internalMessageAdapter.unsetExternalFederationEventOnMessage(externalEventId, internalMessage, reaction); |
||||
} |
||||
} |
||||
@ -0,0 +1,21 @@ |
||||
import emojione from 'emojione'; |
||||
|
||||
import type { MatrixEventMessageReact } from '../definitions/events/MessageReacted'; |
||||
import { FederationMessageReactionEventDto } from '../../../application/input/MessageReceiverDto'; |
||||
import { convertExternalRoomIdToInternalRoomIdFormat } from './RoomReceiver'; |
||||
|
||||
const convertEmojisMatrixFormatToRCFormat = (emoji: string): string => emojione.toShort(emoji); |
||||
export const convertEmojisRCFormatToMatrixFormat = (emoji: string): string => emojione.shortnameToUnicode(emoji); |
||||
|
||||
export class MatrixMessageReceiverConverter { |
||||
public static toMessageReactionDto(externalEvent: MatrixEventMessageReact): FederationMessageReactionEventDto { |
||||
return new FederationMessageReactionEventDto({ |
||||
externalEventId: externalEvent.event_id, |
||||
externalRoomId: externalEvent.room_id, |
||||
normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), |
||||
externalSenderId: externalEvent.sender, |
||||
emoji: convertEmojisMatrixFormatToRCFormat(externalEvent.content['m.relates_to'].key), |
||||
externalReactedEventId: externalEvent.content['m.relates_to'].event_id, |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
import type { IBaseEventContent } from '../AbstractMatrixEvent'; |
||||
import { AbstractMatrixEvent } from '../AbstractMatrixEvent'; |
||||
import { MatrixEventType } from '../MatrixEventType'; |
||||
|
||||
export interface IMatrixEventContentMessageReacted extends IBaseEventContent { |
||||
'm.relates_to': { |
||||
event_id: string; |
||||
key: string; |
||||
rel_type: string; |
||||
}; |
||||
} |
||||
|
||||
export class MatrixEventMessageReact extends AbstractMatrixEvent { |
||||
public content: IMatrixEventContentMessageReacted; |
||||
|
||||
public type = MatrixEventType.MESSAGE_REACTED; |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
import type { IBaseEventContent } from '../AbstractMatrixEvent'; |
||||
import { AbstractMatrixEvent } from '../AbstractMatrixEvent'; |
||||
import { MatrixEventType } from '../MatrixEventType'; |
||||
|
||||
export type IMatrixEventContentRoomRedacted = IBaseEventContent; |
||||
|
||||
export class MatrixEventRoomRedacted extends AbstractMatrixEvent { |
||||
public content: IMatrixEventContentRoomRedacted; |
||||
|
||||
public type = MatrixEventType.ROOM_EVENT_REDACTED; |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
import type { FederationMessageServiceListener } from '../../../application/MessageServiceListener'; |
||||
import { MatrixMessageReceiverConverter } from '../converters/MessageReceiver'; |
||||
import type { MatrixEventMessageReact } from '../definitions/events/MessageReacted'; |
||||
import { MatrixEventType } from '../definitions/MatrixEventType'; |
||||
import { MatrixBaseEventHandler } from './BaseEvent'; |
||||
|
||||
export class MatrixMessageReactedHandler extends MatrixBaseEventHandler { |
||||
public eventType: string = MatrixEventType.MESSAGE_REACTED; |
||||
|
||||
constructor(private messageService: FederationMessageServiceListener) { |
||||
super(); |
||||
} |
||||
|
||||
public async handle(externalEvent: MatrixEventMessageReact): Promise<void> { |
||||
await this.messageService.onMessageReaction(MatrixMessageReceiverConverter.toMessageReactionDto(externalEvent)); |
||||
} |
||||
} |
||||
@ -0,0 +1,64 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import type { IMessage, IUpload, IUser } from '@rocket.chat/core-typings'; |
||||
import { Uploads } from '@rocket.chat/models'; |
||||
|
||||
import { FileUpload } from '../../../../../file-upload/server'; |
||||
import { parseFileIntoMessageAttachments } from '../../../../../file-upload/server/methods/sendFileMessage'; |
||||
|
||||
export class RocketChatFileAdapter { |
||||
public async uploadFile( |
||||
readableStream: ReadableStream, |
||||
internalRoomId: string, |
||||
internalUser: IUser, |
||||
fileRecord: Partial<IUpload>, |
||||
): Promise<{ files: IMessage['files']; attachments: IMessage['attachments'] }> { |
||||
return new Promise<{ files: IMessage['files']; attachments: IMessage['attachments'] }>((resolve, reject) => { |
||||
const fileStore = FileUpload.getStore('Uploads'); |
||||
// this needs to be here due to a high coupling in the third party lib that rely on the logged in user
|
||||
Meteor.runAsUser(internalUser._id, async () => { |
||||
const uploadedFile = fileStore.insertSync(fileRecord, readableStream); |
||||
try { |
||||
const { files, attachments } = await parseFileIntoMessageAttachments(uploadedFile, internalRoomId, internalUser); |
||||
|
||||
resolve({ files, attachments }); |
||||
} catch (error) { |
||||
reject(error); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async getBufferFromFileRecord(fileRecord: IUpload): Promise<Buffer> { |
||||
return new Promise((resolve, reject) => { |
||||
FileUpload.getBuffer(fileRecord, (err: Error, buffer: Buffer) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
resolve(buffer); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async getFileRecordById(fileId: string): Promise<IUpload | undefined | null> { |
||||
return Uploads.findOneById(fileId); |
||||
} |
||||
|
||||
public async extractMetadataFromFile(file: IUpload): Promise<{ height?: number; width?: number; format?: string }> { |
||||
if (file.type?.startsWith('image/')) { |
||||
const metadata = await FileUpload.extractMetadata(file); |
||||
|
||||
return { |
||||
format: metadata.format, |
||||
height: metadata.height, |
||||
width: metadata.width, |
||||
}; |
||||
} |
||||
if (file.type?.startsWith('video/')) { |
||||
return { |
||||
height: 200, |
||||
width: 250, |
||||
}; |
||||
} |
||||
return {}; |
||||
} |
||||
} |
||||
@ -1,9 +1,99 @@ |
||||
import { sendMessage } from '../../../../../lib/server'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import type { IMessage } from '@rocket.chat/core-typings'; |
||||
import { Messages } from '@rocket.chat/models'; |
||||
|
||||
import { deleteMessage, sendMessage, updateMessage } from '../../../../../lib/server'; |
||||
import { executeSetReaction } from '../../../../../reactions/server/setReaction'; |
||||
import type { FederatedRoom } from '../../../domain/FederatedRoom'; |
||||
import type { FederatedUser } from '../../../domain/FederatedUser'; |
||||
|
||||
const DEFAULT_EMOJI_TO_REACT_WHEN_RECEIVED_EMOJI_DOES_NOT_EXIST = ':grey_question:'; |
||||
|
||||
export class RocketChatMessageAdapter { |
||||
public async sendMessage(user: FederatedUser, room: FederatedRoom, messageText: string): Promise<void> { |
||||
sendMessage(user.getInternalReference(), { msg: messageText }, room.getInternalReference()); |
||||
public async sendMessage(user: FederatedUser, room: FederatedRoom, messageText: string, externalEventId: string): Promise<void> { |
||||
sendMessage(user.getInternalReference(), { federation: { eventId: externalEventId }, msg: messageText }, room.getInternalReference()); |
||||
} |
||||
|
||||
public async editMessage(user: FederatedUser, newMessageText: string, originalMessage: IMessage): Promise<void> { |
||||
const updatedMessage = Object.assign({}, originalMessage, { msg: newMessageText }); |
||||
updateMessage(updatedMessage, user.getInternalReference(), originalMessage); |
||||
} |
||||
|
||||
public async sendFileMessage( |
||||
user: FederatedUser, |
||||
room: FederatedRoom, |
||||
files: IMessage['files'], |
||||
attachments: IMessage['attachments'], |
||||
externalEventId: string, |
||||
): Promise<void> { |
||||
Promise.resolve( |
||||
sendMessage( |
||||
user.getInternalReference(), |
||||
{ |
||||
federation: { eventId: externalEventId }, |
||||
rid: room.getInternalId(), |
||||
ts: new Date(), |
||||
file: (files || [])[0], |
||||
files, |
||||
attachments, |
||||
}, |
||||
room.getInternalReference(), |
||||
), |
||||
); |
||||
} |
||||
|
||||
public async deleteMessage(message: IMessage, user: FederatedUser): Promise<void> { |
||||
deleteMessage(message, user.getInternalReference()); |
||||
} |
||||
|
||||
public async reactToMessage(user: FederatedUser, message: IMessage, reaction: string, externalEventId: string): Promise<void> { |
||||
// we need to run this as the user due to a high coupling in this function that relies on the logged in user
|
||||
Meteor.runAsUser(user.getInternalId(), async () => { |
||||
try { |
||||
await executeSetReaction(reaction, message._id); |
||||
user.getUsername() && |
||||
(await Messages.setFederationReactionEventId(user.getUsername() as string, message._id, reaction, externalEventId)); |
||||
} catch (error: any) { |
||||
if (error?.message?.includes('Invalid emoji provided.')) { |
||||
await executeSetReaction(DEFAULT_EMOJI_TO_REACT_WHEN_RECEIVED_EMOJI_DOES_NOT_EXIST, message._id); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public async unreactToMessage(user: FederatedUser, message: IMessage, reaction: string, externalEventId: string): Promise<void> { |
||||
// we need to run this as the user due to a high coupling in this function that relies on the logged in user
|
||||
Meteor.runAsUser(user.getInternalId(), async () => { |
||||
await executeSetReaction(reaction, message._id); |
||||
await Messages.unsetFederationReactionEventId(externalEventId, message._id, reaction); |
||||
}); |
||||
} |
||||
|
||||
public async findOneByFederationIdOnReactions(federationEventId: string, user: FederatedUser): Promise<IMessage | null | undefined> { |
||||
return ( |
||||
(user.getUsername() && Messages.findOneByFederationIdAndUsernameOnReactions(federationEventId, user.getUsername() as string)) || |
||||
undefined |
||||
); |
||||
} |
||||
|
||||
public async getMessageByFederationId(federationEventId: string): Promise<IMessage | null> { |
||||
return Messages.findOneByFederationId(federationEventId); |
||||
} |
||||
|
||||
public async unsetExternalFederationEventOnMessage(externalEventId: string, message: IMessage, reaction: string): Promise<void> { |
||||
await Messages.unsetFederationReactionEventId(externalEventId, message._id, reaction); |
||||
} |
||||
|
||||
public async getMessageById(internalMessageId: string): Promise<IMessage | null> { |
||||
return Messages.findOneById(internalMessageId); |
||||
} |
||||
|
||||
public async setExternalFederationEventOnMessage( |
||||
username: string, |
||||
message: IMessage, |
||||
reaction: string, |
||||
externalEventId: string, |
||||
): Promise<void> { |
||||
await Messages.setFederationReactionEventId(username, message._id, reaction, externalEventId); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,7 @@ |
||||
export const escapeExternalFederationEventId = (externalEventId: string): string => { |
||||
return externalEventId.replace(/\$/g, '__sign__'); |
||||
}; |
||||
|
||||
export const unescapeExternalFederationEventId = (externalEventId: string): string => { |
||||
return externalEventId.replace(/__sign__/g, '$'); |
||||
}; |
||||
@ -1,43 +0,0 @@ |
||||
import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; |
||||
|
||||
import type { IFederationReceiverBaseRoomInputDto } from '../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; |
||||
import { FederationBaseRoomInputDto } from '../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; |
||||
|
||||
export interface IFederationCreateInputDto extends IFederationReceiverBaseRoomInputDto { |
||||
roomType: RoomType; |
||||
} |
||||
|
||||
export interface IFederationRoomNameChangeInputDto extends IFederationReceiverBaseRoomInputDto { |
||||
normalizedRoomName: string; |
||||
} |
||||
|
||||
export interface IFederationRoomChangeTopicInputDto extends IFederationReceiverBaseRoomInputDto { |
||||
roomTopic: string; |
||||
} |
||||
|
||||
export class FederationRoomChangeJoinRulesDto extends FederationBaseRoomInputDto { |
||||
constructor({ roomType, externalRoomId, normalizedRoomId }: IFederationCreateInputDto) { |
||||
super({ externalRoomId, normalizedRoomId }); |
||||
this.roomType = roomType; |
||||
} |
||||
|
||||
roomType: RoomType; |
||||
} |
||||
|
||||
export class FederationRoomChangeNameDto extends FederationBaseRoomInputDto { |
||||
constructor({ externalRoomId, normalizedRoomId, normalizedRoomName }: IFederationRoomNameChangeInputDto) { |
||||
super({ externalRoomId, normalizedRoomId }); |
||||
this.normalizedRoomName = normalizedRoomName; |
||||
} |
||||
|
||||
normalizedRoomName: string; |
||||
} |
||||
|
||||
export class FederationRoomChangeTopicDto extends FederationBaseRoomInputDto { |
||||
constructor({ externalRoomId, normalizedRoomId, roomTopic }: IFederationRoomChangeTopicInputDto) { |
||||
super({ externalRoomId, normalizedRoomId }); |
||||
this.roomTopic = roomTopic; |
||||
} |
||||
|
||||
roomTopic: string; |
||||
} |
||||
@ -1,23 +0,0 @@ |
||||
import { FederationService } from '../../../../../../app/federation-v2/server/application/AbstractFederationService'; |
||||
import type { RocketChatSettingsAdapter } from '../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; |
||||
import type { IFederationBridgeEE } from '../../domain/IFederationBridge'; |
||||
import type { RocketChatRoomAdapterEE } from '../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatUserAdapterEE } from '../../infrastructure/rocket-chat/adapters/User'; |
||||
import type { FederationCreateDirectMessageDto } from '../input/RoomSenderDto'; |
||||
|
||||
export class FederationRoomServiceSenderEE extends FederationService { |
||||
constructor( |
||||
protected internalRoomAdapter: RocketChatRoomAdapterEE, |
||||
protected internalUserAdapter: RocketChatUserAdapterEE, |
||||
protected internalSettingsAdapter: RocketChatSettingsAdapter, |
||||
protected bridge: IFederationBridgeEE, |
||||
) { |
||||
super(bridge, internalUserAdapter, internalSettingsAdapter); |
||||
} |
||||
|
||||
public async createLocalDirectMessageRoom(dmRoomCreateInput: FederationCreateDirectMessageDto): Promise<void> { |
||||
const { internalInviterId, invitees } = dmRoomCreateInput; |
||||
|
||||
await this.internalRoomAdapter.createLocalDirectMessageRoom(invitees, internalInviterId); |
||||
} |
||||
} |
||||
@ -1,14 +1,14 @@ |
||||
import type { RocketChatSettingsAdapter } from '../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; |
||||
import { FederatedUserEE } from '../../domain/FederatedUser'; |
||||
import type { IFederationBridgeEE } from '../../domain/IFederationBridge'; |
||||
import type { RocketChatRoomAdapterEE } from '../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatUserAdapterEE } from '../../infrastructure/rocket-chat/adapters/User'; |
||||
import type { RocketChatSettingsAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; |
||||
import { FederatedUserEE } from '../../../domain/FederatedUser'; |
||||
import type { IFederationBridgeEE } from '../../../domain/IFederationBridge'; |
||||
import type { RocketChatRoomAdapterEE } from '../../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatUserAdapterEE } from '../../../infrastructure/rocket-chat/adapters/User'; |
||||
import type { |
||||
FederationBeforeDirectMessageRoomCreationDto, |
||||
FederationOnDirectMessageRoomCreationDto, |
||||
FederationRoomInviteUserDto, |
||||
} from '../input/RoomSenderDto'; |
||||
import { FederationServiceEE } from './AbstractFederationService'; |
||||
} from '../../input/RoomSenderDto'; |
||||
import { FederationServiceEE } from '../AbstractFederationService'; |
||||
|
||||
export class FederationDMRoomInternalHooksServiceSender extends FederationServiceEE { |
||||
constructor( |
||||
@ -1,23 +1,25 @@ |
||||
import type { RocketChatSettingsAdapter } from '../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; |
||||
import { FederatedRoomEE } from '../../domain/FederatedRoom'; |
||||
import { FederatedUserEE } from '../../domain/FederatedUser'; |
||||
import type { IFederationBridgeEE } from '../../domain/IFederationBridge'; |
||||
import type { RocketChatRoomAdapterEE } from '../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatUserAdapterEE } from '../../infrastructure/rocket-chat/adapters/User'; |
||||
import type { RocketChatMessageAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Message'; |
||||
import type { RocketChatSettingsAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; |
||||
import { FederatedRoomEE } from '../../../domain/FederatedRoom'; |
||||
import { FederatedUserEE } from '../../../domain/FederatedUser'; |
||||
import type { IFederationBridgeEE } from '../../../domain/IFederationBridge'; |
||||
import type { RocketChatRoomAdapterEE } from '../../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatUserAdapterEE } from '../../../infrastructure/rocket-chat/adapters/User'; |
||||
import type { |
||||
FederationBeforeAddUserToARoomDto, |
||||
FederationOnRoomCreationDto, |
||||
FederationOnUsersAddedToARoomDto, |
||||
FederationRoomInviteUserDto, |
||||
FederationSetupRoomDto, |
||||
} from '../input/RoomSenderDto'; |
||||
import { FederationServiceEE } from './AbstractFederationService'; |
||||
} from '../../input/RoomSenderDto'; |
||||
import { FederationServiceEE } from '../AbstractFederationService'; |
||||
|
||||
export class FederationRoomInternalHooksServiceSender extends FederationServiceEE { |
||||
constructor( |
||||
protected internalRoomAdapter: RocketChatRoomAdapterEE, |
||||
protected internalUserAdapter: RocketChatUserAdapterEE, |
||||
protected internalSettingsAdapter: RocketChatSettingsAdapter, |
||||
protected internalMessageAdapter: RocketChatMessageAdapter, |
||||
protected bridge: IFederationBridgeEE, |
||||
) { |
||||
super(bridge, internalUserAdapter, internalSettingsAdapter); |
||||
@ -0,0 +1,25 @@ |
||||
import { FederationRoomServiceSender } from '../../../../../../../app/federation-v2/server/application/sender/RoomServiceSender'; |
||||
import type { RocketChatFileAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/File'; |
||||
import type { RocketChatSettingsAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; |
||||
import type { IFederationBridgeEE } from '../../../domain/IFederationBridge'; |
||||
import type { RocketChatRoomAdapterEE } from '../../../infrastructure/rocket-chat/adapters/Room'; |
||||
import type { RocketChatUserAdapterEE } from '../../../infrastructure/rocket-chat/adapters/User'; |
||||
import type { FederationCreateDirectMessageDto } from '../../input/RoomSenderDto'; |
||||
|
||||
export class FederationRoomServiceSenderEE extends FederationRoomServiceSender { |
||||
constructor( |
||||
protected internalRoomAdapter: RocketChatRoomAdapterEE, |
||||
protected internalUserAdapter: RocketChatUserAdapterEE, |
||||
protected internalSettingsAdapter: RocketChatSettingsAdapter, |
||||
protected internalFileAdapter: RocketChatFileAdapter, |
||||
protected bridge: IFederationBridgeEE, |
||||
) { |
||||
super(internalRoomAdapter, internalUserAdapter, internalSettingsAdapter, internalFileAdapter, bridge); |
||||
} |
||||
|
||||
public async createLocalDirectMessageRoom(dmRoomCreateInput: FederationCreateDirectMessageDto): Promise<void> { |
||||
const { internalInviterId, invitees } = dmRoomCreateInput; |
||||
|
||||
await this.internalRoomAdapter.createLocalDirectMessageRoom(invitees, internalInviterId); |
||||
} |
||||
} |
||||
@ -1,49 +1,50 @@ |
||||
// /* eslint-disable */
|
||||
// import proxyquire from 'proxyquire';
|
||||
// import { expect } from 'chai';
|
||||
// import sinon from 'sinon';
|
||||
/* eslint-disable */ |
||||
import proxyquire from 'proxyquire'; |
||||
import { expect } from 'chai'; |
||||
import sinon from 'sinon'; |
||||
|
||||
// const remove = sinon.stub();
|
||||
// proxyquire.noCallThru().load('../../../../../../../../../lib/callbacks', {
|
||||
// 'meteor/meteor': {},
|
||||
// 'meteor/random': {
|
||||
// Random: {
|
||||
// id: () => 1,
|
||||
// },
|
||||
// },
|
||||
// callbacks: {
|
||||
// remove,
|
||||
// },
|
||||
// });
|
||||
const remove = sinon.stub(); |
||||
const { FederationHooksEE } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/hooks', { |
||||
'meteor/meteor': { |
||||
'@global': true, |
||||
}, |
||||
'meteor/random': { |
||||
Random: { |
||||
id: () => 1, |
||||
}, |
||||
'@global': true, |
||||
}, |
||||
'../../../../../../../lib/callbacks': { |
||||
callbacks: { remove }, |
||||
}, |
||||
}); |
||||
|
||||
// import { FederationHooksEE } from '../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/hooks';
|
||||
|
||||
// describe.only('FederationEE - Infrastructure - RocketChat - Hooks', () => {
|
||||
// describe('#removeAll()', () => {
|
||||
// it('should remove the specific validation for EE environments', () => {
|
||||
// FederationHooksEE.removeAll();
|
||||
// expect(remove.callCount).to.be.equal(7);
|
||||
// expect(
|
||||
// remove.getCall(0).calledWith('beforeCreateDirectRoom', 'federation-v2-before-create-direct-message-room'),
|
||||
// ).to.be.equal(true);
|
||||
// expect(
|
||||
// remove.getCall(1).calledWith('afterCreateDirectRoom', 'federation-v2-after-create-direct-message-room'),
|
||||
// ).to.be.equal(true);
|
||||
// expect(
|
||||
// remove.getCall(2).calledWith('afterAddedToRoom', 'federation-v2-after-add-users-to-a-room'),
|
||||
// ).to.be.equal(true);
|
||||
// expect(
|
||||
// remove.getCall(3).calledWith('federation.afterCreateFederatedRoom', 'federation-v2-after-create-room'),
|
||||
// ).to.be.equal(true);
|
||||
// expect(
|
||||
// remove.getCall(4).calledWith('federation.beforeAddUserAToRoom', 'federation-v2-before-add-user-to-the-room'),
|
||||
// ).to.be.equal(true);
|
||||
// expect(
|
||||
// remove.getCall(5).calledWith('afterRoomNameChange', 'federation-v2-after-room-name-changed'),
|
||||
// ).to.be.equal(true);
|
||||
// expect(
|
||||
// remove.getCall(6).calledWith('afterRoomTopicChange', 'federation-v2-after-room-topic-changed'),
|
||||
// ).to.be.equal(true);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { |
||||
describe('#removeAll()', () => { |
||||
it('should remove the specific validation for EE environments', () => { |
||||
FederationHooksEE.removeAll(); |
||||
expect(remove.callCount).to.be.equal(7); |
||||
expect( |
||||
remove.getCall(0).calledWith('beforeCreateDirectRoom', 'federation-v2-before-create-direct-message-room'), |
||||
).to.be.equal(true); |
||||
expect( |
||||
remove.getCall(1).calledWith('afterCreateDirectRoom', 'federation-v2-after-create-direct-message-room'), |
||||
).to.be.equal(true); |
||||
expect( |
||||
remove.getCall(2).calledWith('afterAddedToRoom', 'federation-v2-after-add-users-to-a-room'), |
||||
).to.be.equal(true); |
||||
expect( |
||||
remove.getCall(3).calledWith('federation.afterCreateFederatedRoom', 'federation-v2-after-create-room'), |
||||
).to.be.equal(true); |
||||
expect( |
||||
remove.getCall(4).calledWith('federation.beforeAddUserAToRoom', 'federation-v2-before-add-user-to-the-room'), |
||||
).to.be.equal(true); |
||||
expect( |
||||
remove.getCall(5).calledWith('afterRoomNameChange', 'federation-v2-after-room-name-changed'), |
||||
).to.be.equal(true); |
||||
expect( |
||||
remove.getCall(6).calledWith('afterRoomTopicChange', 'federation-v2-after-room-topic-changed'), |
||||
).to.be.equal(true); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
@ -0,0 +1,155 @@ |
||||
/* eslint-disable import/first */ |
||||
import { expect } from 'chai'; |
||||
import sinon from 'sinon'; |
||||
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; |
||||
import proxyquire from 'proxyquire'; |
||||
|
||||
const { FederatedUser } = proxyquire.noCallThru().load('../../../../../../../app/federation-v2/server/domain/FederatedUser', { |
||||
mongodb: { |
||||
'ObjectId': class ObjectId { |
||||
toHexString(): string { |
||||
return 'hexString'; |
||||
} |
||||
}, |
||||
'@global': true, |
||||
}, |
||||
}); |
||||
|
||||
const { FederatedRoom } = proxyquire.noCallThru().load('../../../../../../../app/federation-v2/server/domain/FederatedRoom', { |
||||
mongodb: { |
||||
'ObjectId': class ObjectId { |
||||
toHexString(): string { |
||||
return 'hexString'; |
||||
} |
||||
}, |
||||
'@global': true, |
||||
}, |
||||
}); |
||||
|
||||
import { FederationMessageServiceListener } from '../../../../../../../app/federation-v2/server/application/MessageServiceListener'; |
||||
|
||||
describe('Federation - Application - FederationMessageServiceListener', () => { |
||||
let service: FederationMessageServiceListener; |
||||
const roomAdapter = { |
||||
getFederatedRoomByExternalId: sinon.stub(), |
||||
}; |
||||
const userAdapter = { |
||||
getFederatedUserByExternalId: sinon.stub(), |
||||
}; |
||||
const messageAdapter = { |
||||
getMessageByFederationId: sinon.stub(), |
||||
reactToMessage: sinon.stub(), |
||||
}; |
||||
const settingsAdapter = { |
||||
getHomeServerDomain: sinon.stub().returns('localDomain'), |
||||
}; |
||||
|
||||
beforeEach(() => { |
||||
service = new FederationMessageServiceListener( |
||||
roomAdapter as any, |
||||
userAdapter as any, |
||||
messageAdapter as any, |
||||
settingsAdapter as any, |
||||
{} as any, |
||||
); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
roomAdapter.getFederatedRoomByExternalId.reset(); |
||||
userAdapter.getFederatedUserByExternalId.reset(); |
||||
messageAdapter.getMessageByFederationId.reset(); |
||||
messageAdapter.reactToMessage.reset(); |
||||
}); |
||||
|
||||
describe('#onMessageReaction()', () => { |
||||
const user = FederatedUser.createInstance('externalInviterId', { |
||||
name: 'normalizedInviterId', |
||||
username: 'normalizedInviterId', |
||||
existsOnlyOnProxyServer: false, |
||||
}); |
||||
const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); |
||||
|
||||
it('should NOT react to the message if the room does not exists', async () => { |
||||
roomAdapter.getFederatedRoomByExternalId.resolves(undefined); |
||||
await service.onMessageReaction({ |
||||
externalReactedEventId: 'externalReactedEventId', |
||||
} as any); |
||||
|
||||
expect(messageAdapter.reactToMessage.called).to.be.false; |
||||
}); |
||||
|
||||
it('should NOT react to the message if the user does not exists', async () => { |
||||
roomAdapter.getFederatedRoomByExternalId.resolves(room); |
||||
userAdapter.getFederatedUserByExternalId.resolves(undefined); |
||||
await service.onMessageReaction({ |
||||
externalReactedEventId: 'externalReactedEventId', |
||||
} as any); |
||||
|
||||
expect(messageAdapter.reactToMessage.called).to.be.false; |
||||
}); |
||||
|
||||
it('should NOT react to the message if the message does not exists', async () => { |
||||
roomAdapter.getFederatedRoomByExternalId.resolves(room); |
||||
userAdapter.getFederatedUserByExternalId.resolves(user); |
||||
messageAdapter.getMessageByFederationId.resolves(undefined); |
||||
await service.onMessageReaction({ |
||||
externalReactedEventId: 'externalReactedEventId', |
||||
} as any); |
||||
|
||||
expect(messageAdapter.reactToMessage.called).to.be.false; |
||||
}); |
||||
|
||||
it('should NOT react to the message if it is not a Matrix federation one', async () => { |
||||
roomAdapter.getFederatedRoomByExternalId.resolves(room); |
||||
userAdapter.getFederatedUserByExternalId.resolves(user); |
||||
messageAdapter.getMessageByFederationId.resolves({ msg: 'newMessageText' }); |
||||
await service.onMessageReaction({ |
||||
externalReactedEventId: 'externalReactedEventId', |
||||
} as any); |
||||
|
||||
expect(messageAdapter.reactToMessage.called).to.be.false; |
||||
}); |
||||
|
||||
it('should NOT react to the message if the user already reacted to it', async () => { |
||||
roomAdapter.getFederatedRoomByExternalId.resolves(room); |
||||
userAdapter.getFederatedUserByExternalId.resolves(user); |
||||
messageAdapter.getMessageByFederationId.resolves({ |
||||
msg: 'newMessageText', |
||||
federation: { eventId: 'eventId' }, |
||||
reactions: { |
||||
':emoji:': { |
||||
usernames: ['normalizedInviterId'], |
||||
}, |
||||
}, |
||||
}); |
||||
await service.onMessageReaction({ |
||||
externalReactedEventId: 'externalReactedEventId', |
||||
emoji: ':emoji:', |
||||
} as any); |
||||
|
||||
expect(messageAdapter.reactToMessage.called).to.be.false; |
||||
}); |
||||
|
||||
it('should react to the message', async () => { |
||||
const message = { |
||||
msg: 'newMessageText', |
||||
federation: { eventId: 'eventId' }, |
||||
reactions: { |
||||
':emoji:': { |
||||
usernames: [], |
||||
}, |
||||
}, |
||||
}; |
||||
roomAdapter.getFederatedRoomByExternalId.resolves(room); |
||||
userAdapter.getFederatedUserByExternalId.resolves(user); |
||||
messageAdapter.getMessageByFederationId.resolves(message); |
||||
await service.onMessageReaction({ |
||||
externalEventId: 'externalEventId', |
||||
externalReactedEventId: 'externalReactedEventId', |
||||
emoji: ':emoji:', |
||||
} as any); |
||||
|
||||
expect(messageAdapter.reactToMessage.calledWith(user, message, ':emoji:', 'externalEventId')).to.be.true; |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,91 @@ |
||||
/* eslint-disable import/first */ |
||||
import { expect } from 'chai'; |
||||
import sinon from 'sinon'; |
||||
|
||||
import { getExternalMessageSender } from '../../../../../../../../app/federation-v2/server/application/sender/MessageSenders'; |
||||
|
||||
describe('Federation - Application - Message Senders', () => { |
||||
const bridge = { |
||||
sendMessage: sinon.stub(), |
||||
sendMessageFileToRoom: sinon.stub(), |
||||
}; |
||||
const fileAdapter = { |
||||
getBufferFromFileRecord: sinon.stub(), |
||||
getFileRecordById: sinon.stub(), |
||||
extractMetadataFromFile: sinon.stub(), |
||||
}; |
||||
|
||||
afterEach(() => { |
||||
bridge.sendMessage.reset(); |
||||
bridge.sendMessageFileToRoom.reset(); |
||||
fileAdapter.getBufferFromFileRecord.reset(); |
||||
fileAdapter.getFileRecordById.reset(); |
||||
fileAdapter.extractMetadataFromFile.reset(); |
||||
}); |
||||
|
||||
describe('TextExternalMessageSender', () => { |
||||
const roomId = 'roomId'; |
||||
const senderId = 'senderId'; |
||||
const message = { msg: 'text' } as any; |
||||
|
||||
describe('#sendMessage()', () => { |
||||
it('should send a message through the bridge', async () => { |
||||
await getExternalMessageSender({} as any, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); |
||||
expect(bridge.sendMessage.calledWith(roomId, senderId, message.msg)).to.be.true; |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('FileExternalMessageSender', () => { |
||||
const roomId = 'roomId'; |
||||
const senderId = 'senderId'; |
||||
const message = { msg: 'text', files: [{ _id: 'fileId' }] } as any; |
||||
|
||||
describe('#sendMessage()', () => { |
||||
it('should not upload the file to the bridge if the file does not exists', async () => { |
||||
fileAdapter.getFileRecordById.resolves(undefined); |
||||
await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); |
||||
expect(bridge.sendMessageFileToRoom.called).to.be.false; |
||||
expect(fileAdapter.getBufferFromFileRecord.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not upload the file to the bridge if the file size does not exists', async () => { |
||||
fileAdapter.getFileRecordById.resolves({}); |
||||
await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); |
||||
expect(bridge.sendMessageFileToRoom.called).to.be.false; |
||||
expect(fileAdapter.getBufferFromFileRecord.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not upload the file to the bridge if the file type does not exists', async () => { |
||||
fileAdapter.getFileRecordById.resolves({ size: 12 }); |
||||
await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); |
||||
expect(bridge.sendMessageFileToRoom.called).to.be.false; |
||||
expect(fileAdapter.getBufferFromFileRecord.called).to.be.false; |
||||
}); |
||||
|
||||
it('should send a message (upload the file) through the bridge', async () => { |
||||
fileAdapter.getFileRecordById.resolves({ name: 'filename', size: 12, type: 'image/png' }); |
||||
fileAdapter.getBufferFromFileRecord.resolves({ buffer: 'buffer' }); |
||||
await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); |
||||
expect(fileAdapter.getBufferFromFileRecord.calledWith({ name: 'filename', size: 12, type: 'image/png' })).to.be.true; |
||||
expect( |
||||
bridge.sendMessageFileToRoom.calledWith( |
||||
roomId, |
||||
senderId, |
||||
{ buffer: 'buffer' }, |
||||
{ |
||||
filename: 'filename', |
||||
fileSize: 12, |
||||
mimeType: 'image/png', |
||||
metadata: { |
||||
width: undefined, |
||||
height: undefined, |
||||
format: undefined, |
||||
}, |
||||
}, |
||||
), |
||||
).to.be.true; |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,286 @@ |
||||
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; |
||||
import { expect } from 'chai'; |
||||
import sinon from 'sinon'; |
||||
import proxyquire from 'proxyquire'; |
||||
|
||||
import { FederationMessageServiceSender } from '../../../../../../../../app/federation-v2/server/application/sender/MessageServiceSender'; |
||||
|
||||
const { FederatedUser } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/domain/FederatedUser', { |
||||
mongodb: { |
||||
'ObjectId': class ObjectId { |
||||
toHexString(): string { |
||||
return 'hexString'; |
||||
} |
||||
}, |
||||
'@global': true, |
||||
}, |
||||
}); |
||||
|
||||
const { FederatedRoom } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/domain/FederatedRoom', { |
||||
mongodb: { |
||||
'ObjectId': class ObjectId { |
||||
toHexString(): string { |
||||
return 'hexString'; |
||||
} |
||||
}, |
||||
'@global': true, |
||||
}, |
||||
}); |
||||
|
||||
describe('Federation - Application - FederationMessageServiceSender', () => { |
||||
let service: FederationMessageServiceSender; |
||||
const roomAdapter = { |
||||
getFederatedRoomByInternalId: sinon.stub(), |
||||
}; |
||||
const userAdapter = { |
||||
getFederatedUserByInternalId: sinon.stub(), |
||||
}; |
||||
const settingsAdapter = { |
||||
getHomeServerDomain: sinon.stub().returns('localDomain'), |
||||
}; |
||||
const messageAdapter = { |
||||
setExternalFederationEventOnMessage: sinon.stub(), |
||||
unsetExternalFederationEventOnMessage: sinon.stub(), |
||||
}; |
||||
const bridge = { |
||||
extractHomeserverOrigin: sinon.stub(), |
||||
sendMessageReaction: sinon.stub(), |
||||
redactEvent: sinon.stub(), |
||||
}; |
||||
|
||||
beforeEach(() => { |
||||
service = new FederationMessageServiceSender( |
||||
roomAdapter as any, |
||||
userAdapter as any, |
||||
settingsAdapter as any, |
||||
messageAdapter as any, |
||||
bridge as any, |
||||
); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
roomAdapter.getFederatedRoomByInternalId.reset(); |
||||
userAdapter.getFederatedUserByInternalId.reset(); |
||||
bridge.extractHomeserverOrigin.reset(); |
||||
messageAdapter.setExternalFederationEventOnMessage.reset(); |
||||
messageAdapter.unsetExternalFederationEventOnMessage.reset(); |
||||
bridge.sendMessageReaction.reset(); |
||||
bridge.redactEvent.reset(); |
||||
}); |
||||
|
||||
describe('#sendExternalMessageReaction()', () => { |
||||
const user = FederatedUser.createInstance('externalInviterId', { |
||||
name: 'normalizedInviterId', |
||||
username: 'normalizedInviterId', |
||||
existsOnlyOnProxyServer: true, |
||||
}); |
||||
const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); |
||||
|
||||
it('should not send the reaction if the internal message does not exists', async () => { |
||||
await service.sendExternalMessageReaction(undefined as any, {} as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction if the internal user does not exists', async () => { |
||||
await service.sendExternalMessageReaction({} as any, undefined as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction if the internal user id does not exists', async () => { |
||||
await service.sendExternalMessageReaction({} as any, {} as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction if the internal message room id does not exists', async () => { |
||||
await service.sendExternalMessageReaction({} as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction the user does not exists', async () => { |
||||
userAdapter.getFederatedUserByInternalId.resolves(undefined); |
||||
await service.sendExternalMessageReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction the room does not exists', async () => { |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(undefined); |
||||
await service.sendExternalMessageReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction the the message is not from matrix federation', async () => { |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
await service.sendExternalMessageReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the reaction if the user is not from the same home server', async () => { |
||||
bridge.extractHomeserverOrigin.returns('externalDomain'); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
await service.sendExternalMessageReaction( |
||||
{ rid: 'roomId', federation: { eventId: 'eventId' } } as any, |
||||
{ _id: 'id' } as any, |
||||
'reaction', |
||||
); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should send the reaction', async () => { |
||||
bridge.extractHomeserverOrigin.returns('localDomain'); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
bridge.sendMessageReaction.resolves('returnedEventId'); |
||||
await service.sendExternalMessageReaction( |
||||
{ rid: 'roomId', federation: { eventId: 'eventId' } } as any, |
||||
{ _id: 'id' } as any, |
||||
'reaction', |
||||
); |
||||
|
||||
expect(bridge.sendMessageReaction.calledWith(room.getExternalId(), user.getExternalId(), 'eventId', 'reaction')).to.be.true; |
||||
expect( |
||||
messageAdapter.setExternalFederationEventOnMessage.calledWith( |
||||
user.getUsername(), |
||||
{ rid: 'roomId', federation: { eventId: 'eventId' } }, |
||||
'reaction', |
||||
'returnedEventId', |
||||
), |
||||
).to.be.true; |
||||
}); |
||||
}); |
||||
|
||||
describe('#sendExternalMessageUnReaction()', () => { |
||||
const user = FederatedUser.createInstance('externalInviterId', { |
||||
name: 'normalizedInviterId', |
||||
username: 'normalizedInviterId', |
||||
existsOnlyOnProxyServer: true, |
||||
}); |
||||
const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); |
||||
|
||||
it('should not send the unreaction if the internal message does not exists', async () => { |
||||
await service.sendExternalMessageUnReaction(undefined as any, {} as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction if the internal user does not exists', async () => { |
||||
await service.sendExternalMessageUnReaction({} as any, undefined as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction if the internal user id does not exists', async () => { |
||||
await service.sendExternalMessageUnReaction({} as any, {} as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction if the internal message room id does not exists', async () => { |
||||
await service.sendExternalMessageUnReaction({} as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction the user does not exists', async () => { |
||||
userAdapter.getFederatedUserByInternalId.resolves(undefined); |
||||
await service.sendExternalMessageUnReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction the room does not exists', async () => { |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(undefined); |
||||
await service.sendExternalMessageUnReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction the the message is not from matrix federation', async () => { |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
await service.sendExternalMessageUnReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction if the user is not from the same home server', async () => { |
||||
bridge.extractHomeserverOrigin.onCall(0).returns('localDomain'); |
||||
bridge.extractHomeserverOrigin.onCall(1).returns('externalDomain'); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
await service.sendExternalMessageUnReaction( |
||||
{ rid: 'roomId', federation: { eventId: 'eventId' } } as any, |
||||
{ _id: 'id' } as any, |
||||
'reaction', |
||||
); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction if the user is not from the same home server', async () => { |
||||
bridge.extractHomeserverOrigin.returns('externalDomain'); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
await service.sendExternalMessageUnReaction( |
||||
{ rid: 'roomId', federation: { eventId: 'eventId' } } as any, |
||||
{ _id: 'id' } as any, |
||||
'reaction', |
||||
); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should not send the unreaction if there is no existing reaction', async () => { |
||||
bridge.extractHomeserverOrigin.returns('localDomain'); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
await service.sendExternalMessageUnReaction( |
||||
{ rid: 'roomId', federation: { eventId: 'eventId' } } as any, |
||||
{ _id: 'id' } as any, |
||||
'reaction', |
||||
); |
||||
|
||||
expect(bridge.sendMessageReaction.called).to.be.false; |
||||
}); |
||||
|
||||
it('should send the unreaction', async () => { |
||||
const message = { |
||||
rid: 'roomId', |
||||
federation: { eventId: 'eventId' }, |
||||
reactions: { |
||||
reaction: { |
||||
federationReactionEventIds: { |
||||
eventId: user.getUsername(), |
||||
}, |
||||
}, |
||||
}, |
||||
} as any; |
||||
bridge.extractHomeserverOrigin.returns('localDomain'); |
||||
roomAdapter.getFederatedRoomByInternalId.resolves(room); |
||||
userAdapter.getFederatedUserByInternalId.resolves(user); |
||||
await service.sendExternalMessageUnReaction(message, { _id: 'id', username: user.getUsername() } as any, 'reaction'); |
||||
|
||||
expect(bridge.redactEvent.calledWith(room.getExternalId(), user.getExternalId(), 'eventId')).to.be.true; |
||||
expect(messageAdapter.unsetExternalFederationEventOnMessage.calledWith('eventId', message, 'reaction')).to.be.true; |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,55 @@ |
||||
import { expect } from 'chai'; |
||||
import sinon from 'sinon'; |
||||
|
||||
import { |
||||
MatrixEnumRelatesToRelType, |
||||
MatrixEnumSendMessageType, |
||||
} from '../../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/events/RoomMessageSent'; |
||||
import { MatrixRoomMessageSentHandler } from '../../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers/Room'; |
||||
|
||||
describe('Federation - Infrastructure - handlers - Room - MatrixRoomMessageSentHandler ', () => { |
||||
const normalMessageStub = sinon.stub(); |
||||
const editedMessageStub = sinon.stub(); |
||||
const fileMessageStub = sinon.stub(); |
||||
const roomService = { |
||||
onExternalMessageReceived: normalMessageStub, |
||||
onExternalFileMessageReceived: fileMessageStub, |
||||
onExternalMessageEditedReceived: editedMessageStub, |
||||
}; |
||||
const handler = new MatrixRoomMessageSentHandler(roomService as any); |
||||
|
||||
describe('#handle()', () => { |
||||
const handlers: Record<string, any> = { |
||||
[MatrixEnumSendMessageType.TEXT]: normalMessageStub, |
||||
[MatrixEnumSendMessageType.AUDIO]: fileMessageStub, |
||||
[MatrixEnumSendMessageType.FILE]: fileMessageStub, |
||||
[MatrixEnumSendMessageType.IMAGE]: fileMessageStub, |
||||
[MatrixEnumSendMessageType.NOTICE]: normalMessageStub, |
||||
[MatrixEnumSendMessageType.VIDEO]: fileMessageStub, |
||||
[MatrixEnumSendMessageType.EMOTE]: normalMessageStub, |
||||
}; |
||||
|
||||
Object.keys(handlers).forEach((type) => { |
||||
it(`should call the correct handler for ${type}`, async () => { |
||||
await handler.handle({ content: { msgtype: type, url: 'url', info: { mimetype: 'mime', size: 12 } } } as any); |
||||
expect(handlers[type].called).to.be.true; |
||||
}); |
||||
}); |
||||
|
||||
it('should call the default handler if no handler is found', async () => { |
||||
await handler.handle({ content: { msgtype: 'unknown', url: 'url', info: { mimetype: 'mime', size: 12 } } } as any); |
||||
expect(normalMessageStub.called).to.be.true; |
||||
}); |
||||
|
||||
it('should call the edit message method when it is an edition event', async () => { |
||||
await handler.handle({ |
||||
content: { |
||||
'msgtype': MatrixEnumSendMessageType.TEXT, |
||||
'm.new_content': {}, |
||||
'm.relates_to': { rel_type: MatrixEnumRelatesToRelType.REPLACE }, |
||||
}, |
||||
} as any); |
||||
expect(editedMessageStub.called).to.be.true; |
||||
}); |
||||
}); |
||||
}); |
||||
Loading…
Reference in new issue