def process_message(self, message: EFBMsg) -> Optional[EFBMsg]: """ Process a message with middleware Args: message (:obj:`.EFBMsg`): Message object to process Returns: Optional[:obj:`.EFBMsg`]: Processed message or None if discarded. """ if not self.sent_by_master(message): return message fn = message.filename if not (message.type == MsgType.Sticker or (fn and (fn.endswith('.png') or fn.endswith('.gif')))): return message filename = message.filename self.logger.info(f"Converting {filename} to JPEG...") sticker = Image.open(message.file.file.raw) img = sticker.convert('RGB') # Create a new file message.file.close() message.file = NamedTemporaryFile(suffix='.jpg') message.filename = os.path.basename(message.file.name) img_data = io.BytesIO() img.save(img_data, format='jpeg') message.file.write(img_data.getvalue()) message.file.file.seek(0) message.type = MsgType.Image message.mime = 'image/jpeg' message.path = message.file.name return message
def master_qr_code(self, uuid, status, qrcode=None): status = int(status) if self.qr_uuid == (uuid, status): return self.qr_uuid = (uuid, status) msg = EFBMsg() msg.uid = f"ews_auth_{uuid}_{status}" msg.type = MsgType.Text msg.chat = EFBChat(self).system() msg.chat.chat_name = self._("EWS User Auth") msg.author = msg.chat msg.deliver_to = coordinator.master if status == 201: msg.type = MsgType.Text msg.text = self._('Confirm on your phone.') elif status == 200: msg.type = MsgType.Text msg.text = self._("Successfully logged in.") elif uuid != self.qr_uuid: msg.type = MsgType.Image file = NamedTemporaryFile(suffix=".png") qr_url = "https://login.weixin.qq.com/l/" + uuid QRCode(qr_url).png(file, scale=10) msg.text = self._("QR code expired, please scan the new one.") msg.path = file.name msg.file = file msg.mime = 'image/png' if status in (200, 201) or uuid != self.qr_uuid: coordinator.send_message(msg)
def master_qr_code(self, uuid, status, qrcode=None): status = int(status) msg = EFBMsg() msg.type = MsgType.Text msg.chat = EFBChat(self).system() msg.chat.chat_name = self._("EWS User Auth") msg.author = msg.chat msg.deliver_to = coordinator.master if status == 201: msg.type = MsgType.Text msg.text = self._('Confirm on your phone.') elif status == 200: msg.type = MsgType.Text msg.text = self._("Successfully logged in.") elif uuid != self.qr_uuid: msg.type = MsgType.Image # path = os.path.join("storage", self.channel_id) # if not os.path.exists(path): # os.makedirs(path) # path = os.path.join(path, 'QR-%s.jpg' % int(time.time())) # self.logger.debug("master_qr_code file path: %s", path) file = NamedTemporaryFile(suffix=".png") qr_url = "https://login.weixin.qq.com/l/" + uuid QRCode(qr_url).png(file, scale=10) msg.text = self._("QR code expired, please scan the new one.") msg.path = file.name msg.file = file msg.mime = 'image/png' if status in (200, 201) or uuid != self.qr_uuid: coordinator.send_message(msg) self.qr_uuid = uuid
def qq_file_after_wrapper(self, data): efb_msg = EFBMsg() efb_msg.file = data['file'] efb_msg.type = MsgType.File mime = magic.from_file(efb_msg.file.name, mime=True) if isinstance(mime, bytes): mime = mime.decode() efb_msg.path = efb_msg.file.name efb_msg.mime = mime efb_msg.filename = quote(data['filename']) return efb_msg
def test_basic_verify(self): with self.subTest("Valid text message"): msg = EFBMsg() msg.deliver_to = self.channel msg.author = self.chat msg.chat = self.chat msg.type = MsgType.Text msg.text = "Message" msg.verify() for i in (MsgType.Image, MsgType.Audio, MsgType.File, MsgType.Sticker): with self.subTest(f"Valid {i} message"), NamedTemporaryFile() as f: msg = EFBMsg() msg.deliver_to = self.channel msg.author = self.chat msg.chat = self.chat msg.type = i msg.file = f msg.filename = "test.bin" msg.path = f.name msg.mime = "application/octet-stream" msg.verify() with self.subTest("Missing deliver_to"), self.assertRaises(ValueError): msg = EFBMsg() msg.author = self.chat msg.chat = self.chat msg.type = MsgType.Text msg.text = "Message" msg.verify() with self.subTest("Missing author"), self.assertRaises(ValueError): msg = EFBMsg() msg.deliver_to = self.channel msg.chat = self.chat msg.type = MsgType.Text msg.text = "Message" msg.verify() with self.subTest("Missing chat"), self.assertRaises(ValueError): msg = EFBMsg() msg.deliver_to = self.channel msg.author = self.chat msg.type = MsgType.Text msg.text = "Message" msg.verify() with self.subTest("Missing type"), self.assertRaises(ValueError): msg = EFBMsg() msg.deliver_to = self.channel msg.author = self.chat msg.chat = self.chat msg.text = "Message" msg.verify()
def test_verify_media_msg(chat, master_channel, media_type): with NamedTemporaryFile() as f: msg = EFBMsg() msg.deliver_to = master_channel msg.author = chat msg.chat = chat msg.type = media_type msg.file = f msg.filename = "test.bin" msg.path = f.name msg.mime = "application/octet-stream" msg.verify()
def test_pickle_media_message(media_type): with NamedTemporaryFile() as f: msg = EFBMsg() msg.deliver_to = coordinator.master msg.author = chat msg.chat = chat msg.type = media_type msg.file = f msg.filename = "test.bin" msg.path = f.name msg.mime = "application/octet-stream" msg.uid = "message_id" msg.verify() msg_dup = pickle.loads(pickle.dumps(msg)) for attr in ("deliver_to", "author", "chat", "type", "chat", "filename", "path", "mime", "text", "uid"): assert getattr(msg, attr) == getattr(msg_dup, attr)
def qq_record_wrapper(self, data): efb_msg = EFBMsg() try: transformed_file = self.inst.coolq_api_query("get_record", file=data['file'], out_format='mp3') efb_msg.type = MsgType.Audio efb_msg.file = download_voice(transformed_file['file'], self.inst.client_config['api_root'].rstrip("/"), self.inst.client_config['access_token']) mime = magic.from_file(efb_msg.file.name, mime=True) if isinstance(mime, bytes): mime = mime.decode() efb_msg.path = efb_msg.file.name efb_msg.mime = mime except Exception: efb_msg.type = MsgType.Unsupported efb_msg.text = self._('[Voice Message] Please check it on your QQ') return [efb_msg]
def qq_image_wrapper(self, data) -> EFBMsg: efb_msg = EFBMsg() if 'url' not in data: efb_msg.type = MsgType.Text efb_msg.text = '[Download image failed, please check on your QQ client]' return efb_msg efb_msg.file = cq_get_image(data['url']) if efb_msg.file is None: efb_msg.type = MsgType.Text efb_msg.text = '[Download image failed, please check on your QQ client]' return efb_msg efb_msg.type = MsgType.Image mime = magic.from_file(efb_msg.file.name, mime=True) if isinstance(mime, bytes): mime = mime.decode() efb_msg.path = efb_msg.file.name efb_msg.mime = mime return efb_msg
def qq_image_wrapper(self, data): efb_msg = EFBMsg() if 'url' not in data: efb_msg.type = MsgType.Text efb_msg.text = self._('[Image Source missing]') return efb_msg efb_msg.file = cq_get_image(data['url']) if efb_msg.file is None: efb_msg.type = MsgType.Text efb_msg.text = self._('[Download image failed, please check on your QQ client]') return efb_msg efb_msg.type = MsgType.Image mime = magic.from_file(efb_msg.file.name, mime=True) if isinstance(mime, bytes): mime = mime.decode() efb_msg.filename = efb_msg.file.name efb_msg.path = efb_msg.file.name efb_msg.mime = mime if "gif" in mime: efb_msg.type = MsgType.Animation return [efb_msg]
def process_telegram_message(self, bot: telegram.Bot, update: telegram.Update, channel_id: Optional[str] = None, chat_id: Optional[str] = None, target_msg: Optional[str] = None): """ Process messages came from Telegram. Args: bot: Telegram bot update: Telegram message update channel_id: Slave channel ID if specified chat_id: Slave chat ID if specified target_msg: Target slave message if specified Returns: """ target: Optional[str] = None target_channel: Optional[str] = None target_log: Optional['MsgLog'] = None # Message ID for logging message_id = utils.message_id_to_str(update=update) multi_slaves: bool = False destination: Optional[str] = None slave_msg: Optional[EFBMsg] = None message: telegram.Message = update.effective_message edited = bool(update.edited_message or update.edited_channel_post) self.logger.debug('[%s] Message is edited: %s, %s', message_id, edited, message.edit_date) private_chat = update.effective_chat.type == telegram.Chat.PRIVATE if not private_chat: # from group linked_chats = self.db.get_chat_assoc( master_uid=utils.chat_id_to_str(self.channel_id, update.effective_chat.id)) if len(linked_chats) == 1: destination = linked_chats[0] elif len(linked_chats) > 1: multi_slaves = True reply_to = bool(getattr(message, "reply_to_message", None)) # Process predefined target (slave) chat. if channel_id and chat_id: destination = utils.chat_id_to_str(channel_id, chat_id) if target_msg: target_log = self.db.get_msg_log(master_msg_id=target_msg) if target_log: target = target_log.slave_origin_uid target_channel, target_uid = utils.chat_id_str_to_id( target) else: self.logger.info( "[%s], Predefined chat %d.%d with target msg") return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another message. (UC07)")) elif private_chat: if reply_to: dest_msg = self.db.get_msg_log( master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if dest_msg: destination = dest_msg.slave_origin_uid else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another one. (UC03)")) else: return self.bot.reply_error( update, self._("Please reply to an incoming message. (UC04)")) else: # group chat if multi_slaves: if reply_to: dest_msg = self.db.get_msg_log( master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if dest_msg: destination = dest_msg.slave_origin_uid else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another one. (UC05)")) else: return self.bot.reply_error( update, self. _("This group is linked to multiple remote chats. " "Please reply to an incoming message. " "To unlink all remote chats, please send /unlink_all . (UC06)" )) elif destination: if reply_to: target_log = \ self.db.get_msg_log(master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if target_log: target = target_log.slave_origin_uid target_channel, target_uid = utils.chat_id_str_to_id( target) else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another message. (UC07)")) else: return self.bot.reply_error( update, self._("This group is not linked to any chat. (UC06)")) self.logger.debug( "[%s] Telegram received. From private chat: %s; Group has multiple linked chats: %s; " "Message replied to another message: %s", message_id, private_chat, multi_slaves, reply_to) self.logger.debug("[%s] Destination chat = %s", message_id, destination) channel, uid = utils.chat_id_str_to_id(destination) if channel not in coordinator.slaves: return self.bot.reply_error( update, self._("Internal error: Channel \"{0}\" not found.").format( channel)) m = EFBMsg() try: m.uid = message_id mtype = get_msg_type(message) # Chat and author related stuff m.author = EFBChat(self.channel).self() m.chat = EFBChat(coordinator.slaves[channel]) m.chat.chat_uid = uid chat_info = self.db.get_slave_chat_info(channel, uid) if chat_info: m.chat.chat_name = chat_info.slave_chat_name m.chat.chat_alias = chat_info.slave_chat_alias m.chat.chat_type = ChatType(chat_info.slave_chat_type) m.deliver_to = coordinator.slaves[channel] if target and target_channel == channel: trgt_msg = EFBMsg() trgt_msg.type = MsgType.Text trgt_msg.text = target_log.text trgt_msg.uid = target_log.slave_message_id trgt_msg.chat = EFBChat(coordinator.slaves[target_channel]) trgt_msg.chat.chat_name = target_log.slave_origin_display_name trgt_msg.chat.chat_alias = target_log.slave_origin_display_name trgt_msg.chat.chat_uid = utils.chat_id_str_to_id( target_log.slave_origin_uid)[1] if target_log.slave_member_uid: trgt_msg.author = EFBChat( coordinator.slaves[target_channel]) trgt_msg.author.chat_name = target_log.slave_member_display_name trgt_msg.author.chat_alias = target_log.slave_member_display_name trgt_msg.author.chat_uid = target_log.slave_member_uid elif target_log.sent_to == 'master': trgt_msg.author = trgt_msg.chat else: trgt_msg.author = EFBChat(self.channel).self() m.target = trgt_msg self.logger.debug( "[%s] This message replies to another message of the same channel.\n" "Chat ID: %s; Message ID: %s.", message_id, trgt_msg.chat.chat_uid, trgt_msg.uid) # Type specific stuff self.logger.debug("[%s] Message type from Telegram: %s", message_id, mtype) if self.TYPE_DICT.get(mtype, None): m.type = self.TYPE_DICT[mtype] self.logger.debug("[%s] EFB message type: %s", message_id, mtype) else: self.logger.info( "[%s] Message type %s is not supported by ETM", message_id, mtype) raise EFBMessageTypeNotSupported( "Message type %s is not supported by ETM" % mtype) if m.type not in coordinator.slaves[ channel].supported_message_types: self.logger.info( "[%s] Message type %s is not supported by channel %s", message_id, m.type.name, channel) raise EFBMessageTypeNotSupported( "Message type %s is not supported by channel %s" % (m.type, coordinator.slaves[channel].channel_name)) # Parse message text and caption to markdown msg_md_text = message.text and message.text_markdown if msg_md_text and msg_md_text == escape_markdown(message.text): msg_md_text = message.text msg_md_text = msg_md_text or "" msg_md_caption = message.caption and message.caption_markdown if msg_md_caption and msg_md_caption == escape_markdown( message.caption): msg_md_caption = message.caption msg_md_caption = msg_md_caption or "" # Flag for edited message if edited: m.edit = True text = msg_md_text or msg_md_caption msg_log = self.db.get_msg_log( master_msg_id=utils.message_id_to_str(update=update)) if not msg_log or msg_log == self.FAIL_FLAG: raise EFBMessageNotFound() m.uid = msg_log.slave_message_id if text.startswith(self.DELETE_FLAG): coordinator.send_status( EFBMessageRemoval( source_channel=self.channel, destination_channel=coordinator.slaves[channel], message=m)) self.db.delete_msg_log( master_msg_id=utils.message_id_to_str(update=update)) m = None return self.logger.debug('[%s] Message is edited (%s)', m.uid, m.edit) # Enclose message as an EFBMsg object by message type. if mtype == TGMsgType.Text: m.text = msg_md_text elif mtype == TGMsgType.Photo: m.text = msg_md_caption m.file, m.mime, m.filename, m.path = self._download_file( message.photo[-1]) elif mtype == TGMsgType.Sticker: # Convert WebP to the more common PNG m.text = "" m.file, m.mime, m.filename, m.path = self._download_file( message.sticker, 'image/webp') self.logger.debug( "[%s] Trying to convert WebP sticker (%s) to PNG.", message_id, m.path) f = tempfile.NamedTemporaryFile(suffix=".png") Image.open(m.file).convert("RGBA").save(f, 'png') m.file.close() m.file, m.mime, m.filename, m.path = f, 'image/png', os.path.basename( f.name), f.name self.logger.debug( "[%s] WebP sticker is converted to PNG (%s).", message_id, f.name) elif mtype == TGMsgType.Animation: m.text = "" self.logger.debug( "[%s] Telegram message is a \"Telegram GIF\".", message_id) m.filename = getattr(message.document, "file_name", None) or None m.file, m.mime, m.filename, m.path = self._download_gif( message.document, channel) m.mime = message.document.mime_type or m.mime elif mtype == TGMsgType.Document: m.text = msg_md_caption self.logger.debug("[%s] Telegram message type is document.", message_id) m.filename = getattr(message.document, "file_name", None) or None m.file, m.mime, filename, m.path = self._download_file( message.document, message.document.mime_type) m.filename = m.filename or filename m.mime = message.document.mime_type or m.mime elif mtype == TGMsgType.Video: m.text = msg_md_caption m.file, m.mime, m.filename, m.path = self._download_file( message.video, message.video.mime_type) elif mtype == TGMsgType.Audio: m.text = "%s - %s\n%s" % (message.audio.title, message.audio.performer, msg_md_caption) m.file, m.mime, m.filename, m.path = self._download_file( message.audio, message.audio.mime_type) elif mtype == TGMsgType.Voice: m.text = msg_md_caption m.file, m.mime, m.filename, m.path = self._download_file( message.voice, message.voice.mime_type) elif mtype == TGMsgType.Location: # TRANSLATORS: Message body text for location messages. m.text = self._("Location") m.attributes = EFBMsgLocationAttribute( message.location.latitude, message.location.longitude) elif mtype == TGMsgType.Venue: m.text = message.location.title + "\n" + message.location.adderss m.attributes = EFBMsgLocationAttribute( message.venue.location.latitude, message.venue.location.longitude) elif mtype == TGMsgType.Contact: contact: telegram.Contact = message.contact m.text = self._( "Shared a contact: {first_name} {last_name}\n{phone_number}" ).format(first_name=contact.first_name, last_name=contact.last_name, phone_number=contact.phone_number) else: raise EFBMessageTypeNotSupported( self._("Message type {0} is not supported.").format(mtype)) # return self.bot.reply_error(update, "Message type not supported. (MN02)") slave_msg = coordinator.send_message(m) except EFBChatNotFound as e: self.bot.reply_error(update, e.args[0] or self._("Chat is not found.")) except EFBMessageTypeNotSupported as e: self.bot.reply_error( update, e.args[0] or self._("Message type is not supported.")) except EFBOperationNotSupported as e: self.bot.reply_error( update, self._("Message editing is not supported.\n\n{!s}".format(e))) except Exception as e: self.bot.reply_error( update, self._("Message is not sent.\n\n{!r}".format(e))) finally: if m: msg_log_d = { "master_msg_id": utils.message_id_to_str(update=update), "text": m.text or "Sent a %s" % m.type, "slave_origin_uid": utils.chat_id_to_str(chat=m.chat), "slave_origin_display_name": "__chat__", "msg_type": m.type, "sent_to": "slave", "slave_message_id": None if m.edit else "%s.%s" % (self.FAIL_FLAG, int(time.time())), # Overwritten later if slave message ID exists "update": m.edit } # Store media related information to local database for tg_media_type in ('audio', 'animation', 'document', 'video', 'voice', 'video_note'): attachment = getattr(message, tg_media_type, None) if attachment: msg_log_d.update(media_type=tg_media_type, file_id=attachment.file_id, mime=attachment.mime_type) break if not msg_log_d.get('media_type', None): if getattr(message, 'sticker', None): msg_log_d.update(media_type='sticker', file_id=message.sticker.file_id, mime='image/webp') elif getattr(message, 'photo', None): attachment = message.photo[-1] msg_log_d.update(media_type=tg_media_type, file_id=attachment.file_id, mime='image/jpeg') if slave_msg: msg_log_d['slave_message_id'] = slave_msg.uid self.db.add_msg_log(**msg_log_d) if m.file: m.file.close()
def poll(self): error_count = 0 first_load = False last_msg_dict = {} path_pattern = os.path.join(self.storage_path, "last_msg.json") if os.path.isfile(path_pattern): with open(path_pattern, "r") as f: last_msg_dict = json.loads(f.read()) else: first_load = True while True: try: chat_list = requests.get(user_url % (self.hostname, self.port)).json() # Check message update for chat_item in chat_list: user_id = chat_item["userId"] if user_id in ["weixin", "notifymessage"]: continue # Get current message list msg_list = requests.get( chat_url % (self.hostname, self.port, user_id, 5)).json()[1:] cur_date = '{0:%y-%m-%d}'.format(datetime.now()) # Get last message last_msg = {} if user_id in last_msg_dict: last_msg = last_msg_dict[user_id] # Record last message if len(msg_list) > 0: last_msg_dict[user_id] = clean_msg( msg_list[0], cur_date) if first_load: continue # Get new message list new_msg_list = [] for idx, msg in enumerate(msg_list): if newer_msg(msg, last_msg, cur_date, is_latest=(idx == 0)): new_msg_list.append(clean_msg(msg, cur_date)) else: break new_msg_list.reverse() # Handle message for msg in new_msg_list: sender = "Unknown" content = msg["title"] if ":" in msg["title"] and msg["userId"].endswith( "@chatroom"): segs = msg["title"].split(":") sender = segs[0] content = ":".join(segs[1:]) if "from" in msg: sender = msg["from"] #print("sender: %s\ncontent: %s\nurl: %s\ntime: %s\n" % (sender, content, msg["url"], msg["subTitle"])) # default: text efb_msg = EFBMsg() efb_msg.type = MsgType.Text efb_msg.text = content if len(msg["url"]) > 0: # Check picture if msg["url"].endswith( ".gif") or msg["url"].endswith( ".jpg") or msg["url"].endswith( ".jpeg") or msg["url"].endswith( ".png"): efb_msg.type = MsgType.Image efb_msg.file = self.cq_get_image(msg["url"]) mime = magic.from_file(msg["url"], mime=True) if isinstance(mime, bytes): mime = mime.decode() efb_msg.path = msg["url"] efb_msg.mime = mime # Check video elif msg["url"].endswith(".mp4"): efb_msg.type = MsgType.Video efb_msg.file = self.cq_get_image(msg["url"]) mime = magic.from_file(msg["url"], mime=True) if isinstance(mime, bytes): mime = mime.decode() efb_msg.path = msg["url"] efb_msg.mime = mime # Check document elif msg["url"].startswith("/"): efb_msg.type = MsgType.File efb_msg.file = self.cq_get_image(msg["url"]) efb_msg.path = msg["url"] # Add link to content else: efb_msg.type = MsgType.Text if not msg["url"] in content: content += "\n(" + msg["url"] + ")" efb_msg.text = content efb_msg.uid = user_id efb_msg.chat = self.get_efb_chat(user_id) if efb_msg.chat.chat_type == ChatType.User: efb_msg.text = sender + ":\n" + content efb_msg.author = efb_msg.chat else: author = copy.deepcopy(efb_msg.chat) author.chat_type = ChatType.User author.chat_name = sender efb_msg.author = author efb_msg.deliver_to = coordinator.master self.send_message_wrapper(efb_msg) path_pattern = os.path.join(self.storage_path, "last_msg.json") with open(path_pattern, "w") as f: f.write(json.dumps(last_msg_dict)) first_load = False except requests.exceptions.RequestException as e: print(e) error_count += 1 if error_count >= 12: context = { 'message': 'Connection Failed', 'uid_prefix': 'alert', 'event_description': 'WeChat Alert' } self.send_msg_to_master(context) error_count = 0 except json.decoder.JSONDecodeError as e: print(e) error_count += 1 if error_count >= 12: context = { 'message': 'Connection Failed', 'uid_prefix': 'alert', 'event_description': 'WeChat Alert' } self.send_msg_to_master(context) error_count = 0 except Exception as e: print(e) time.sleep(5)