tgvkbot/vk_messages.py
Sergey 55d0b8ae1e Фикс размера картинок
Больше не надеемся на позицию, а считаем ширину и высоту самостоятельно
2020-12-03 19:27:12 +03:00

1188 lines
50 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('<br>', '\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'<b>{name}</b>' + '\n'
elif not forwarded:
header = ''
to_tg_chat = forward_setting.tgchat.cid
else:
if forwarded or not is_multichat:
header = f'<b>{name}</b>' + '\n'
elif is_multichat:
header = f'<b>{name} @ {quote_html(full_chat["title"])}</b>' + '\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 = '<i>Войс:</i> ' + 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 = '<i>Ошибка отправки сообщения VK → Telegram</i>'
if vk_msg_url:
text += '\n' + f'<a href="{vk_msg_url}">Сообщение</a>'
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'<i>Войс:</i>{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 = '<i>🔍 {}</i>'.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'<i>🎥 Видеозапись</i> <a href="{video_url}">{title}</a>', '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'<a href="{gif_url}">GIF</a>', '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'<a href="{doc_url}">📄 {docname}</a>', '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'<a href="{doc_url}">📄 {content["docname"]}</a>', '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'<a href="{gift_url}">Подарок</a>', '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', '&#8203;')
if not photo_name:
photo_name = '&#8203;'
photo_content = f'<a href="{photo_url}">{photo_name}</a>'
if photo_name != '&#8203;':
photo_content += '\n'
return {'content': photo_content + f'<a href="{link_url}">🔗 {link_name}</a>', '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'<a href="{attachment[atype]["thumb_photo"]}">&#8203;</a>'
title = f'<a href="{market_url}">{attachment[atype].get("title", "") or "🛍 Товар"}</a>'
description = attachment[atype].get('description', '')
if description:
description = f'\n<i>{description}</i>'
price = ''
if attachment[atype].get('price'):
price = f'\n<b>{attachment[atype]["price"]["text"]}</b>'
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'<a href="{photo_url or ""}">&#8203;</a>'
title = f'<a href="{market_album_url}">{attachment[atype].get("title", "") or "🛒 Подборка Товаров"}</a>'
count = f'\n<i>Число товаров:</i> {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'<a href="{wall_url}">📰 Запись на стене</a>', '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'<a href="{wall_reply_url}">💬 Комментарий к записи</a>{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