def send_status(self, status: EFBStatus): if isinstance(status, EFBMessageRemoval): if not status.message.author.is_self: raise EFBMessageError(self._('You can only recall your own messages.')) try: ews_utils.message_to_dummy_message(status.message.uid, self).recall() except wxpy.ResponseError as e: raise EFBMessageError( self._('Failed to recall the message.') + '{0} ({1})'.format(e.err_msg, e.err_code)) else: raise EFBOperationNotSupported()
def send_status(self, status: 'EFBStatus'): if isinstance(status, EFBMessageRemoval): if not status.message.author.is_self: raise EFBMessageError(self._('You can only recall your own messages.')) try: uid_type = status.message.uid.split('_') self.recall_message(uid_type[1]) except CoolQAPIFailureException: raise EFBMessageError( self._('Failed to recall the message. This message may have already expired.')) else: raise EFBOperationNotSupported()
def _bot_send_video(self, chat: wxpy.Chat, filename: str, file: IO[bytes]) -> wxpy.SentMessage: try: return chat.send_video(filename, file=file) except wxpy.ResponseError as e: e = self.substitute_known_error_reason(e) raise EFBMessageError(self._("Error from Web WeChat while sending video: [{code}] {message}") .format(code=e.err_code, message=e.err_msg))
def _bot_send_msg(self, chat: wxpy.Chat, message: str) -> wxpy.SentMessage: try: return chat.send_msg(message) except wxpy.ResponseError as e: e = self.substitute_known_error_reason(e) raise EFBMessageError(self._("Error from Web WeChat while sending message: [{code}] {message}") .format(code=e.err_code, message=e.err_msg))
def _bot_send_msg(self, chat: wxpy.Chat, message: str) -> wxpy.SentMessage: try: return chat.send_msg(message) except wxpy.ResponseError as e: raise EFBMessageError( self. _("Unknown error occurred while delivering the message: [{code}] {message}" ).format(code=e.err_code, message=e.err_msg))
def _bot_send_video(self, chat: wxpy.Chat, filename: str, file: IO[bytes]) -> wxpy.SentMessage: try: return chat.send_video(filename, file=file) except wxpy.ResponseError as e: raise EFBMessageError( self. _("Unknown error occurred while delivering the video: [{code}] {message}" ).format(code=e.err_code, message=e.err_msg))
def send_status(self, status: Status): if isinstance(status, MessageRemoval): if not isinstance(status.message.author, SelfChatMember): raise EFBOperationNotSupported( self._('You can only recall your own messages.')) if status.message.uid: try: msg_ids = json.loads(status.message.uid) except JSONDecodeError: raise EFBMessageError( self._("ID of the message to recall is invalid.")) else: raise EFBMessageError( self._("ID of the message to recall is not found.")) failed = 0 if any(len(i) == 1 for i in msg_ids): # Message is not sent through EWS raise EFBOperationNotSupported( self._("You may only recall messages sent via EWS.") ) for i in msg_ids: try: ews_utils.message_id_to_dummy_message(i, self).recall() except wxpy.ResponseError: failed += 1 if failed: raise EFBMessageError( self.ngettext( 'Failed to recall {failed} of {total} message.', 'Failed to recall {failed} of {total} messages.', len(msg_ids) ).format(failed=failed, total=len(msg_ids))) else: val = (status.message.uid, len(msg_ids)) for i in msg_ids: self.slave_message.recall_msg_id_conversion[str( i[1])] = val else: raise EFBOperationNotSupported()
def _check_file_download(self, file_obj: telegram.File): """ Check if the file is available for download.. Args: file_obj (telegram.File): PTB file object Raises: EFBMessageError: When file exceeds the maximum download size. """ size = getattr(file_obj, "file_size", None) if size and size > telegram.constants.MAX_FILESIZE_DOWNLOAD: raise EFBMessageError( self._("Attachment is too large. Maximum is 20 MB. (AT01)"))
def _check_file_download(self, file_obj: Any): """ Check if the file is available for download.. Args: file_obj (telegram.File): PTB file object Raises: EFBMessageError: When file exceeds the maximum download size. """ size = getattr(file_obj, "file_size", None) if size and size > MAX_FILESIZE_DOWNLOAD: size_str = humanize.naturalsize(size) max_size_str = humanize.naturalsize(MAX_FILESIZE_DOWNLOAD) raise EFBMessageError( self. _("Attachment is too large ({size}). Maximum allowed by Telegram Bot API is {max_size}. (AT01)" ).format(size=size_str, max_size=max_size_str))
def _download_file( self, file_obj: telegram.File, mime: Optional[str] = None) -> Tuple[IO[bytes], str, str, str]: """ Download media file from telegram platform. Args: file_obj (telegram.File): PTB file object mime (str): MIME type of the message Returns: Tuple[IO[bytes], str, str, str]: ``tempfile`` file-like object, MIME type, proposed file name, file path Raises: EFBMessageError: When file exceeds the maximum download size. """ size = getattr(file_obj, "file_size", None) file_id = file_obj.file_id if size and size > telegram.constants.MAX_FILESIZE_DOWNLOAD: raise EFBMessageError( self._("Attachment is too large. Maximum is 20 MB. (AT01)")) f = self.bot.get_file(file_id) if not mime: ext = os.path.splitext(f.file_path)[1] mime = mimetypes.guess_type(f.file_path, strict=False)[0] else: ext = mimetypes.guess_extension(mime, strict=False) file = tempfile.NamedTemporaryFile(suffix=ext) full_path = file.name f.download(out=file) file.seek(0) mime = getattr(file_obj, "mime_type", mime or magic.from_file(full_path, mime=True)) if type(mime) is bytes: mime = mime.decode() return file, mime, os.path.basename(full_path), full_path
def _download_file(self, tg_msg, file_obj, mime=''): """ Download media file from telegram platform. Args: tg_msg: Telegram message instance file_obj: File object mime: Type of message Returns: tuple of str[2]: Full path of the file, MIME type """ size = getattr(file_obj, "file_size", None) file_id = file_obj.file_id if size and size > telegram.constants.MAX_FILESIZE_DOWNLOAD: raise EFBMessageError( self._("Attachment is too large. Maximum is 20 MB. (AT01)")) f = self.bot.get_file(file_id) # self.logger.log(99, 'f.file_path[%r]', f.file_path) if not mime: ext = os.path.splitext(f.file_path)[1] mime = mimetypes.guess_type(f.file_path, strict=False)[0] else: ext = mimetypes.guess_extension(mime, strict=False) or ".unknown" file = tempfile.NamedTemporaryFile(suffix=ext) full_path = file.name f.download(out=file) file.seek(0) mime = getattr(file_obj, "mime_type", mime or magic.from_file(full_path, mime=True)) if type(mime) is bytes: mime = mime.decode() # self.logger.log(99, 'mime[%s], full_path[%s]', mime, full_path) return file, mime, os.path.basename(full_path)
def send_message(self, msg: EFBMsg) -> EFBMsg: """Send a message to WeChat. Supports text, image, sticker, and file. Args: msg (channel.EFBMsg): Message Object to be sent. Returns: This method returns nothing. Raises: EFBMessageTypeNotSupported: Raised when message type is not supported by the channel. """ chat: wxpy.Chat = self.chats.get_wxpy_chat_by_uid(msg.chat.chat_uid) r: wxpy.SentMessage self.logger.info("[%s] Sending message to WeChat:\n" "uid: %s\n" "UserName: %s\n" "NickName: %s\n" "Type: %s\n" "Text: %s", msg.uid, msg.chat.chat_uid, chat.user_name, chat.name, msg.type, msg.text) chat.mark_as_read() self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit) if msg.edit: if self.flag('delete_on_edit'): try: ews_utils.message_to_dummy_message(msg.uid, self).recall() except wxpy.ResponseError as e: self.logger.error("[%s] Trying to recall message but failed: %s", msg.uid, e) raise EFBMessageError(self._('Failed to recall message, edited message was not sent.')) else: raise EFBOperationNotSupported() if msg.type in [MsgType.Text, MsgType.Link]: if isinstance(msg.target, EFBMsg): max_length = self.flag("max_quote_length") qt_txt = "%s" % msg.target.text if max_length > 0: tgt_text = qt_txt[:max_length] if len(qt_txt) >= max_length: tgt_text += "…" tgt_text = "「%s」" % tgt_text elif max_length < 0: tgt_text = "「%s」" % qt_txt else: tgt_text = "" if isinstance(chat, wxpy.Group) and not msg.target.author.is_self: tgt_alias = "@%s\u2005 " % msg.target.author.display_name else: tgt_alias = "" msg.text = "%s%s\n\n%s" % (tgt_alias, tgt_text, msg.text) r = self._bot_send_msg(chat, msg.text) self.logger.debug('[%s] Sent as a text message. %s', msg.uid, msg.text) elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation): self.logger.info("[%s] Image/GIF/Sticker %s", msg.uid, msg.type) convert_to = None file = msg.file assert file is not None if self.flag('send_stickers_and_gif_as_jpeg'): if msg.type == MsgType.Sticker or msg.mime == "image/gif": convert_to = "image/jpeg" else: if msg.type == MsgType.Sticker: convert_to = "image/gif" if convert_to == "image/gif": with NamedTemporaryFile(suffix=".gif") as f: try: img = Image.open(file) try: alpha = img.split()[3] mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) except IndexError: mask = Image.eval(img.split()[0], lambda a: 0) img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=255) img.paste(255, mask) img.save(f, transparency=255) msg.path = f.name self.logger.debug('[%s] Image converted from %s to GIF', msg.uid, msg.mime) file.close() f.seek(0) if os.fstat(f.fileno()).st_size > self.MAX_FILE_SIZE: raise EFBMessageError(self._("Image size is too large. (IS02)")) r = self._bot_send_image(chat, f.name, f) finally: if not file.closed: file.close() elif convert_to == "image/jpeg": with NamedTemporaryFile(suffix=".jpg") as f: try: img = Image.open(file).convert('RGBA') out = Image.new("RGBA", img.size, (255,255,255,255)) out.paste(img, img) out.convert('RGB').save(f) msg.path = f.name self.logger.debug('[%s] Image converted from %s to JPEG', msg.uid, msg.mime) file.close() f.seek(0) if os.fstat(f.fileno()).st_size > self.MAX_FILE_SIZE: raise EFBMessageError(self._("Image size is too large. (IS02)")) r = self._bot_send_image(chat, f.name, f) finally: if not file.closed: file.close() else: try: if os.fstat(file.fileno()).st_size > self.MAX_FILE_SIZE: raise EFBMessageError(self._("Image size is too large. (IS01)")) self.logger.debug("[%s] Sending %s (image) to WeChat.", msg.uid, msg.path) r = self._bot_send_image(chat, msg.path, file) finally: if not file.closed: file.close() if msg.text: self._bot_send_msg(chat, msg.text) elif msg.type in (MsgType.File, MsgType.Audio): self.logger.info("[%s] Sending %s to WeChat\nFileName: %s\nPath: %s\nFilename: %s", msg.uid, msg.type, msg.text, msg.path, msg.filename) r = self._bot_send_file(chat, msg.filename, file=msg.file) if msg.text: self._bot_send_msg(chat, msg.text) msg.file.close() elif msg.type == MsgType.Video: self.logger.info("[%s] Sending video to WeChat\nFileName: %s\nPath: %s", msg.uid, msg.text, msg.path) r = self._bot_send_video(chat, msg.path, file=msg.file) if msg.text: self._bot_send_msg(chat, msg.text) msg.file.close() else: raise EFBMessageTypeNotSupported() msg.uid = ews_utils.generate_message_uid(r) self.logger.debug('WeChat message is assigned with unique ID: %s', msg.uid) return msg
def send_message(self, msg: Message) -> Message: """Send a message to WeChat. Supports text, image, sticker, and file. Args: msg (channel.Message): Message Object to be sent. Returns: This method returns nothing. Raises: EFBChatNotFound: Raised when a chat required is not found. EFBMessageTypeNotSupported: Raised when the message type sent is not supported by the channel. EFBOperationNotSupported: Raised when an message edit request is sent, but not supported by the channel. EFBMessageNotFound: Raised when an existing message indicated is not found. E.g.: The message to be edited, the message referred in the :attr:`msg.target <.Message.target>` attribute. EFBMessageError: Raised when other error occurred while sending or editing the message. """ if msg.chat == self.user_auth_chat: raise EFBChatNotFound chat: wxpy.Chat = self.chats.get_wxpy_chat_by_uid(msg.chat.uid) # List of "SentMessage" response for all messages sent r: List[wxpy.SentMessage] = [] self.logger.info("[%s] Sending message to WeChat:\n" "uid: %s\n" "UserName: %s\n" "NickName: %s\n" "Type: %s\n" "Text: %s", msg.uid, msg.chat.uid, chat.user_name, chat.name, msg.type, msg.text) try: chat.mark_as_read() except wxpy.ResponseError as e: self.logger.exception( "[%s] Error occurred while marking chat as read. (%s)", msg.uid, e) send_text_only = False self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit) if msg.edit and msg.uid: if self.flag('delete_on_edit'): msg_ids = json.loads(msg.uid) if msg.type in self.MEDIA_MSG_TYPES and not msg.edit_media: # Treat message as text message to prevent resend of media msg_ids = msg_ids[1:] send_text_only = True failed = 0 for i in msg_ids: try: ews_utils.message_id_to_dummy_message(i, self).recall() except wxpy.ResponseError as e: self.logger.error( "[%s] Trying to recall message but failed: %s", msg.uid, e) failed += 1 if failed: raise EFBMessageError( self.ngettext('Failed to recall {failed} out of {total} message, edited message was not sent.', 'Failed to recall {failed} out of {total} messages, edited message was not sent.', len(msg_ids)).format( failed=failed, total=len(msg_ids) )) # Not caching message ID as message recall feedback is not needed in edit mode else: raise EFBOperationNotSupported() if send_text_only or msg.type in [MsgType.Text, MsgType.Link]: if isinstance(msg.target, Message): max_length = self.flag("max_quote_length") qt_txt = msg.target.text or msg.target.type.name if max_length > 0: if len(qt_txt) >= max_length: tgt_text = qt_txt[:max_length] tgt_text += "…" else: tgt_text = qt_txt elif max_length < 0: tgt_text = qt_txt else: tgt_text = "" if isinstance(chat, wxpy.Group) and not isinstance(msg.target.author, SelfChatMember): tgt_alias = "@%s\u2005:" % msg.target.author.display_name else: tgt_alias = "" msg.text = f"「{tgt_alias}{tgt_text}」\n- - - - - - - - - - - - - - -\n{msg.text}" r.append(self._bot_send_msg(chat, msg.text)) self.logger.debug( '[%s] Sent as a text message. %s', msg.uid, msg.text) elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation): self.logger.info("[%s] Image/GIF/Sticker %s", msg.uid, msg.type) convert_to = None file = msg.file assert file is not None if self.flag('send_stickers_and_gif_as_jpeg'): if msg.type == MsgType.Sticker or msg.mime == "image/gif": convert_to = "image/jpeg" else: if msg.type == MsgType.Sticker: convert_to = "image/gif" if convert_to == "image/gif": with NamedTemporaryFile(suffix=".gif") as f: try: img = Image.open(file) try: alpha = img.split()[3] mask = Image.eval( alpha, lambda a: 255 if a <= 128 else 0) except IndexError: mask = Image.eval(img.split()[0], lambda a: 0) img = img.convert('RGB').convert( 'P', palette=Image.ADAPTIVE, colors=255) img.paste(255, mask) img.save(f, transparency=255) msg.path = Path(f.name) self.logger.debug( '[%s] Image converted from %s to GIF', msg.uid, msg.mime) file.close() if f.seek(0, 2) > self.MAX_FILE_SIZE: raise EFBMessageError( self._("Image size is too large. (IS02)")) f.seek(0) r.append(self._bot_send_image(chat, f.name, f)) finally: if not file.closed: file.close() elif convert_to == "image/jpeg": with NamedTemporaryFile(suffix=".jpg") as f: try: img = Image.open(file).convert('RGBA') out = Image.new("RGBA", img.size, (255, 255, 255, 255)) out.paste(img, img) out.convert('RGB').save(f) msg.path = Path(f.name) self.logger.debug( '[%s] Image converted from %s to JPEG', msg.uid, msg.mime) file.close() if f.seek(0, 2) > self.MAX_FILE_SIZE: raise EFBMessageError( self._("Image size is too large. (IS02)")) f.seek(0) r.append(self._bot_send_image(chat, f.name, f)) finally: if not file.closed: file.close() else: try: if file.seek(0, 2) > self.MAX_FILE_SIZE: raise EFBMessageError( self._("Image size is too large. (IS01)")) file.seek(0) self.logger.debug( "[%s] Sending %s (image) to WeChat.", msg.uid, msg.path) filename = msg.filename or (msg.path and msg.path.name) assert filename r.append(self._bot_send_image(chat, filename, file)) finally: if not file.closed: file.close() if msg.text: r.append(self._bot_send_msg(chat, msg.text)) elif msg.type in (MsgType.File, MsgType.Audio): self.logger.info("[%s] Sending %s to WeChat\nFileName: %s\nPath: %s\nFilename: %s", msg.uid, msg.type, msg.text, msg.path, msg.filename) filename = msg.filename or (msg.path and msg.path.name) assert filename and msg.file r.append(self._bot_send_file(chat, filename, file=msg.file)) if msg.text: self._bot_send_msg(chat, msg.text) if not msg.file.closed: msg.file.close() elif msg.type == MsgType.Video: self.logger.info( "[%s] Sending video to WeChat\nFileName: %s\nPath: %s", msg.uid, msg.text, msg.path) filename = msg.filename or (msg.path and msg.path.name) assert filename and msg.file r.append(self._bot_send_video(chat, filename, file=msg.file)) if msg.text: r.append(self._bot_send_msg(chat, msg.text)) if not msg.file.closed: msg.file.close() else: raise EFBMessageTypeNotSupported() msg.uid = ews_utils.generate_message_uid(r) self.logger.debug( 'WeChat message is assigned with unique ID: %s', msg.uid) return msg
def send_message(self, msg: EFBMsg) -> EFBMsg: """ Process a message from slave channel and deliver it to the user. Args: msg (EFBMsg): The message. """ try: xid = msg.uid self.logger.debug("[%s] Slave message delivered to ETM.\n%s", xid, msg) chat_uid = utils.chat_id_to_str(chat=msg.chat) tg_chat = self.db.get_chat_assoc(slave_uid=chat_uid) if tg_chat: tg_chat = tg_chat[0] self.logger.debug("[%s] The message should deliver to %s", xid, tg_chat) if tg_chat == ETMChat.MUTE_CHAT_ID: self.logger.debug("[%s] Sender of the message is muted.", xid) return msg multi_slaves = False if tg_chat: slaves = self.db.get_chat_assoc(master_uid=tg_chat) if slaves and len(slaves) > 1: multi_slaves = True self.logger.debug("[%s] Sender is linked with other chats in a Telegram group.", xid) self.logger.debug("[%s] Message is in chat %s", xid, msg.chat) # Generate chat text template & Decide type target tg_dest = self.channel.config['admins'][0] if tg_chat: # if this chat is linked tg_dest = int(utils.chat_id_str_to_id(tg_chat)[1]) msg_template = self.generate_message_template(msg, tg_chat, multi_slaves) self.logger.debug("[%s] Message is sent to Telegram chat %s, with header \"%s\".", xid, tg_dest, msg_template) # When editing message old_msg_id: Tuple[str, str] = None if msg.edit: old_msg = self.db.get_msg_log(slave_msg_id=msg.uid, slave_origin_uid=utils.chat_id_to_str(chat=msg.chat)) if old_msg: old_msg_id: Tuple[str, str] = utils.message_id_str_to_id(old_msg.master_msg_id) else: self.logger.info('[%s] Was supposed to edit this message, ' 'but it does not exist in database. Sending new message instead.', msg.uid) # When targeting a message (reply to) target_msg_id: Tuple[str, str] = None if isinstance(msg.target, EFBMsg): self.logger.debug("[%s] Message is replying to %s.", msg.uid, msg.target) log = self.db.get_msg_log( slave_msg_id=msg.target.uid, slave_origin_uid=utils.chat_id_to_str(chat=msg.target.chat) ) if not log: self.logger.debug("[%s] Target message %s is not found in database.", msg.uid, msg.target) else: self.logger.debug("[%s] Target message has database entry: %s.", msg.uid, log) target_msg_id = utils.message_id_str_to_id(log.master_msg_id) if not target_msg_id or target_msg_id[0] != str(tg_dest): self.logger.error('[%s] Trying to reply to a message not from this chat. ' 'Message destination: %s. Target message: %s.', msg.uid, tg_dest, target_msg_id) else: target_msg_id = target_msg_id[1] commands: Optional[List[EFBMsgCommand]] = None reply_markup: Optional[telegram.InlineKeyboardMarkup] = None if msg.commands: commands = msg.commands.commands if old_msg_id: raise EFBMessageError(self._('Command message cannot be edited')) buttons = [] for i, ival in enumerate(commands): buttons.append([telegram.InlineKeyboardButton(ival.name, callback_data=str(i))]) reply_markup = telegram.InlineKeyboardMarkup(buttons) msg.text = msg.text or "" # Type dispatching if msg.type == MsgType.Text: tg_msg = self.slave_message_text(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Link: tg_msg = self.slave_message_link(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type in [MsgType.Image, MsgType.Sticker]: tg_msg = self.slave_message_image(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.File: tg_msg = self.slave_message_file(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Audio: tg_msg = self.slave_message_audio(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Location: tg_msg = self.slave_message_location(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Video: tg_msg = self.slave_message_video(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Unsupported: tg_msg = self.slave_message_unsupported(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) else: self.bot.send_chat_action(tg_dest, telegram.ChatAction.TYPING) tg_msg = self.bot.send_message(tg_dest, prefix=msg_template, text=self._("Unsupported type of message. (UT01)")) if tg_msg and msg.commands: self.channel.commands.register_command(tg_msg, ETMCommandMsgStorage( commands, coordinator.slaves[msg.chat.channel_id], msg_template, msg.text )) self.logger.debug("[%s] Message is sent to the user.", xid) if not msg.author.is_system: msg_log = {"master_msg_id": utils.message_id_to_str(tg_msg.chat.id, tg_msg.message_id), "text": msg.text or "Sent a %s." % msg.type, "msg_type": msg.type, "sent_to": "master" if msg.author.is_self else 'slave', "slave_origin_uid": utils.chat_id_to_str(chat=msg.chat), "slave_origin_display_name": msg.chat.chat_alias, "slave_member_uid": msg.author.chat_uid if not msg.author.is_self else None, "slave_member_display_name": msg.author.chat_alias if not msg.author.is_self else None, "slave_message_id": msg.uid, "update": msg.edit } self.db.add_msg_log(**msg_log) self.logger.debug("[%s] Message inserted/updated to the database.", xid) except Exception as e: self.logger.error("[%s] Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", xid, repr(msg), repr(e), traceback.format_exc())