async def _handle_twitter_attachment( self, source: 'u.User', sender: 'p.Puppet', message: MessageData) -> Optional[MediaMessageEventContent]: content = None intent = sender.intent_for(self) media = message.attachment.media if media: reuploaded_info = await self._reupload_twitter_media( source, media.media_url_https, intent) thumbnail_info = None if media.video_info: thumbnail_info = reuploaded_info best_variant = None for variant in media.video_info.variants: if ((not best_variant or (variant.bitrate or 0) > (best_variant.bitrate or 0) or self._is_better_mime(best_variant, variant))): best_variant = variant reuploaded_info = await self._reupload_twitter_media( source, best_variant.url, intent) content = MediaMessageEventContent( body=reuploaded_info.file_name, url=reuploaded_info.mxc, file=reuploaded_info.decryption_info, external_url=media.media_url_https) if message.attachment.video: content.msgtype = MessageType.VIDEO content.info = VideoInfo( mimetype=reuploaded_info.mime_type, size=reuploaded_info.size, width=media.original_info.width, height=media.original_info.height, duration=media.video_info.duration_millis // 1000) elif message.attachment.photo or message.attachment.animated_gif: content.msgtype = MessageType.IMAGE content.info = ImageInfo(mimetype=reuploaded_info.mime_type, size=reuploaded_info.size, width=media.original_info.width, height=media.original_info.height) if thumbnail_info: content.info.thumbnail_url = thumbnail_info.mxc content.info.thumbnail_file = thumbnail_info.decryption_info content.info.thumbnail_info = ThumbnailInfo( mimetype=thumbnail_info.mime_type, size=thumbnail_info.size, width=media.original_info.width, height=media.original_info.height) # Remove the attachment link from message.text start, end = media.indices message.text = message.text[:start] + message.text[end:] elif message.attachment.tweet: # TODO special handling for tweets? pass return content
async def make_qr(intent: IntentAPI, data: Union[str, bytes], body: str = None) -> MediaMessageEventContent: # TODO always encrypt QR codes? buffer = io.BytesIO() image = qrcode.make(data) size = image.pixel_size image.save(buffer, "PNG") qr = buffer.getvalue() mxc = await intent.upload_media(qr, "image/png", "qr.png", len(qr)) return MediaMessageEventContent(body=body or data, url=mxc, msgtype=MessageType.IMAGE, info=ImageInfo(mimetype="image/png", size=len(qr), width=size, height=size))
async def process_hangouts_attachments( self, event: ChatMessageEvent, intent: IntentAPI) -> Optional[EventID]: attachments_pb = event._event.chat_message.message_content.attachment if len(event.attachments) > 1: self.log.warning("Can't handle more that one attachment") return None attachment = event.attachments[0] attachment_pb = attachments_pb[0] embed_item = attachment_pb.embed_item # Get the filename from the headers async with self.az.http_session.request("GET", attachment) as resp: value, params = cgi.parse_header( resp.headers["Content-Disposition"]) mime = resp.headers["Content-Type"] filename = params.get('filename', attachment.split("/")[-1]) # TODO: This also catches movies, but I can't work out how they present # differently to images if embed_item.type[0] == hangouts.ITEM_TYPE_PLUS_PHOTO: data = await self._get_remote_bytes(attachment) upload_mime = mime decryption_info = None if self.encrypted and encrypt_attachment: data, decryption_info = encrypt_attachment(data) upload_mime = "application/octet-stream" mxc_url = await intent.upload_media(data, mime_type=upload_mime, filename=filename) if decryption_info: decryption_info.url = mxc_url content = MediaMessageEventContent(url=mxc_url, file=decryption_info, body=filename, info=ImageInfo(size=len(data), mimetype=mime), msgtype=MessageType.IMAGE) return await self._send_message(intent, content, timestamp=event.timestamp) return None
async def _handle_instagram_media(self, source: 'u.User', intent: IntentAPI, item: ThreadItem) -> Optional[EventID]: # TODO maybe use a dict and item.item_type instead of a ton of ifs method = self._reupload_instagram_media if item.media: media_data = item.media elif item.visual_media: media_data = item.visual_media.media elif item.animated_media: media_data = item.animated_media method = self._reupload_instagram_animated elif item.voice_media: media_data = item.voice_media method = self._reupload_instagram_voice elif item.reel_share: media_data = item.reel_share.media elif item.story_share: media_data = item.story_share.media elif item.media_share: media_data = item.media_share else: media_data = None if not media_data: self.log.debug(f"Unsupported media type in item {item}") return None elif isinstance(media_data, ExpiredMediaItem): self.log.debug(f"Expired media in item {item}") # TODO send error message return None reuploaded = await method(source, media_data, intent) if not reuploaded: self.log.trace(f"Upload of {media_data} failed") # TODO error message? return None content = MediaMessageEventContent(body=reuploaded.file_name, external_url=reuploaded.url, url=reuploaded.mxc, file=reuploaded.decryption_info, info=reuploaded.info, msgtype=reuploaded.msgtype) return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
async def _handle_facebook_sticker(self, intent: IntentAPI, sticker: FBSticker, reply_to: str) -> EventID: # TODO handle animated stickers? mxc, mime, size, decryption_info = await self._reupload_fb_file( sticker.url, intent, encrypt=self.encrypted) return await self._send_message( intent, event_type=EventType.STICKER, content=MediaMessageEventContent( url=mxc, file=decryption_info, msgtype=MessageType.STICKER, body=sticker.label, info=ImageInfo(width=sticker.width, size=size, height=sticker.height, mimetype=mime), relates_to=self._get_facebook_reply(reply_to)))
async def handle_telegram_document(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: RelatesTo = None ) -> Optional[EventID]: document = evt.media.document attrs = self._parse_telegram_document_attributes(document.attributes) if document.size > config["bridge.max_document_size"] * 1000 ** 2: name = attrs.name or "" caption = f"\n{evt.message}" if evt.message else "" return await intent.send_notice(self.mxid, f"Too large file {name}{caption}") thumb_loc, thumb_size = self._get_largest_photo_size(document) if thumb_size and not isinstance(thumb_size, (PhotoSize, PhotoCachedSize)): self.log.debug(f"Unsupported thumbnail type {type(thumb_size)}") thumb_loc = None thumb_size = None parallel_id = source.tgid if config["bridge.parallel_file_transfer"] else None file = await util.transfer_file_to_matrix(source.client, intent, document, thumb_loc, is_sticker=attrs.is_sticker, tgs_convert=config["bridge.animated_sticker"], filename=attrs.name, parallel_id=parallel_id) if not file: return None info, name = self._parse_telegram_document_meta(evt, file, attrs, thumb_size) await intent.set_typing(self.mxid, is_typing=False) event_type = EventType.ROOM_MESSAGE # Riot only supports images as stickers, so send animated webm stickers as m.video if attrs.is_sticker and file.mime_type.startswith("image/"): event_type = EventType.STICKER content = MediaMessageEventContent( body=name or "unnamed file", info=info, url=file.mxc, relates_to=relates_to, external_url=self._get_external_url(evt), msgtype={ "video/": MessageType.VIDEO, "audio/": MessageType.AUDIO, "image/": MessageType.IMAGE, }.get(info.mimetype[:6], MessageType.FILE)) return await intent.send_message_event(self.mxid, event_type, content, timestamp=evt.date)
def _make_media_content(attachment: Attachment) -> MediaMessageEventContent: if attachment.content_type.startswith("image/"): msgtype = MessageType.IMAGE info = ImageInfo(mimetype=attachment.content_type, width=attachment.width, height=attachment.height) elif attachment.content_type.startswith("video/"): msgtype = MessageType.VIDEO info = VideoInfo(mimetype=attachment.content_type, width=attachment.width, height=attachment.height) elif attachment.voice_note or attachment.content_type.startswith("audio/"): msgtype = MessageType.AUDIO info = AudioInfo(mimetype=attachment.content_type) else: msgtype = MessageType.FILE info = FileInfo(mimetype=attachment.content_type) if not attachment.custom_filename: ext = mimetypes.guess_extension(attachment.content_type) or "" attachment.custom_filename = attachment.id + ext return MediaMessageEventContent(msgtype=msgtype, info=info, body=attachment.custom_filename)
async def _reupload(self, status: int) -> Optional[MediaMessageEventContent]: url = self.config["url"].format(status=status) self.log.info(f"Reuploading {url}") resp = await self.http.get(url) if resp.status != 200: resp.raise_for_status() data = await resp.read() img = Image.open(BytesIO(data)) width, height = img.size mimetype = Image.MIME[img.format] filename = f"{status}{guess_extension(mimetype)}" mxc = await self.client.upload_media(data, mimetype, filename=filename) return MediaMessageEventContent(msgtype=MessageType.IMAGE, body=filename, url=mxc, info=ImageInfo(mimetype=mimetype, size=len(data), width=width, height=height))
async def send_movie_info(self, evt: MessageEvent, movie) -> None: mxc_uri = await self.client.upload_media(data=movie.get_image_binary()) text_message = f'{movie.title}' if len(movie.overview) > 200: three_dotts = " [...]" else: three_dotts = "" html_message = f"""<p><b>{escape(movie.title)}</b></p> <p>{escape(movie.overview)[:200]}{three_dotts}</p> <p>taken from www.themoviedb.org</p>""" content = TextMessageEventContent( msgtype=MessageType.TEXT, format=Format.HTML, body=f"{text_message}", formatted_body=f"{html_message}") await evt.respond(content) content = MediaMessageEventContent( msgtype=MessageType.IMAGE, body=f"Image {movie.title}", url=f"{mxc_uri}") await evt.respond(content)
async def upload_qr() -> None: nonlocal qr_event_id buffer = io.BytesIO() image = qrcode.make(qr_login.url) size = image.pixel_size image.save(buffer, "PNG") qr = buffer.getvalue() mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr)) content = MediaMessageEventContent(body=qr_login.url, url=mxc, msgtype=MessageType.IMAGE, info=ImageInfo(mimetype="image/png", size=len(qr), width=size, height=size)) if qr_event_id: content.set_edit(qr_event_id) await evt.az.intent.send_message(evt.room_id, content) else: content.set_reply(evt.event_id) qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
def send_file( self, room_id: RoomID, url: ContentURI, info: BaseFileInfo | None = None, file_name: str | None = None, file_type: MessageType = MessageType.FILE, relates_to: RelatesTo | None = None, **kwargs, ) -> Awaitable[EventID]: """ Send a file to a room. Args: room_id: The ID of the room to send the message to. url: The Matrix content repository URI of the file. You can upload files using :meth:`~MediaRepositoryMethods.upload_media`. info: Additional metadata about the file, e.g. mimetype, image size, video duration, etc file_name: The name for the file to send. file_type: The general file type to send. The file type can be further specified by setting the ``mimetype`` field of the ``info`` parameter. Defaults to :attr:`MessageType.FILE` (unspecified file type, e.g. document) relates_to: Message relation metadata used for things like replies. **kwargs: Optional parameters to pass to the :meth:`HTTPAPI.request` method. Returns: The ID of the event that was sent. """ return self.send_message( room_id, MediaMessageEventContent(url=url, info=info, body=file_name, relates_to=relates_to, msgtype=file_type), **kwargs, )
async def _handle_twitter_attachment( self, source: 'u.User', sender: 'p.Puppet', message: MessageData) -> Optional[MediaMessageEventContent]: content = None intent = sender.intent_for(self) media = message.attachment.media if media: # TODO this doesn't actually work for gifs and videos # The actual url is in media.video_info.variants[0].url # Gifs are also videos reuploaded_info = await self._reupload_twitter_media( source, media, intent) content = MediaMessageEventContent( body=reuploaded_info.file_name, url=reuploaded_info.mxc, file=reuploaded_info.decryption_info, external_url=media.media_url_https) if message.attachment.video: content.msgtype = MessageType.VIDEO content.info = VideoInfo( mimetype=reuploaded_info.mime_type, size=reuploaded_info.size, width=media.original_info.width, height=media.original_info.height, duration=media.video_info.duration_millis // 1000) elif message.attachment.photo or message.attachment.animated_gif: content.msgtype = MessageType.IMAGE content.info = ImageInfo(mimetype=reuploaded_info.mime_type, size=reuploaded_info.size, width=media.original_info.width, height=media.original_info.height) # Remove the attachment link from message.text start, end = media.indices message.text = message.text[:start] + message.text[end:] elif message.attachment.tweet: # TODO special handling for tweets? pass return content
async def handle_telegram_photo( self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: RelatesTo = None) -> Optional[EventID]: media: MessageMediaPhoto = evt.media if media.photo is None and media.ttl_seconds: return await self._send_message( intent, TextMessageEventContent(msgtype=MessageType.NOTICE, body="Photo has expired")) loc, largest_size = self._get_largest_photo_size(media.photo) if loc is None: content = TextMessageEventContent( msgtype=MessageType.TEXT, body="Failed to bridge image", external_url=self._get_external_url(evt)) return await self._send_message(intent, content, timestamp=evt.date) file = await util.transfer_file_to_matrix(source.client, intent, loc, encrypt=self.encrypted) if not file: return None if self.get_config("inline_images") and (evt.message or evt.fwd_from or evt.reply_to_msg_id): content = await formatter.telegram_to_matrix( evt, source, self.main_intent, prefix_html= f"<img src='{file.mxc}' alt='Inline Telegram photo'/><br/>", prefix_text="Inline image: ") content.external_url = self._get_external_url(evt) await intent.set_typing(self.mxid, is_typing=False) return await self._send_message(intent, content, timestamp=evt.date) info = ImageInfo(height=largest_size.h, width=largest_size.w, orientation=0, mimetype=file.mime_type, size=(len(largest_size.bytes) if (isinstance(largest_size, PhotoCachedSize)) else largest_size.size)) ext = sane_mimetypes.guess_extension(file.mime_type) name = f"disappearing_image{ext}" if media.ttl_seconds else f"image{ext}" await intent.set_typing(self.mxid, is_typing=False) content = MediaMessageEventContent( msgtype=MessageType.IMAGE, info=info, body=name, relates_to=relates_to, external_url=self._get_external_url(evt)) if file.decryption_info: content.file = file.decryption_info else: content.url = file.mxc result = await self._send_message(intent, content, timestamp=evt.date) if media.ttl_seconds: self.loop.create_task( self._expire_telegram_photo(intent, result, media.ttl_seconds)) if evt.message: caption_content = await formatter.telegram_to_matrix( evt, source, self.main_intent, no_reply_fallback=True) caption_content.external_url = content.external_url result = await self._send_message(intent, caption_content, timestamp=evt.date) return result
async def handle_telegram_document( self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: RelatesTo = None) -> Optional[EventID]: document = evt.media.document attrs = self._parse_telegram_document_attributes(document.attributes) if document.size > config["bridge.max_document_size"] * 1000**2: name = attrs.name or "" caption = f"\n{evt.message}" if evt.message else "" # TODO encrypt return await intent.send_notice(self.mxid, f"Too large file {name}{caption}") thumb_loc, thumb_size = self._get_largest_photo_size(document) if thumb_size and not isinstance(thumb_size, (PhotoSize, PhotoCachedSize)): self.log.debug(f"Unsupported thumbnail type {type(thumb_size)}") thumb_loc = None thumb_size = None parallel_id = source.tgid if config[ "bridge.parallel_file_transfer"] else None file = await util.transfer_file_to_matrix( source.client, intent, document, thumb_loc, is_sticker=attrs.is_sticker, tgs_convert=config["bridge.animated_sticker"], filename=attrs.name, parallel_id=parallel_id, encrypt=self.encrypted) if not file: return None info, name = self._parse_telegram_document_meta( evt, file, attrs, thumb_size) await intent.set_typing(self.mxid, is_typing=False) event_type = EventType.ROOM_MESSAGE # Elements only support images as stickers, so send animated webm stickers as m.video if attrs.is_sticker and file.mime_type.startswith("image/"): event_type = EventType.STICKER # Tell clients to render the stickers as 256x256 if they're bigger if info.width > 256 or info.height > 256: if info.width > info.height: info.height = int(info.height / (info.width / 256)) info.width = 256 else: info.width = int(info.width / (info.height / 256)) info.height = 256 if info.thumbnail_info: info.thumbnail_info.width = info.width info.thumbnail_info.height = info.height if attrs.is_gif: info["fi.mau.telegram.gif"] = True info["fi.mau.loop"] = True info["fi.mau.autoplay"] = True info["fi.mau.no_audio"] = True content = MediaMessageEventContent( body=name or "unnamed file", info=info, relates_to=relates_to, external_url=self._get_external_url(evt), msgtype={ "video/": MessageType.VIDEO, "audio/": MessageType.AUDIO, "image/": MessageType.IMAGE, }.get(info.mimetype[:6], MessageType.FILE)) if file.decryption_info: content.file = file.decryption_info else: content.url = file.mxc res = await self._send_message(intent, content, event_type=event_type, timestamp=evt.date) if evt.message: caption_content = await formatter.telegram_to_matrix( evt, source, self.main_intent, no_reply_fallback=True) caption_content.external_url = content.external_url res = await self._send_message(intent, caption_content, timestamp=evt.date) return res
from mautrix.types import (MessageType, MediaMessageEventContent, ImageInfo, ThumbnailInfo, ContentURI) gif_versions: Dict[str, MediaMessageEventContent] = { "crap": MediaMessageEventContent( msgtype=MessageType.IMAGE, body="putkiteippi.gif", url=ContentURI("mxc://maunium.net/IkSoSYYrtaYJQeCaABSLqKiD"), info=ImageInfo( mimetype="image/gif", width=364, height=153, size=2079294, thumbnail_url=ContentURI( "mxc://maunium.net/iivOnCDjcGqGvnwnNWxSbAvb"), thumbnail_info=ThumbnailInfo( mimetype="image/png", width=364, height=153, size=51302, ), ), ), "low": MediaMessageEventContent( msgtype=MessageType.IMAGE, body="putkiteippi.gif", url=ContentURI("mxc://maunium.net/PXbFbVEmcGzkaMXwUOrzZcQM"), info=ImageInfo(
async def _handle_facebook_attachment(self, intent: IntentAPI, attachment: AttachmentClass, reply_to: str) -> Optional[EventID]: if isinstance(attachment, AudioAttachment): mxc, mime, size, decryption_info = await self._reupload_fb_file( attachment.url, intent, attachment.filename, encrypt=self.encrypted) event_id = await self._send_message( intent, MediaMessageEventContent( url=mxc, file=decryption_info, msgtype=MessageType.AUDIO, body=attachment.filename, info=AudioInfo(size=size, mimetype=mime, duration=attachment.duration), relates_to=self._get_facebook_reply(reply_to))) # elif isinstance(attachment, VideoAttachment): # TODO elif isinstance(attachment, FileAttachment): mxc, mime, size, decryption_info = await self._reupload_fb_file( attachment.url, intent, attachment.name, encrypt=self.encrypted) event_id = await self._send_message( intent, MediaMessageEventContent( url=mxc, file=decryption_info, msgtype=MessageType.FILE, body=attachment.name, info=FileInfo(size=size, mimetype=mime), relates_to=self._get_facebook_reply(reply_to))) elif isinstance(attachment, ImageAttachment): mxc, mime, size, decryption_info = await self._reupload_fb_file( attachment.large_preview_url or attachment.preview_url, intent, encrypt=self.encrypted) event_id = await self._send_message( intent, MediaMessageEventContent( url=mxc, file=decryption_info, msgtype=MessageType.IMAGE, body=f"image.{attachment.original_extension}", info=ImageInfo(size=size, mimetype=mime, width=attachment.large_preview_width, height=attachment.large_preview_height), relates_to=self._get_facebook_reply(reply_to))) elif isinstance(attachment, LocationAttachment): content = await self._convert_facebook_location(intent, attachment) content.relates_to = self._get_facebook_reply(reply_to) event_id = await self._send_message(intent, content) elif isinstance(attachment, ShareAttachment): # These are handled in the text formatter return None else: self.log.warning(f"Unsupported attachment type: {attachment}") return None self._last_bridged_mxid = event_id return event_id