diff --git a/packages/core/src/events/m.room.message.ts b/packages/core/src/events/m.room.message.ts index b59f62654..343a716d6 100644 --- a/packages/core/src/events/m.room.message.ts +++ b/packages/core/src/events/m.room.message.ts @@ -1,15 +1,59 @@ import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; -type MessageType = - | 'm.text' - | 'm.emote' - | 'm.notice' - | 'm.image' - | 'm.file' - | 'm.audio' - | 'm.video' - | 'm.location'; +export type TextMessageType = 'm.text' | 'm.emote' | 'm.notice'; +export type FileMessageType = 'm.image' | 'm.file' | 'm.audio' | 'm.video'; +export type LocationMessageType = 'm.location'; +export type MessageType = + | TextMessageType + | FileMessageType + | LocationMessageType; + +// Base message content +type BaseMessageContent = { + body: string; + 'm.mentions'?: Record; + format?: string; + formatted_body?: string; + 'm.relates_to'?: MessageRelation; +}; + +// Text message content +export type TextMessageContent = BaseMessageContent & { + msgtype: TextMessageType; +}; + +// File message content +export type FileMessageContent = BaseMessageContent & { + msgtype: FileMessageType; + url: string; + info?: { + size?: number; + mimetype?: string; + w?: number; + h?: number; + duration?: number; + thumbnail_url?: string; + thumbnail_info?: { + w?: number; + h?: number; + mimetype?: string; + size?: number; + }; + }; +}; + +// Location message content +export type LocationMessageContent = BaseMessageContent & { + msgtype: LocationMessageType; + geo_uri: string; +}; + +// New content for edits +type NewContent = + | Pick + | Pick + | Pick; declare module './eventBase' { interface Events { @@ -17,19 +61,12 @@ declare module './eventBase' { unsigned: { age_ts: number; }; - content: { - body: string; - msgtype: MessageType; - 'm.mentions'?: Record; - format?: string; - formatted_body?: string; - 'm.relates_to'?: MessageRelation; - 'm.new_content'?: { - body: string; - msgtype: MessageType; - format?: string; - formatted_body?: string; - }; + content: ( + | TextMessageContent + | FileMessageContent + | LocationMessageContent + ) & { + 'm.new_content'?: NewContent; }; }; } @@ -67,19 +104,12 @@ export const isRoomMessageEvent = ( export interface RoomMessageEvent extends EventBase { type: 'm.room.message'; - content: { - body: string; - msgtype: MessageType; - 'm.mentions'?: Record; - format?: string; - formatted_body?: string; - 'm.relates_to'?: MessageRelation; - 'm.new_content'?: { - body: string; - msgtype: MessageType; - format?: string; - formatted_body?: string; - }; + content: ( + | TextMessageContent + | FileMessageContent + | LocationMessageContent + ) & { + 'm.new_content'?: NewContent; }; unsigned: { age: number; @@ -110,19 +140,12 @@ export const roomMessageEvent = ({ prev_events: string[]; depth: number; unsigned?: RoomMessageEvent['unsigned']; - content: { - body: string; - msgtype: MessageType; - 'm.mentions'?: Record; - format?: string; - formatted_body?: string; - 'm.relates_to'?: MessageRelation; - 'm.new_content'?: { - body: string; - msgtype: MessageType; - format?: string; - formatted_body?: string; - }; + content: ( + | TextMessageContent + | FileMessageContent + | LocationMessageContent + ) & { + 'm.new_content'?: NewContent; }; origin?: string; ts?: number; diff --git a/packages/federation-sdk/src/services/message.service.ts b/packages/federation-sdk/src/services/message.service.ts index fa231aca9..138928fc9 100644 --- a/packages/federation-sdk/src/services/message.service.ts +++ b/packages/federation-sdk/src/services/message.service.ts @@ -26,6 +26,27 @@ import { FederationService } from './federation.service'; import { RoomService } from './room.service'; import { StateService } from './state.service'; +// File message content type +export type FileMessageContent = { + body: string; + msgtype: 'm.image' | 'm.file' | 'm.video' | 'm.audio'; + url: string; + info?: { + size?: number; + mimetype?: string; + w?: number; + h?: number; + duration?: number; + thumbnail_url?: string; + thumbnail_info?: { + w?: number; + h?: number; + mimetype?: string; + size?: number; + }; + }; +}; + @singleton() export class MessageService { private readonly logger = createLogger('MessageService'); @@ -121,25 +142,7 @@ export class MessageService { async sendFileMessage( roomId: string, - content: { - body: string; - msgtype: 'm.image' | 'm.file' | 'm.video' | 'm.audio'; - url: string; - info?: { - size?: number; - mimetype?: string; - w?: number; - h?: number; - duration?: number; - thumbnail_url?: string; - thumbnail_info?: { - w?: number; - h?: number; - mimetype?: string; - size?: number; - }; - }; - }, + content: FileMessageContent, senderUserId: string, ): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); diff --git a/packages/room/src/types/v3-11.ts b/packages/room/src/types/v3-11.ts index c9b5c5087..3780f1830 100644 --- a/packages/room/src/types/v3-11.ts +++ b/packages/room/src/types/v3-11.ts @@ -338,10 +338,21 @@ export type PduRoomNameEventContent = z.infer< typeof PduRoomNameEventContentSchema >; -export const PduMessageEventContentSchema = z.object({ +// Base message content schema +const BaseMessageContentSchema = z.object({ body: z.string().describe('The body of the message.'), - // TODO: add more types - msgtype: z.enum(['m.text', 'm.image']).describe('The type of the message.'), + msgtype: z + .enum([ + 'm.text', + 'm.image', + 'm.file', + 'm.audio', + 'm.video', + 'm.emote', + 'm.notice', + 'm.location', + ]) + .describe('The type of the message.'), // Optional fields for message edits and relations aka threads 'm.relates_to': z .object({ @@ -368,23 +379,6 @@ export const PduMessageEventContentSchema = z.object({ }) .optional() .describe('Relation information for edits, replies, reactions, etc.'), - 'm.new_content': z - .object({ - body: z.string().describe('The new body of the message for edits.'), - msgtype: z - .enum(['m.text', 'm.image']) - .describe('The type of the new message content.'), - format: z - .enum(['org.matrix.custom.html']) - .describe('The format of the message content.') - .optional(), - formatted_body: z - .string() - .describe('The formatted body of the message.') - .optional(), - }) - .optional() - .describe('The new content for message edits.'), format: z .enum(['org.matrix.custom.html']) .describe('The format of the message content.') @@ -395,6 +389,102 @@ export const PduMessageEventContentSchema = z.object({ .optional(), }); +// File info schema +const FileInfoSchema = z.object({ + size: z.number().describe('The size of the file in bytes.').optional(), + mimetype: z.string().describe('The MIME type of the file.').optional(), + w: z.number().describe('The width of the image/video in pixels.').optional(), + h: z.number().describe('The height of the image/video in pixels.').optional(), + duration: z + .number() + .describe('The duration of the audio/video in milliseconds.') + .optional(), + thumbnail_url: z + .string() + .describe('The URL of the thumbnail image.') + .optional(), + thumbnail_info: z + .object({ + w: z + .number() + .describe('The width of the thumbnail in pixels.') + .optional(), + h: z + .number() + .describe('The height of the thumbnail in pixels.') + .optional(), + mimetype: z + .string() + .describe('The MIME type of the thumbnail.') + .optional(), + size: z + .number() + .describe('The size of the thumbnail in bytes.') + .optional(), + }) + .describe('Information about the thumbnail.') + .optional(), +}); + +// Text message content (m.text, m.emote, m.notice) +const TextMessageContentSchema = BaseMessageContentSchema.extend({ + msgtype: z.enum(['m.text', 'm.emote', 'm.notice']), +}); + +// File message content (m.image, m.file, m.audio, m.video) +const FileMessageContentSchema = BaseMessageContentSchema.extend({ + msgtype: z.enum(['m.image', 'm.file', 'm.audio', 'm.video']), + url: z.string().describe('The URL of the file.'), + info: FileInfoSchema.describe('Information about the file.').optional(), +}); + +// Location message content (m.location) +const LocationMessageContentSchema = BaseMessageContentSchema.extend({ + msgtype: z.literal('m.location'), + geo_uri: z.string().describe('The geo URI of the location.'), + // Additional location fields can be added here +}); + +// New content schema for edits +const NewContentSchema = z.discriminatedUnion('msgtype', [ + TextMessageContentSchema.pick({ + body: true, + msgtype: true, + format: true, + formatted_body: true, + }), + FileMessageContentSchema.pick({ + body: true, + msgtype: true, + url: true, + info: true, + }), + LocationMessageContentSchema.pick({ + body: true, + msgtype: true, + geo_uri: true, + }), +]); + +// Main message content schema using discriminated union +export const PduMessageEventContentSchema = z.union([ + TextMessageContentSchema.extend({ + 'm.new_content': NewContentSchema.optional().describe( + 'The new content for message edits.', + ), + }), + FileMessageContentSchema.extend({ + 'm.new_content': NewContentSchema.optional().describe( + 'The new content for message edits.', + ), + }), + LocationMessageContentSchema.extend({ + 'm.new_content': NewContentSchema.optional().describe( + 'The new content for message edits.', + ), + }), +]); + export type PduMessageEventContent = z.infer< typeof PduMessageEventContentSchema >;