import urllib from concurrent.futures._base import CancelledError, TimeoutError from aiogram.utils.markdown import quote_html, hlink from aiovk.longpoll import LongPoll from aiogram.utils.exceptions import MessageError from bot import * log = logging.getLogger('vk_messages') inline_link_re = re.compile('\[([a-zA-Z0-9_]*)\|(.*?)\]', re.MULTILINE) ################### Честно взято по лицензии https://github.com/vk-brain/sketal/blob/master/LICENSE ################### def parse_msg_flags(bitmask, keys=('unread', 'outbox', 'replied', 'important', 'chat', 'friends', 'spam', 'deleted', 'fixed', 'media', 'hidden')): """Функция для чтения битовой маски и возврата словаря значений""" start = 1 values = [] for _ in range(1, 12): result = bitmask & start start *= 2 values.append(bool(result)) return dict(zip(keys, values)) from enum import Enum class Wait(Enum): NO = 0 YES = 1 CUSTOM = 2 class EventType(Enum): Longpoll = 0 ChatChange = 1 Callback = 2 class Event: __slots__ = ("api", "type", "reserved_by", "occupied_by", "meta") def __init__(self, api, evnt_type): self.api = api self.type = evnt_type self.meta = {} self.reserved_by = [] self.occupied_by = [] # https://vk.com/dev/using_longpoll class LongpollEvent(Event): __slots__ = ("evnt_data", "id") def __init__(self, api, evnt_id, evnt_data): super().__init__(api, EventType.Longpoll) self.id = evnt_id self.evnt_data = evnt_data def __str__(self): return f"LongpollEvent ({self.id}, {self.evnt_data[1] if len(self.evnt_data) > 1 else '_'})" class MessageEventData(object): __slots__ = ("is_multichat", "user_id", "full_text", "full_message_data", "time", "msg_id", "attaches", "is_out", "forwarded", "chat_id", "true_user_id", "is_forwarded", "true_msg_id") @staticmethod def from_message_body(obj): data = MessageEventData() data.attaches = {} data.forwarded = [] c = 0 for a in obj.get("attachments", []): c += 1 data.attaches[f'attach{c}_type'] = a['type'] try: data.attaches[f'attach{c}'] = f'{a[a["type"]]["owner_id"]}_{a[a["type"]]["id"]}' except KeyError: data.attaches[f'attach{c}'] = "" if 'fwd_messages' in obj: data.forwarded = MessageEventData.parse_brief_forwarded_messages(obj) if "chat_id" in obj: data.is_multichat = True data.chat_id = int(obj["chat_id"]) if "id" in obj: data.msg_id = obj["id"] data.true_msg_id = obj["id"] data.user_id = int(obj['user_id']) data.true_user_id = int(obj['user_id']) data.full_text = obj['text'] data.time = int(obj['date']) data.is_out = obj.get('out', False) data.is_forwarded = False data.full_message_data = obj return data @staticmethod def parse_brief_forwarded_messages(obj): if 'fwd_messages' not in obj: return () result = [] for mes in obj['fwd_messages']: result.append((mes.get('id', None), MessageEventData.parse_brief_forwarded_messages(mes))) return tuple(result) @staticmethod def parse_brief_forwarded_messages_from_lp(data): result = [] token = "" i = -1 while True: i += 1 if i >= len(data): if token: result.append((token, ())) break if data[i] in "1234567890_-": token += data[i] continue if data[i] in (",", ")"): if not token: continue result.append((token, ())) token = "" continue if data[i] == ":": stack = 1 for j in range(i + 2, len(data)): if data[j] == "(": stack += 1 elif data[j] == ")": stack -= 1 if stack == 0: jump_to_i = j break sub_data = data[i + 2: jump_to_i] result.append((token, MessageEventData.parse_brief_forwarded_messages_from_lp(sub_data))) i = jump_to_i + 1 token = "" continue return tuple(result) def __init__(self): self.is_multichat = False self.is_forwarded = False self.is_out = False self.chat_id = 0 self.user_id = 0 self.true_user_id = 0 self.full_text = "" self.time = "" self.msg_id = 0 self.true_msg_id = 0 self.attaches = None self.forwarded = None self.full_message_data = None class Attachment(object): __slots__ = ('type', 'owner_id', 'id', 'access_key', 'url', 'ext') def __init__(self, attach_type, owner_id, aid, access_key=None, url=None, ext=None): self.type = attach_type self.owner_id = owner_id self.id = aid self.access_key = access_key self.url = url self.ext = ext @staticmethod def from_upload_result(result, attach_type="photo"): url = None for k in result: if "photo_" in k: url = result[k] elif "link_" in k: url = result[k] elif "url" == k: url = result[k] return Attachment(attach_type, result["owner_id"], result["id"], url=url, ext=result.get("ext")) @staticmethod def from_raw(raw_attach): a_type = raw_attach['type'] attach = raw_attach[a_type] url = None for k, v in attach.items(): if "photo_" in k: url = v elif "link_" in k: url = v elif "url" == k: url = v return Attachment(a_type, attach.get('owner_id', ''), attach.get('id', ''), attach.get('access_key'), url, ext=attach.get("ext")) def value(self): if self.access_key: return f'{self.type}{self.owner_id}_{self.id}_{self.access_key}' return f'{self.type}{self.owner_id}_{self.id}' def __str__(self): return self.value() MAX_LENGHT = 4000 from math import ceil class LPMessage(object): """Класс, объект которого передаётся в плагин для упрощённого ответа""" __slots__ = ('message_data', 'api', 'is_multichat', 'chat_id', 'user_id', 'is_out', 'true_user_id', 'timestamp', 'answer_values', 'msg_id', 'text', 'full_text', 'meta', 'is_event', 'brief_attaches', 'brief_forwarded', '_full_attaches', '_full_forwarded', 'reserved_by', 'occupied_by', 'peer_id', "is_forwarded", 'true_msg_id') def __init__(self, vk_api_object, message_data): self.message_data = message_data self.api = vk_api_object self.reserved_by = [] self.occupied_by = [] self.meta = {} self.is_event = False self.is_multichat = message_data.is_multichat self.is_forwarded = message_data.is_forwarded self.user_id = message_data.user_id self.true_user_id = message_data.true_user_id self.chat_id = message_data.chat_id self.peer_id = (message_data.chat_id or message_data.user_id) + self.is_multichat * 2000000000 self.full_text = message_data.full_text self.text = self.full_text.replace(""", "\"") # Not need .lower() there # edited by @Kylmakalle self.msg_id = message_data.msg_id self.true_msg_id = message_data.true_msg_id self.is_out = message_data.is_out self.timestamp = message_data.time self.brief_forwarded = message_data.forwarded self._full_forwarded = None self.brief_attaches = message_data.attaches self._full_attaches = None if self.is_multichat: self.answer_values = {'chat_id': self.chat_id} else: self.answer_values = {'user_id': self.user_id} async def get_full_attaches(self): """Get list of all attachments as `Attachment` for this message""" if self._full_attaches is None: await self.get_full_data() return self._full_attaches async def get_full_forwarded(self): """Get list of all forwarded messages as `LPMessage` for this message""" if self._full_forwarded is None: await self.get_full_data() return self._full_forwarded async def get_full_data(self, message_data=None): """Update lists of all forwarded messages and all attachments for this message""" self._full_attaches = [] self._full_forwarded = [] if not message_data: values = {'message_ids': self.msg_id} full_message_data = await self.api.messages.getById(**values) if not full_message_data or not full_message_data['items']: # Если пришёл пустой ответ от VK API return message = full_message_data['items'][0] else: message = message_data if "attachments" in message: for raw_attach in message["attachments"]: attach = Attachment.from_raw(raw_attach) # Создаём аттач self._full_attaches.append(attach) # Добавляем к нашему внутреннему списку аттачей if 'fwd_messages' in message: self._full_forwarded, self.brief_forwarded = await self.parse_forwarded_messages(message) async def parse_forwarded_messages(self, im): if 'fwd_messages' not in im: return (), () result = [] brief_result = [] for mes in im['fwd_messages']: obj = MessageEventData.from_message_body(mes) obj.msg_id = self.msg_id obj.chat_id = self.chat_id obj.user_id = self.user_id obj.is_multichat = self.is_multichat obj.is_out = self.is_out obj.is_forwarded = True m = await LPMessage.create(self.api, obj) big_result, small_result = await self.parse_forwarded_messages(mes) result.append((m, big_result)) brief_result.append((m.msg_id, small_result)) return tuple(result), tuple(brief_result) @staticmethod def prepare_message(message): """Split message to parts that can be send by `messages.send`""" message_length = len(message) if message_length <= MAX_LENGHT: return [message] def fit_parts(sep): current_length = 0 current_message = "" sep_length = len(sep) parts = message.split(sep) length = len(parts) for j in range(length): m = parts[j] temp_length = len(m) if temp_length > MAX_LENGHT: return if j != length - 1 and current_length + temp_length + sep_length <= MAX_LENGHT: current_message += m + sep current_length += temp_length + sep_length elif current_length + temp_length <= MAX_LENGHT: current_message += m current_length += temp_length elif current_length + temp_length > MAX_LENGHT: yield current_message current_length = temp_length current_message = m if j != length - 1 and current_length + sep_length < MAX_LENGHT: current_message += sep current_length += sep_length if current_message: yield current_message result = list(fit_parts("\n")) if not result: result = list(fit_parts(" ")) if not result: result = [] for i in range(int(ceil(message_length / MAX_LENGHT))): result.append(message[i * MAX_LENGHT: (i + 1) * MAX_LENGHT]) return result return result @staticmethod async def create(vk_api_object, data): msg = LPMessage(vk_api_object, data) if data.full_message_data: await msg.get_full_data(data.full_message_data) return msg class ChatChangeEvent(Event): __slots__ = ("source_act", "source_mid", "chat_id", "new_title", "old_title", "changer", "chat_id", "new_cover", "user_id") def __init__(self, api, user_id, chat_id, source_act, source_mid, new_title, old_title, new_cover, changer): super().__init__(api, EventType.ChatChange) self.chat_id = chat_id self.user_id = user_id self.source_act = source_act self.source_mid = source_mid self.new_cover = new_cover self.new_title = new_title self.old_title = old_title self.changer = changer async def check_event(api, user_id, chat_id, attaches): if chat_id != 0 and "source_act" in attaches: photo = attaches.get("attach1_type") + attaches.get("attach1") if "attach1" in attaches else None evnt = ChatChangeEvent(api, user_id, chat_id, attaches.get("source_act"), int(attaches.get("source_mid", 0)), attaches.get("source_text"), attaches.get("source_old_text"), photo, int(attaches.get("from", 0))) await process_event(evnt) return True return False async def process_longpoll_event(api, new_event): if not new_event: return event_id = new_event[0] if event_id != 4 and event_id != 5: evnt = LongpollEvent(api, event_id, new_event) return # await process_event(evnt) data = MessageEventData() data.msg_id = new_event[1] data.attaches = new_event[6] data.time = int(new_event[4]) try: data.user_id = int(data.attaches['from']) data.chat_id = int(new_event[3]) - 2000000000 data.is_multichat = True del data.attaches['from'] except KeyError: data.user_id = int(new_event[3]) data.is_multichat = False # https://vk.com/dev/using_longpoll_2 flags = parse_msg_flags(new_event[2]) if flags['outbox']: return data.is_out = True data.full_text = new_event[5].replace('
', '\n') if "fwd" in data.attaches: data.forwarded = MessageEventData.parse_brief_forwarded_messages_from_lp(data.attaches["fwd"]) del data.attaches["fwd"] else: data.forwarded = [] msg = LPMessage(api, data) if await check_event(api, data.user_id, data.chat_id, data.attaches): msg.is_event = True await process_message(msg) ####################################################################################################################### async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, user_id=None, forward_settings=None, vkchat=None, full_msg=None, forwarded=False, vk_msg_id=None, main_message=None, known_users=None, force_disable_notify=None, full_chat=None): token = token or msg.api._session.access_token is_multichat = is_multichat or msg.is_multichat vk_msg_id = vk_msg_id or msg.msg_id user_id = user_id or msg.user_id known_users = known_users or {} header_message = None vkuser = VkUser.objects.filter(token=token).first() if not vkuser: return if user_id not in known_users or {}: peer_id, first_name, last_name = await get_name(user_id, msg.api) known_users[user_id] = (peer_id, first_name, last_name) else: peer_id, first_name, last_name = known_users[user_id] if is_multichat: vk_chat_id = vk_chat_id or msg.peer_id else: vk_chat_id = vk_chat_id or peer_id if not vkchat: vkchat, created_vkchat = await get_vk_chat(vk_chat_id) forward_setting = forward_settings or Forward.objects.filter(owner=vkuser.owner, vkchat=vkchat).first() full_msg = full_msg or await msg.api('messages.getById', message_ids=', '.join(str(x) for x in [vk_msg_id])) # Узнаем title чата if is_multichat: full_chat = await msg.api('messages.getChat', chat_id=vk_chat_id - 2000000000) if full_msg.get('items'): for vk_msg in full_msg['items']: # Формируем ссылку на сообщение на случай ошибки # message id vk_msg_url_chat_id = None if vk_msg.get("peer_id"): try: if int(vk_msg.get("peer_id")) >= 2000000000: vk_msg_url_chat_id = f"c{int(vk_msg.get('peer_id')) - 2000000000}" except: pass if not vk_msg_url_chat_id: vk_msg_url_chat_id = vk_msg.get("from_id") or "" # vk_msg_url_msg_id = vk_msg.get("id") or vk_msg.get("conversation_message_id") or "" vk_msg_url = f'https://vk.com/im?msgid={vk_msg_url_msg_id}&sel={vk_msg_url_chat_id}' disable_notify = force_disable_notify or bool(vk_msg.get('push_settings', False)) attaches_scheme = [] if vk_msg.get('attachments'): attaches_scheme = [await process_attachment(attachment, token, vk_msg_url) for attachment in vk_msg['attachments']] if vk_msg.get('geo'): location = vk_msg['geo']['coordinates']['latitude'], vk_msg['geo']['coordinates']['longitude'] is_venue = vk_msg['geo'].get('place') if is_venue: attaches_scheme.append({'content': [location[0], location[1], is_venue.get('title', 'Место'), is_venue.get('city', 'Город')], 'type': 'venue'}) else: attaches_scheme.append({'content': [location[0], location[1]], 'type': 'location'}) name = first_name + ((' ' + last_name) if last_name else '') if forward_setting: if forwarded or is_multichat: header = f'{name}' + '\n' elif not forwarded: header = '' to_tg_chat = forward_setting.tgchat.cid else: if forwarded or not is_multichat: header = f'{name}' + '\n' elif is_multichat: header = f'{name} @ {quote_html(full_chat["title"])}' + '\n' to_tg_chat = vkuser.owner.uid # Логика реплая на сообщение, которое уже есть в чате if not main_message: if vk_msg.get('reply_message'): reply_msg_in_db = Message.objects.filter( vk_chat=vk_chat_id, vk_id=vk_msg['reply_message'].get('id') or vk_msg['reply_message'].get( 'conversation_message_id'), tg_chat=to_tg_chat ).first() if reply_msg_in_db: main_message = reply_msg_in_db.tg_id body_parts = [] body = quote_html(vk_msg.get('text', '')) if body: if (len(header) + len(body)) > MAX_MESSAGE_LENGTH: body_parts = safe_split_text(header + body, MAX_MESSAGE_LENGTH) body_parts[-1] = body_parts[-1] + '\n' else: body += '\n' if attaches_scheme: first_text_attach = next((attach for attach in attaches_scheme if attach and attach['type'] == 'text'), None) if first_text_attach: if body_parts and (len(first_text_attach) + len(body_parts[-1])) > MAX_MESSAGE_LENGTH: body_parts.append(first_text_attach['content']) else: body += first_text_attach['content'] attaches_scheme.remove(first_text_attach) # ТК у некоторых войсов транскрипт не происходит, то мы можем их потерять. Так делать больше не будем. # first_voice_attach = next( # (attach for attach in attaches_scheme if attach and attach['type'] == 'audio_message'), # None) # if first_voice_attach: # # Будем отправлять только те войсы, в которых завершен транскрипт сообщений # if first_voice_attach.get('transcript_state') != 'done': # return if body_parts: for body_part in range(len(body_parts)): m = inline_link_re.finditer(body_parts[body_part]) for i in m: vk_url = f'https://vk.com/{i.group(1)}' check_url = await check_vk_url(vk_url) if check_url: body_parts[body_part] = body_parts[body_part].replace(i.group(0), hlink(f'{i.group(2)}', url=vk_url)) try: await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) except: return try: # Чтобы не падало при реплае на сообщение из чата внутри ТГ tg_message = await bot.send_message(vkuser.owner.uid, body_parts[body_part], parse_mode=ParseMode.HTML, reply_to_message_id=main_message, disable_notification=disable_notify) except MessageError: # Надо бы обновить aiogram, чтобы можно было ловить MessageToReplyNotFound tg_message = await bot.send_message(vkuser.owner.uid, body_parts[body_part], parse_mode=ParseMode.HTML, reply_to_message_id=None, disable_notification=disable_notify) if body_part == 0: header_message = tg_message if forwarded: main_message = header_message.message_id Message.objects.create( vk_chat=vk_chat_id, vk_id=vk_msg_id, tg_chat=tg_message.chat.id, tg_id=tg_message.message_id ) elif not body_parts and (header + body): m = inline_link_re.finditer(body) for i in m: vk_url = f'https://vk.com/{i.group(1)}' check_url = await check_vk_url(vk_url) if check_url: body = body.replace(i.group(0), hlink(f'{i.group(2)}', url=vk_url)) try: await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) except: return try: # Чтобы не падало при реплае на сообщение из чата внутри ТГ header_message = tg_message = await bot.send_message(to_tg_chat, header + body, parse_mode=ParseMode.HTML, reply_to_message_id=main_message, disable_notification=disable_notify) except MessageError: # Надо бы обновить aiogram, чтобы можно было ловить MessageToReplyNotFound header_message = tg_message = await bot.send_message(to_tg_chat, header + body, parse_mode=ParseMode.HTML, reply_to_message_id=None, disable_notification=disable_notify) if forwarded: main_message = header_message.message_id Message.objects.create( vk_chat=vk_chat_id, vk_id=vk_msg_id, tg_chat=tg_message.chat.id, tg_id=tg_message.message_id ) photo_attachments = [attach for attach in attaches_scheme if attach and attach['type'] == 'photo'] if len(photo_attachments) > 1: media = MediaGroup() for photo in photo_attachments: media.attach_photo(photo['content']) tg_messages = await tgsend(bot.send_media_group, to_tg_chat, media, reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) for tg_message in tg_messages: Message.objects.create( vk_chat=vk_chat_id, vk_id=vk_msg_id, tg_chat=tg_message.chat.id, tg_id=tg_message.message_id ) for attachment in attaches_scheme: if attachment: tg_message = None if attachment['type'] == 'text': await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) tg_message = await tgsend(bot.send_message, to_tg_chat, attachment['content'], parse_mode=ParseMode.HTML, reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) elif attachment['type'] == 'photo' and len(photo_attachments) == 1: await bot.send_chat_action(to_tg_chat, ChatActions.UPLOAD_PHOTO) tg_message = await tgsend(bot.send_photo, to_tg_chat, attachment['content'], reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) elif attachment['type'] == 'document': await bot.send_chat_action(to_tg_chat, ChatActions.UPLOAD_DOCUMENT) tg_message = await tgsend(bot.send_document, to_tg_chat, attachment.get('content', '') or attachment.get('url'), reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) if 'content' in attachment: try: # Иногда тут появляется url, лень проверять откуда растут ноги attachment['content'].close() except: pass try: # Тут вообще не оч понятно, почему не удаляет os.remove(os.path.join(attachment['temp_path'], attachment['file_name'] + attachment['custom_ext'])) except: pass elif attachment['type'] == 'video': await bot.send_chat_action(to_tg_chat, ChatActions.UPLOAD_VIDEO) tg_message = await tgsend(bot.send_video, to_tg_chat, attachment['content'], reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) elif attachment['type'] == 'sticker': await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) tg_message = await tgsend(bot.send_sticker, to_tg_chat, attachment['content'], reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) elif attachment['type'] == 'location': await bot.send_chat_action(to_tg_chat, ChatActions.FIND_LOCATION) tg_message = await tgsend(bot.send_location, to_tg_chat, *attachment['content'], reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) elif attachment['type'] == 'venue': await bot.send_chat_action(to_tg_chat, ChatActions.FIND_LOCATION) tg_message = await tgsend(bot.send_venue, to_tg_chat, *attachment['content'], reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url) elif attachment['type'] == 'audio': await bot.send_chat_action(to_tg_chat, ChatActions.UPLOAD_DOCUMENT) tg_message = await tgsend(bot.send_audio, to_tg_chat, audio=attachment['content'], caption=attachment.get('caption', None), performer=attachment.get('artist', None), title=attachment.get('title', None), reply_to_message_id=main_message, disable_notification=disable_notify, vk_msg_url=vk_msg_url, parse_mode='HTML') elif attachment['type'] == 'audio_message': await bot.send_chat_action(to_tg_chat, ChatActions.RECORD_AUDIO) tg_message = await tgsend(bot.send_voice, to_tg_chat, voice=attachment['content']) # Надо бы делать по-умнее, но очень лень. # if attachment.get('transcript'): # transcript_text = 'Войс: ' + attachment['transcript'] # transcript_message = await tgsend(bot.send_message, to_tg_chat, text=transcript_text, # reply_to_message_id=tg_message.message_id, # parse_mode=ParseMode.HTML) # Message.objects.create( # vk_chat=vk_chat_id, # vk_id=vk_msg_id, # tg_chat=transcript_message.chat.id, # tg_id=transcript_message.message_id # ) if tg_message: Message.objects.create( vk_chat=vk_chat_id, vk_id=vk_msg_id, tg_chat=tg_message.chat.id, tg_id=tg_message.message_id ) if vk_msg.get('fwd_messages'): await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) for fwd_message in vk_msg['fwd_messages']: # Не у всех сообщений есть уникальный id, похоже надо сохранять conversation_message_id в том числе # И делать миграции if fwd_message.get('id'): fwd_msgs_in_db = Message.objects.filter( vk_chat=vk_chat_id, vk_id=fwd_message['id'], tg_chat=to_tg_chat ) else: fwd_msgs_in_db = None if fwd_msgs_in_db: for fwd_msg_in_db in fwd_msgs_in_db: try: await bot.forward_message(to_tg_chat, to_tg_chat, fwd_msg_in_db.tg_id, disable_notification=disable_notify, vk_msg_url=vk_msg_url) except: await process_message(msg, token=token, is_multichat=is_multichat, vk_chat_id=vk_chat_id, user_id=fwd_message['from_id'], forward_settings=forward_settings, vk_msg_id=vk_msg_id, vkchat=vkchat, full_msg={'items': [fwd_message]}, forwarded=True, main_message=header_message.message_id if header_message else None, known_users=known_users, force_disable_notify=disable_notify) else: await process_message(msg, token=token, is_multichat=is_multichat, vk_chat_id=vk_chat_id, user_id=fwd_message['from_id'], forward_settings=forward_settings, vk_msg_id=vk_msg_id, vkchat=vkchat, full_msg={'items': [fwd_message]}, forwarded=True, main_message=header_message.message_id if header_message else None, known_users=known_users, force_disable_notify=disable_notify) async def get_name(identifier, api): if identifier > 0: peer = await api('users.get', user_ids=identifier) first_name = peer[0]['first_name'] last_name = peer[0]['last_name'] or '' else: peer = await api('groups.getById', group_ids=abs(identifier)) first_name = peer[0]['name'] last_name = '' peer[0]['id'] = -peer[0]['id'] return peer[0]['id'], first_name, last_name async def tgsend(method, *args, **kwargs): vk_msg_url = kwargs.pop('vk_msg_url', 0) try: tg_message = await method(*args, **kwargs) return tg_message except RetryAfter as e: await asyncio.sleep(e.timeout) await tgsend(method, *args, **kwargs) except Exception: log.exception(msg='Error in message sending', exc_info=True) await tgsend_error_report(args[0], vk_msg_url) async def tgsend_error_report(chat_id, vk_msg_url): try: text = 'Ошибка отправки сообщения VK → Telegram' if vk_msg_url: text += '\n' + f'Сообщение' await bot.send_message(chat_id, text=text, parse_mode='HTML') except RetryAfter as e: await asyncio.sleep(e.timeout) await tgsend_error_report(chat_id, vk_msg_url) except Exception: log.exception(msg='Error in message sending report', exc_info=True) pass async def process_event(msg): pass async def check_vk_url(url): try: with aiohttp.ClientSession(conn_timeout=5) as session: r = await session.request('GET', url) if r.status == 200: return True return False except: return False def form_audio_title(data: dict, delimer=' '): result = data.get('artist') if result: if 'title' in data: result += delimer + data['title'] else: if 'title' in data: result = data['title'] else: return return result def search_max_vk_photo_size(sizes: list) -> dict: return list(sorted(sizes, key=lambda x: (int(x.get('width', 0)), int(x.get('height', 0))), reverse=True))[0] async def process_attachment(attachment, token=None, vk_msg_url=None): atype = attachment.get('type') if atype == 'photo': photo_url = search_max_vk_photo_size(attachment[atype]['sizes'])['url'] return {'content': photo_url, 'type': 'photo'} elif atype == 'audio_message': voice_url = attachment[atype]['link_ogg'] res = {'content': voice_url, 'type': 'audio_message'} if attachment[atype].get('transcript'): return {'content': f'Войс:{attachment[atype]["transcript"]}', 'type': 'text'} return res elif atype == 'audio': if attachment[atype].get('url') and AUDIO_PROXY_URL: try: with aiohttp.ClientSession() as session: r = await session.request('GET', AUDIO_PROXY_URL, params={'url': urllib.parse.quote(attachment[atype]['url']), 'artist': urllib.parse.quote(attachment[atype].get('artist', '')), 'title': urllib.parse.quote(attachment[atype].get('title', ''))}, headers=CHROME_HEADERS) if r.status != 200: raise Exception audio = await r.read() audio = io.BytesIO(audio) return {'content': audio, 'type': 'audio'} except: pass if AUDIO_ACCESS_URL: if token: try: with aiohttp.ClientSession() as session: r = await session.request('GET', AUDIO_ACCESS_URL.format(token=token, owner_id=attachment[atype]['owner_id'], audio_id=attachment[atype]['id'], access_key=attachment[atype].get( 'access_key', ''))) if r.status != 200: raise Exception audio = await r.read() audio = io.BytesIO(audio) return {'content': audio, 'type': 'audio'} except: pass if AUDIO_URL: try: with aiohttp.ClientSession() as session: r = await session.request('GET', AUDIO_URL.format(owner_id=attachment[atype]['owner_id'], audio_id=attachment[atype]['id'])) if r.status != 200: raise Exception audio = await r.read() audio = io.BytesIO(audio) return {'content': audio, 'type': 'audio'} except: pass if AUDIO_SEARCH_URL: try: search = form_audio_title(attachment[atype]) if not search: raise Exception with aiohttp.ClientSession() as session: r = await session.request('GET', AUDIO_SEARCH_URL, params={'q': urllib.parse.quote(search)}) if r.status != 200: raise Exception audios = await r.json() if audios['success'] and audios['data']: if attachment[atype]['duration']: audio = min(audios['data'], key=lambda x: abs(x['duration'] - attachment[atype]['duration'])) else: audio = audios['data'][0] else: raise Exception with aiohttp.ClientSession() as session: r = await session.request('GET', audio["download"]) if r.status != 200: raise Exception audio = await r.read() audio = io.BytesIO(audio) # search = form_audio_title(attachment[atype], ' - ') # caption = '🔍 {}'.format(quote_html(search)) caption = '🔍' return {'content': audio, 'type': 'audio', 'caption': caption, 'artist': attachment[atype].get('artist', None), 'title': attachment[atype].get('title', None)} except: pass return {'content': f'🎵 {hlink(form_audio_title(attachment[atype]), vk_msg_url)}', 'type': 'text'} elif atype == 'video': title = attachment[atype]['title'] owner_id = attachment[atype]['owner_id'] video_id = attachment[atype]['id'] access_key = attachment[atype].get('access_key') video_url = f'https://vk.com/im?z=video{owner_id}_{video_id}' + f'/{access_key}' if access_key else '' return {'content': f'🎥 Видеозапись {title}', 'type': 'text'} elif atype == 'doc': ext = attachment[atype]['ext'] if ext == 'gif': if 'video' in attachment[atype]['preview']: size = int(attachment[atype]['preview']['video']['file_size']) gif_url = attachment[atype]['preview']['video']['src'] if size > MAX_FILE_SIZE: return {'content': f'GIF', 'type': 'text'} return {'content': gif_url, 'type': 'document'} else: photo_url = attachment[atype]['preview']['photo']['sizes'][-1]['src'] return {'content': photo_url, 'type': 'photo'} # elif 'preview' in attachment[atype] and attachment[atype]['preview'].get('graffiti'): # graffiti_url = attachment[atype]['preview']['photo']['sizes'][-1]['src'] # with aiohttp.ClientSession() as session: # img = await (await session.request('GET', graffiti_url)).read() # imgdata = Image.open(io.BytesIO(img)) # webp = io.BytesIO() # imgdata.save(webp, format='WebP') # file_bytes = webp.getvalue() # return {'content': file_bytes, 'type': 'sticker'} else: size = attachment[atype]['size'] doc_url = attachment[atype]['url'] # + f'&{ext}=1' docname = attachment[atype].get('title', 'Документ') if size > MAX_FILE_SIZE: return {'content': f'📄 {docname}', 'type': 'text'} content = await get_content(doc_url, docname) # supported_exts = ['zip', 'pdf', 'jpg', 'png', 'doc', 'docx'] if 'content' in content: content['type'] = 'document' return content else: return {'content': f'📄 {content["docname"]}', 'type': 'text'} elif atype == 'graffiti': graffiti_url = attachment[atype]['url'] with aiohttp.ClientSession() as session: img = await (await session.request('GET', graffiti_url)).read() imgdata = Image.open(io.BytesIO(img)) webp = io.BytesIO() imgdata.save(webp, format='WebP') file_bytes = webp.getvalue() return {'content': file_bytes, 'type': 'sticker'} elif atype == 'sticker': sticker_url = attachment[atype]['images'][-1]['url'] with aiohttp.ClientSession() as session: img = await (await session.request('GET', sticker_url)).read() imgdata = Image.open(io.BytesIO(img)) webp = io.BytesIO() imgdata.save(webp, format='WebP') file_bytes = webp.getvalue() return {'content': file_bytes, 'type': 'sticker'} elif atype == 'gift': gift_url = attachment[atype][get_max_photo(attachment[atype], 'thumb')] return {'content': f'Подарок', 'type': 'text'} elif atype == 'link': link_url = attachment[atype]['url'] link_name = attachment[atype].get('title', '') if link_name: link_name += '\n' link_name += attachment[atype].get('description', '') if not link_name: if 'button' in attachment[atype] and 'action' in attachment[atype]['button']: link_name = attachment[atype]['button']['action'].get('title', '') if not link_name: link_name = 'Прикрепленная ссылка' elif len(link_name) > 200: link_name = link_name[:200] + '...' photo_content = '' if 'photo' in attachment[atype]: photo_url = attachment[atype]['photo']['sizes'][-1]['url'] photo_name = attachment[atype]['photo'].get('text', '​') if not photo_name: photo_name = '​' photo_content = f'{photo_name}' if photo_name != '​': photo_content += '\n' return {'content': photo_content + f'🔗 {link_name}', 'type': 'text'} elif atype == 'market': market_url = f'https://vk.com/market{attachment[atype]["owner_id"]}_{attachment[atype]["id"]}' photo_content = '' if attachment[atype].get('thumb_photo'): photo_content = f'' title = f'{attachment[atype].get("title", "") or "🛍 Товар"}' description = attachment[atype].get('description', '') if description: description = f'\n{description}' price = '' if attachment[atype].get('price'): price = f'\n{attachment[atype]["price"]["text"]}' return {'content': photo_content + title + description + price + '\n', 'type': 'text'} elif atype == 'market_album': market_album_url = f'https://vk.com/market{attachment[atype]["owner_id"]}?section=album_{attachment[atype]["id"]}' photo_content = '' if attachment[atype].get('photo'): photo_url = attachment[atype]['photo']['sizes'][-1]['url'] photo_content = f'' title = f'{attachment[atype].get("title", "") or "🛒 Подборка Товаров"}' count = f'\nЧисло товаров: {attachment[atype]["count"]}' return {'content': photo_content + title + count + '\n', 'type': 'text'} elif atype == 'wall': owner_id = attachment[atype].get('owner_id', '') or attachment[atype].get('from_id', '') or attachment[ atype].get('to_id', '') post_id = attachment[atype]['id'] # access_key = attachment[atype].get('access_key') wall_url = f'https://vk.com/wall{owner_id}_{post_id}' # + f'_{access_key}' if access_key else '' return {'content': f'📰 Запись на стене', 'type': 'text'} elif atype == 'wall_reply': owner_id = attachment[atype].get('owner_id', '') or attachment[atype].get('from_id', '') or attachment[ atype].get('to_id', '') post_id = attachment[atype]['post_id'] wall_reply_url = f'https://vk.com/wall{owner_id}_{post_id}' reply_text = attachment[atype].get('text', '') if reply_text: reply_text = '\n' + reply_text return {'content': f'💬 Комментарий к записи{reply_text}', 'type': 'text'} async def vk_polling(vkuser: VkUser): log.warning('Starting polling for: id ' + str(vkuser.pk)) while True: try: session = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token)) api = API(session) lp = LongPoll(session, mode=10, version=4) while VkUser.objects.filter(token=vkuser.token, is_polling=True).exists(): data = await lp.wait() log.warning(f'Longpoll id {vkuser.pk}: ' + str(data)) if data['updates']: for update in data['updates']: await process_longpoll_event(api, update) break except VkLongPollError: log.error('Longpoll error! {}'.format(vkuser.pk)) await asyncio.sleep(5) except VkAuthError: log.error('Auth Error! {}'.format(vkuser.pk)) vkuser.is_polling = False vkuser.save() break except TimeoutError: log.warning('Polling timeout') await asyncio.sleep(5) except CancelledError: log.warning('Stopped polling for id: ' + str(vkuser.pk)) break except aiohttp.client_exceptions.ServerDisconnectedError: log.warning('Longpoll server disconnected id: ' + str(vkuser.pk)) except VkAPIError: # Invalid/Inaccessible token pass except Exception: log.exception(msg='Error in longpolling', exc_info=True) await asyncio.sleep(5) def vk_polling_tasks(): tasks = [{'token': vkuser.token, 'task': asyncio.ensure_future(vk_polling(vkuser))} for vkuser in VkUser.objects.filter(token__isnull=False, is_polling=True)] log.warning('Starting Vk polling') return tasks