From d7c1ebc498b160064b876b08a557f53f9edd3a91 Mon Sep 17 00:00:00 2001 From: Kylmakalle Date: Mon, 9 Apr 2018 15:43:57 +0300 Subject: [PATCH] Init --- .gitignore | 5 + bot.py | 102 ++++ data/__init__.py | 0 data/migrations/__init__.py | 0 data/models.py | 105 +++++ manage.py | 24 + settings.py | 17 + telegram.py | 881 +++++++++++++++++++++++++++++++++++ vk_messages.py | 897 ++++++++++++++++++++++++++++++++++++ 9 files changed, 2031 insertions(+) create mode 100644 .gitignore create mode 100644 bot.py create mode 100644 data/__init__.py create mode 100644 data/migrations/__init__.py create mode 100644 data/models.py create mode 100644 manage.py create mode 100644 settings.py create mode 100644 telegram.py create mode 100644 vk_messages.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81c14a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +__pycache__/ +data/migrations/* +!data/migrations/__init__.py +config.py \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..319cf2c --- /dev/null +++ b/bot.py @@ -0,0 +1,102 @@ +import io +import logging +import re +import tempfile +import ujson +from pprint import pprint + +import aiohttp +import django.conf +import wget +from PIL import Image +from aiogram import Bot +from aiogram.dispatcher import Dispatcher +from aiogram.types import ParseMode, MediaGroup, InlineKeyboardMarkup, InlineKeyboardButton, ChatActions +from aiogram.utils.exceptions import * +from aiogram.utils.parts import safe_split_text, split_text, MAX_MESSAGE_LENGTH +from aiovk import TokenSession, API +from aiovk.drivers import HttpDriver +from aiovk.exceptions import * +from aiovk.mixins import LimitRateDriverMixin + +from config import * + +django.conf.ENVIRONMENT_VARIABLE = SETTINGS_VAR +os.environ.setdefault(SETTINGS_VAR, "settings") +# Ensure settings are read +from django.core.wsgi import get_wsgi_application + +application = get_wsgi_application() + +from data.models import * + + +class VkSession(TokenSession): + API_VERSION = API_VERSION + + +class RateLimitedDriver(LimitRateDriverMixin, HttpDriver): + requests_per_period = 1 + period = 0.4 + + +DRIVERS = {} + + +async def get_driver(vk_token=None): + if vk_token: + if vk_token in DRIVERS: + return DRIVERS[vk_token] + else: + new_driver = RateLimitedDriver() + DRIVERS[vk_token] = new_driver + return new_driver + else: + return RateLimitedDriver() + + +async def get_vk_chat(cid): + return VkChat.objects.get_or_create(cid=cid) + + +max_photo_re = re.compile('photo_([0-9]*)') + + +async def get_max_photo(obj, keyword='photo'): + maxarr = [] + for k, v in obj.items(): + m = max_photo_re.match(k) + if m: + maxarr.append(int(m.group(1))) + return keyword + '_' + str(max(maxarr)) + + +async def get_content(url, docname='tgvkbot.document', chrome_headers=True, rewrite_name=False, + custom_ext=''): + try: + with aiohttp.ClientSession(headers={ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'} if chrome_headers else {}) as session: + r = await session.request('GET', url) + direct_url = str(r.url) + tempdir = tempfile.gettempdir() + filename_options = {'out': docname} if rewrite_name else {'default': docname} + if direct_url != url: + r.release() + c = await session.request('GET', direct_url) + file = wget.detect_filename(direct_url, headers=dict(c.headers), **filename_options) + temppath = os.path.join(tempdir, file + custom_ext) + with open(temppath, 'wb') as f: + f.write(await c.read()) + else: + file = wget.detect_filename(direct_url, headers=dict(r.headers), **filename_options) + temppath = os.path.join(tempdir, file + custom_ext) + with open(temppath, 'wb') as f: + f.write(await r.read()) + content = open(temppath, 'rb') + return {'content': content, 'file_name': file, 'custom_ext': custom_ext, 'temp_path': tempdir} + except Exception: + return {'url': url, 'docname': docname} + + +bot = Bot(token=BOT_TOKEN) +dp = Dispatcher(bot) diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/migrations/__init__.py b/data/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/models.py b/data/models.py new file mode 100644 index 0000000..e6670c5 --- /dev/null +++ b/data/models.py @@ -0,0 +1,105 @@ +import asyncio + +from django.db import models +from django.db.models.query import QuerySet + + +class AsyncManager(models.Manager): + """ A model manager which uses the AsyncQuerySet. """ + + async def get_query_set(self): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, AsyncQuerySet(self.model, using=self._db)) + + +class AsyncQuerySet(QuerySet): + """ A queryset which allows DB operations to be pre-triggered so that they run in the + background while the application can continue doing other processing. + """ + + def __init__(self, *args, **kwargs): + super(AsyncQuerySet, self).__init__(*args, **kwargs) + + +class TgUser(models.Model): + objects = AsyncManager() + + # id пользователя на сервере Telegram + uid = models.BigIntegerField(unique=True) + + # имя + first_name = models.CharField(max_length=256) + + # фамилия + last_name = models.CharField( + max_length=256, + null=True, + default=None, + ) + + # username + username = models.CharField( + max_length=256, + null=True, + default=None, + ) + + BLOCKED = -1 + BASE = 0 + + STATUSES = ( + (BLOCKED, 'Заблокирован'), + (BASE, 'Базовый'), + ) + + status = models.IntegerField( + choices=STATUSES, + default=BASE + ) + + +class VkUser(models.Model): + objects = AsyncManager() + + token = models.TextField(unique=True) + is_polling = models.BooleanField(default=False) + owner = models.ForeignKey(TgUser, on_delete=models.CASCADE) + + +class VkChat(models.Model): + objects = AsyncManager() + + cid = models.BigIntegerField(unique=True) + + +class TgChat(models.Model): + objects = AsyncManager() + + cid = models.BigIntegerField(unique=True) + + +class Forward(models.Model): + objects = AsyncManager() + + owner = models.ForeignKey(TgUser, on_delete=models.CASCADE) + tgchat = models.ForeignKey(TgChat, on_delete=models.CASCADE) + vkchat = models.ForeignKey(VkChat, on_delete=models.CASCADE) + + +class Message(models.Model): + objects = AsyncManager() + + vk_chat = models.BigIntegerField() + vk_id = models.BigIntegerField(null=True) + tg_chat = models.BigIntegerField() + tg_id = models.BigIntegerField() + + +class MessageMarkup(models.Model): + objects = AsyncManager() + + message_id = models.BigIntegerField() + + chat_id = models.BigIntegerField() + + buttons = models.TextField(null=True, blank=True) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..9f7393f --- /dev/null +++ b/manage.py @@ -0,0 +1,24 @@ +import sys +from config import * + +if __name__ == "__main__": + os.environ.setdefault(SETTINGS_VAR, "settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + import django.conf + + django.conf.ENVIRONMENT_VARIABLE = SETTINGS_VAR + execute_from_command_line(sys.argv) diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..4e98018 --- /dev/null +++ b/settings.py @@ -0,0 +1,17 @@ +from config import * + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': DATABASE_NAME, + 'USER': DATABASE_USER, + 'PASSWORD': DATABASE_PASSWORD, + 'HOST': DATABASE_HOST, + 'PORT': DATABASE_PORT + } +} +INSTALLED_APPS = ( + 'data', +) diff --git a/telegram.py b/telegram.py new file mode 100644 index 0000000..cda6b9f --- /dev/null +++ b/telegram.py @@ -0,0 +1,881 @@ +from aiogram import types +from aiogram.bot.api import FILE_URL +from aiogram.dispatcher.webhook import get_new_configured_app +from aiohttp import web + +from bot import * +from config import * +from vk_messages import vk_polling_tasks, vk_polling + +log = logging.getLogger('telegram') + +oauth_link = re.compile('https://oauth\.vk\.com/blank\.html#access_token=([a-z0-9]*)&expires_in=[0-9]*&user_id=[0-9]*') + + +async def get_pages_switcher(markup, page, pages): + if page != 0: + leftbutton = InlineKeyboardButton('◀', callback_data='page{}'.format(page - 1)) # callback + else: + leftbutton = InlineKeyboardButton('Поиск 🔍', callback_data='search') + if page + 1 < len(pages): + rightbutton = InlineKeyboardButton('▶', callback_data='page{}'.format(page + 1)) + else: + rightbutton = None + + if rightbutton: + markup.row(leftbutton, rightbutton) + else: + markup.row(leftbutton) + + +async def logged(uid, reply_to_message_id=None, to_chat=None): + vk_user = VkUser.objects.filter(owner__uid=uid).first() + if vk_user: + return True + else: + await bot.send_message(to_chat or uid, 'Вход не выполнен! /start для входа', + reply_to_message_id=reply_to_message_id) + return False + + +async def update_user_info(from_user: types.User): + return TgUser.objects.update_or_create(uid=from_user.id, + defaults={'first_name': from_user.first_name, + 'last_name': from_user.last_name, + 'username': from_user.username + }) + + +async def update_chat_info(from_chat: types.Chat): + if from_chat.type == 'private': + return None, False + return TgChat.objects.update_or_create(cid=from_chat.id) + + +async def is_forwarding(text): + if not text: + return False, None + if text == '!': + return True, None + if text.startswith('!'): + return True, text[1:] + return False, text + + +async def is_bot_in_iterator(msg: types.Message): + iterator = msg.new_chat_members or [msg.left_chat_member] or [] + me = await bot.me + for i in iterator: + if me.id == i.id: + return True + return False + + +async def vk_sender(token, tg_message, **kwargs): + session = VkSession(access_token=token, driver=await get_driver(token)) + try: + api = API(session) + vk_msg_id = await api('messages.send', **kwargs) + except VkAuthError: + vk_user = VkUser.objects.filter(token=token).first() + if vk_user: + vk_user.delete() + await bot.send_message(tg_message.chat.id, 'Вход не выполнен! /start для входа', + reply_to_message_id=tg_message.message_id) + return + except VkAPIError: + log.exception(msg='Error in vk sender', exc_info=True) + return None + except Exception: + log.exception(msg='Error in vk sender', exc_info=True) + return None + Message.objects.create( + vk_chat=kwargs['peer_id'], + vk_id=vk_msg_id, + tg_chat=tg_message.chat.id, + tg_id=tg_message.message_id + ) + return vk_msg_id + + +async def generate_send_options(msg, forward=None, forward_messages_exists=False, message=None): + message_options = dict() + if forward: + if msg.reply_to_message is not None: + message_in_db = Message.objects.filter(tg_chat=msg.chat.id, + tg_id=msg.reply_to_message.message_id).first() + if message_in_db and message_in_db.vk_id: + message_options['forward_messages'] = message_in_db.vk_id + message_options['peer_id'] = forward.vkchat.cid + + elif msg.reply_to_message is not None: + message_in_db = Message.objects.filter(tg_chat=msg.chat.id, tg_id=msg.reply_to_message.message_id).first() + if not message_in_db: + await msg.reply('Не знаю в какой чат ответить, нет информации в базе данных.') + return message_options + if forward_messages_exists and message_in_db.vk_id: + message_options['forward_messages'] = message_in_db.vk_id + message_options['peer_id'] = message_in_db.vk_chat + else: + await msg.reply('Не понимаю что делать. Нужна помощь? Используй команду /help') + return message_options + + if message: + message_options['message'] = message + return message_options + + +async def send_vk_action(token, peer_id, action='typing'): + vksession = VkSession(access_token=token, driver=await get_driver(token)) + api = API(vksession) + return await api('messages.setActivity', peer_id=peer_id, activity=action) + + +async def upload_attachment(msg, vk_user, file_id, peer_id, attachment_type, upload_field, upload_method, + on_server_field='file', save_method='', upload_type=None, default_name='tgvkbot.document', + title='tgvkbot.document', rewrite_name=False, custom_ext=''): + try: + file_info = await bot.get_file(file_id) + path = file_info['file_path'] + if msg.content_type == 'audio': + if not custom_ext and '.' in path and path.split('.')[-1] == 'mp3': + custom_ext = '.aac' + except NetworkError: + await msg.reply('Файл слишком большой, максимально допустимый размер 20мб!', parse_mode=ParseMode.HTML) + return + + url = FILE_URL.format(token=bot._BaseBot__token, path=path) + await send_vk_action(vk_user.token, peer_id) + content = await get_content(url, default_name, chrome_headers=False, rewrite_name=rewrite_name, + custom_ext=custom_ext) + filename = (content.get('file_name', '') + content.get('custom_ext', '')) or None + if 'content' in content: + vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token)) + api = API(vksession) + upload_options = {} + if attachment_type != 'photo' and upload_type: + upload_options['type'] = upload_type + if msg.content_type == 'sticker': + webp = Image.open(content['content']).convert('RGBA') + png = io.BytesIO() + webp.save(png, format='png') + content['content'] = png.getvalue() + if attachment_type == 'video': + upload_options['is_private'] = 1 + upload_server = await api(upload_method, **upload_options) + with aiohttp.ClientSession() as session: + data = aiohttp.FormData() + field_data = {} + if filename: + field_data['filename'] = filename + data.add_field(upload_field, content['content'], content_type='multipart/form-data', **field_data) + async with session.post(upload_server['upload_url'], data=data) as upload: + file_on_server = ujson.loads(await upload.text()) + if msg.content_type != 'sticker': + content['content'].close() + os.remove(os.path.join(content['temp_path'], content['file_name'] + content['custom_ext'])) + if attachment_type == 'photo': + save_options = {'server': file_on_server['server'], on_server_field: file_on_server[on_server_field], + 'hash': file_on_server['hash']} + elif attachment_type == 'video': + return f'{attachment_type}{upload_server["owner_id"]}_{upload_server["video_id"]}_{upload_server["access_key"]}' + else: + if 'file' not in file_on_server: + await msg.reply('Ошибка Не удалось загрузить файл. Файл не должен быть исполняемым.', + parse_mode=ParseMode.HTML) + return + save_options = dict({'file': file_on_server['file']}) + save_options['title'] = title + attachment = await api(save_method, **save_options) + return f'{attachment_type}{attachment[0]["owner_id"]}_{attachment[0]["id"]}' + + +async def get_dialogs(token, exclude=list()): + session = VkSession(access_token=token, driver=await get_driver(token)) + api = API(session) + dialogs = await api('messages.getDialogs', count=200) + order = [] + users_ids = [] + group_ids = [] + for chat in dialogs.get('items'): + chat = chat.get('message', '') + if chat: + if 'chat_id' in chat: + if 2000000000 + chat['chat_id'] not in exclude: + chat['title'] = chat['title'] + order.append({'title': chat['title'], 'id': 2000000000 + chat['chat_id']}) + elif chat['user_id'] > 0: + if chat['user_id'] not in exclude: + order.append({'title': 'Диалог ' + str(chat['user_id']), 'id': chat['user_id']}) + users_ids.append(chat['user_id']) + elif chat['user_id'] < 0: + if chat['user_id'] not in exclude: + order.append({'title': 'Диалог ' + str(chat['user_id']), 'id': chat['user_id']}) + group_ids.append(chat['user_id']) + + if users_ids: + users = await api('users.get', user_ids=', '.join(str(x) for x in users_ids)) + else: + users = [] + + if group_ids: + groups = await api('groups.getById', group_ids=', '.join(str(abs(x)) for x in group_ids)) + else: + groups = [] + + for output in order: + if output['id'] > 0: + u = next((i for i in users if i['id'] == output['id']), None) + if u: + output['title'] = f'{u["first_name"]} {u["last_name"]}' + else: + g = next((i for i in groups if -i['id'] == output['id']), None) + if g: + output['title'] = g["name"] + + for button in range(len(order)): + order[button] = InlineKeyboardButton(order[button]['title'], callback_data=f'chat{order[button]["id"]}') + + rows = [order[x:x + 2] for x in range(0, len(order), 2)] + pages = [rows[x:x + 4] for x in range(0, len(rows), 4)] + + return pages + + +async def search_dialogs(msg: types.Message, user=None): + if not user: + user, created = await update_user_info(msg.from_user) + vkuser = VkUser.objects.filter(owner=user).first() + vksession = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token)) + api = API(vksession) + markup = InlineKeyboardMarkup(row_width=1) + await bot.send_chat_action(msg.chat.id, 'typing') + result = await api('messages.searchDialogs', q=msg.text, limit=10) + for chat in result: + title = None + data = None + if chat['type'] == 'profile': + title = f'{chat["first_name"]} {chat["last_name"]}' + data = f'chat{chat["id"]}' + elif chat['type'] == 'chat': + title = chat['title'] + data = f'chat{2000000000 + chat["id"]}' + elif chat['type'] == 'page': + title = await chat['name'] + data = f'chat{-chat["id"]}' + if title and data: + markup.add(InlineKeyboardButton(text=title, callback_data=data)) + markup.add(InlineKeyboardButton('Поиск 🔍', callback_data='search')) + if markup.inline_keyboard: + text = f'Результат поиска по {msg.text}' + else: + text = f'Результат поиска по {msg.text}' + await bot.send_message(msg.chat.id, text, reply_markup=markup, parse_mode=ParseMode.HTML) + + +@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('logged')) +async def check_logged(call: types.CallbackQuery): + vkuser = VkUser.objects.filter(owner__uid=call.from_user.id).count() + if vkuser: + await handle_join(call.message, edit=True, chat_id=call.message.chat.id, message_id=call.message.message_id, + exclude=True) + else: + await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота', + show_alert=True) + + +@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('page')) +async def page_switcher(call: types.CallbackQuery): + # user, created = await update_user_info(call.from_user) + # tgchat, tgchat_created = await update_chat_info(call.message.chat) + page = int(call.data.split('page')[-1]) + message_markup = MessageMarkup.objects.filter( + chat_id=call.message.chat.id, + message_id=call.message.message_id, + ).first() + if message_markup: + pages = ujson.loads(message_markup.buttons) + markup = InlineKeyboardMarkup() + for row in pages[page]: + markup.row(*[InlineKeyboardButton(**button) for button in row]) + await get_pages_switcher(markup, page, pages) + await bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=markup) + await bot.answer_callback_query(call.id) + else: + await bot.answer_callback_query(call.id, 'Нет данных в Базе Данных', show_alert=True) + + +async def get_dialog_info(api, vk_chat_id, name_case='nom'): + title = '' + photo = '' + dialog_type = '' + if vk_chat_id >= 2000000000: + dialog_info = await api('messages.getChat', chat_id=vk_chat_id - 2000000000) + title = await dialog_info['title'] + photo = dialog_info[await get_max_photo(dialog_info)] + dialog_type = 'chat' + elif vk_chat_id > 0: + dialog_info = await api('users.get', user_ids=vk_chat_id, fields='photo_max', name_case=name_case) + first_name = dialog_info[0]['first_name'] + last_name = dialog_info[0]['last_name'] or '' + title = first_name + ' ' + last_name + photo = dialog_info[0]['photo_max'] + dialog_type = 'user' + elif vk_chat_id < 0: + dialog_info = await api('groups.getById', group_ids=abs(vk_chat_id)) + title = dialog_info[0]['name'] + photo = dialog_info[0][await get_max_photo(dialog_info[0])] + dialog_type = 'group' + + return {'title': title, 'photo': photo, 'type': dialog_type} + + +@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('ping')) +async def ping_button(call: types.CallbackQuery): + tg_chat_id = int(call.data.split('ping')[-1]) + try: + await bot.send_message(tg_chat_id, f'Ping!', + parse_mode=ParseMode.HTML) + await bot.answer_callback_query(call.id, 'Ping!') + except BadRequest: + await bot.answer_callback_query(call.id, 'Нет доступа к чату, бот кикнут или чат удалён!', show_alert=True) + + +@dp.callback_query_handler( + func=lambda call: call and call.message and call.data and call.data.startswith('deleteforward')) +async def delete_forward(call: types.CallbackQuery): + forward_id = int(call.data.split('deleteforward')[-1]) + forward_in_db = Forward.objects.filter(id=forward_id).first() + if forward_in_db: + forward_in_db.delete() + + markup = InlineKeyboardMarkup() + + message_markup = MessageMarkup.objects.filter( + message_id=call.message.message_id, + chat_id=call.message.chat.id + ).first() + + buttons = ujson.loads(message_markup.buttons) + + for row in buttons: + if row[1]['callback_data'] == call.data: + buttons.remove(row) + else: + markup.row(*[InlineKeyboardButton(**button) for button in row]) + if message_markup: + if buttons: + message_markup.buttons = ujson.dumps(buttons) + message_markup.save() + await bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=markup) + else: + await bot.edit_message_text( + 'У Вас нет связанных чатов. Чтобы привязать чат, добавьте бота в группу, а если бот уже добавлен - используйте команду /dialogs', + call.message.chat.id, call.message.message_id) + await bot.answer_callback_query(call.id, 'Успешно удалено!') + else: + await bot.edit_message_text('Что-то пошло не так, нет информации в Базе Данных', + message_id=call.message.message_id, chat_id=call.message.chat.id, reply_markup=None) + await bot.answer_callback_query(call.id, 'Ошибка!') + + +@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('setinfo')) +async def set_info(call: types.CallbackQuery): + user, created = await update_user_info(call.from_user) + tgchat, tgchat_created = await update_chat_info(call.message.chat) + vk_chat_id = int(call.data.split('setinfo')[-1]) + vkuser = VkUser.objects.filter(owner=user).first() + if vkuser: + ME = await bot.me + can_edit = False + if not call.message.chat.all_members_are_administrators and ( + (await bot.get_chat_member(call.message.chat.id, ME.id)).status == 'administrator'): + can_edit = True + if not can_edit: + admins = await bot.get_chat_administrators(call.message.chat.id) + for admin in admins: + if admin.user.id == ME.id and admin.can_change_info: + can_edit = True + break + if can_edit: + vksession = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token)) + api = API(vksession) + dialog_info = await get_dialog_info(api, vk_chat_id, name_case='nom') + if dialog_info.get('title', ''): + await bot.set_chat_title(call.message.chat.id, dialog_info['title']) + if dialog_info.get('photo', ''): + content = await get_content(dialog_info['photo']) + await bot.set_chat_photo(call.message.chat.id, content['content']) + content['content'].close() + os.remove(os.path.join(content['temp_path'], content['file_name'] + content['custom_ext'])) + + if dialog_info['type'] == 'user': + dialog_info = await get_dialog_info(api, vk_chat_id, name_case='ins') + text = f'Чат успешно привязан к диалогу c {dialog_info["title"]}' + elif dialog_info['type'] == 'group': + text = f'Чат успешно привязан к диалогу с сообществом {dialog_info["title"]}' + else: + text = f'Чат успешно привязан к диалогу {dialog_info["title"]}' + + await bot.edit_message_text(text, call.message.chat.id, call.message.message_id, parse_mode=ParseMode.HTML) + await bot.answer_callback_query(call.id) + else: + await bot.answer_callback_query(call.id, + 'Недостаточно прав для редактирования информации о группе или бот не администратор!', + show_alert=True) + + else: + await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота', + show_alert=True) + + +@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('chat')) +async def choose_chat(call: types.CallbackQuery): + user, created = await update_user_info(call.from_user) + tgchat, tgchat_created = await update_chat_info(call.message.chat) + vk_chat_id = int(call.data.split('chat')[-1]) + vkuser = VkUser.objects.filter(owner=user).first() + if vkuser: + if call.message.chat.type == 'private': + vksession = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token)) + api = API(vksession) + dialog_info = await get_dialog_info(api, vk_chat_id, name_case='gen') + markup = types.ForceReply(selective=False) + if dialog_info['type'] == 'user': + text = f'Сообщение для {dialog_info["title"]}' + elif dialog_info['type'] == 'group': + text = f'Сообщение сообществу {dialog_info["title"]}' + else: + text = f'Сообщение в диалог {dialog_info["title"]}' + + tg_message = await bot.send_message(call.message.chat.id, text, reply_markup=markup, + parse_mode=ParseMode.HTML) + Message.objects.create( + tg_chat=tg_message.chat.id, + tg_id=tg_message.message_id, + vk_chat=vk_chat_id + ) + else: + forward = Forward.objects.filter(tgchat=tgchat).first() + vkchat = (await get_vk_chat(int(vk_chat_id)))[0] + if forward: + forward.vkchat = vkchat + forward.save() + else: + Forward.objects.create( + tgchat=tgchat, + vkchat=vkchat, + owner=user + ) + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton('Установить аватар и название', callback_data=f'setinfo{vkchat.cid}')) + await bot.edit_message_text( + 'Чат успешно привязан. Я могу автоматически изменить название и установить аватар, сделай бота администратором и убедись в наличии прав на редактирование информации группы', + call.message.chat.id, call.message.message_id, reply_markup=markup) + await bot.answer_callback_query(call.id) + else: + await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота', + show_alert=True) + + +@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data == 'search') +async def search_callback(call: types.CallbackQuery): + vkuser = VkUser.objects.filter(owner__uid=call.from_user.id).count() + if vkuser: + markup = types.ForceReply(selective=False) + await bot.send_message(call.message.chat.id, 'Поиск беседы 🔍', parse_mode=ParseMode.HTML, + reply_markup=markup) + await bot.answer_callback_query(call.id, 'Поиск беседы 🔍') + else: + await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота', + show_alert=True) + + +@dp.message_handler(commands=['start']) +async def send_welcome(msg: types.Message): + user, created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + if not tgchat: + existing_vkuser = VkUser.objects.filter(owner=user).count() + if not existing_vkuser: + link = 'https://oauth.vk.com/authorize?client_id={}&' \ + 'display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=friends,messages,offline,docs,photos,video,stories' \ + '&response_type=token&v={}'.format(VK_APP_ID, API_VERSION) + mark = InlineKeyboardMarkup() + login = InlineKeyboardButton('ВХОД', url=link) + mark.add(login) + await msg.reply('Привет, этот бот поможет тебе общаться ВКонтакте, войди по кнопке ниже' + ' и отправь мне то, что получишь в адресной строке.', + reply_markup=mark) + else: + await msg.reply('Вход уже выполнен!\n/logout для выхода.') + else: + markup = InlineKeyboardMarkup() + me = await bot.me + markup.add(InlineKeyboardButton('Перейти в бота', url=f'https://t.me/{me.username}?start=login')) + await msg.reply('Залогиниться можно только через личный чат с ботом', reply_markup=markup) + + +@dp.message_handler(commands=['stop']) +async def stop_command(msg: types.Message): + user, created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + + existing_vkuser = VkUser.objects.filter(owner=user).first() + if not existing_vkuser: + await msg.reply('Вход не выполнен! Используй команду /start для входа') + else: + polling = next((task for task in TASKS if task['token'] == existing_vkuser.token), None) + if polling: + polling['task'].cancel() + driver = DRIVERS.get(existing_vkuser.token, '') + if driver: + driver.close() + existing_vkuser.delete() + await msg.reply('Успешный выход!') + + +@dp.message_handler(commands=['dialogs', 'd']) +async def dialogs_command(msg: types.Message): + if msg.chat.type == 'private': + await handle_join(msg, text='Выберите диалог для быстрого ответа') + else: + await handle_join(msg, exclude=True) + + +@dp.message_handler(commands=['read', 'r']) +async def read_command(msg: types.Message): + user, created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + if await logged(msg.from_user.id, msg.message_id, msg.chat.id): + vk_user = VkUser.objects.filter(owner=user).first() + if msg.chat.type == 'private': + if msg.reply_to_message: + message_in_db = Message.objects.filter(tg_chat=msg.chat.id, + tg_id=msg.reply_to_message.message_id).first() + if message_in_db: + vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token)) + api = API(vksession) + await api('messages.markAsRead', peer_id=message_in_db.vk_chat) + await bot.send_message(msg.chat.id, 'Диалог прочитан', parse_mode=ParseMode.HTML) + else: + await msg.reply('Не знаю какой чат прочесть, нет информации в базе данных') + else: + forward = Forward.objects.filter(tgchat=tgchat).first() + if forward: + vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token)) + api = API(vksession) + await api('messages.markAsRead', peer_id=forward.vkchat.cid) + await bot.send_message(msg.chat.id, 'Диалог прочитан', parse_mode=ParseMode.HTML) + else: + await msg.reply('Этот чат не привязан к диалогу ВКонтакте, для привязки используй команду /dialogs') + + +@dp.message_handler(commands=['search', 's']) +async def search_command(msg: types.Message): + user, created = await update_user_info(msg.from_user) + vkuser = VkUser.objects.filter(owner=user).count() + if vkuser: + markup = types.ForceReply(selective=False) + await bot.send_message(msg.chat.id, 'Поиск беседы 🔍', parse_mode=ParseMode.HTML, + reply_markup=markup) + else: + await bot.answer_callback_query(msg, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота', + show_alert=True) + + +@dp.message_handler(commands=['chat', 'chats']) +async def chat_command(msg: types.Message): + user, created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + forwards = Forward.objects.filter(owner=user) + if await logged(msg.from_user.id, msg.message_id, msg.chat.id): + if forwards: + vk_user = VkUser.objects.filter(owner=user).first() + vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token)) + api = API(vksession) + markup = InlineKeyboardMarkup() + for forward in forwards: + chat = await get_dialog_info(api, forward.vkchat.cid) + markup.row(*[InlineKeyboardButton(chat['title'], callback_data=f'ping{forward.tgchat.cid}'), + InlineKeyboardButton('❌', callback_data=f'deleteforward{forward.pk}')]) + msg_with_markup = await bot.send_message(msg.chat.id, + 'Список привязанных диалогов\nНажав на имя диалога, бот пинганёт Вас в соответствующем чате Telegram.\nНажав на "❌", привязка чата будет удалена и все сообщение из диалога ВКонтакте будут попадать напрямую к боту', + reply_markup=markup) + for row in markup.inline_keyboard: + for button in range(len(row)): + row[button] = row[button].to_python() + MessageMarkup.objects.create( + message_id=msg_with_markup.message_id, + chat_id=msg_with_markup.chat.id, + buttons=ujson.dumps(markup.inline_keyboard) + ) + else: + await bot.send_message(msg.chat.id, + 'У Вас нет связанных чатов. Чтобы привязать чат, добавьте бота в группу, а если бот уже добавлен - используйте команду /dialogs') + # TODO: /leave chat + # TODO: manage forwards + + +@dp.message_handler(commands=['help']) +async def help_command(msg: types.Message): + user, created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + HELP_MESSAGE = '/start - Логин в Вконтакте\n' \ + '/dialogs /d - Список диалогов\n' \ + '/read /r - Прочесть диалог ВКонтакте\n' \ + '/search /s - Поиск по диалогам\n' \ + '/chat - Список связанных чатов с диалогами ВКонтакте, привязать чат к диалогу можно добавив бота в группу\n' \ + '/stop - Выход из ВКонтакте' \ + '/help - Помощь' + + await bot.send_message(msg.chat.id, HELP_MESSAGE, parse_mode=ParseMode.HTML) + + +@dp.message_handler(content_types=['text']) +async def handle_text(msg: types.Message): + user, created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + if msg.chat.type == 'private': + m = oauth_link.search(msg.text) + if m: + token = m.group(1) + if not VkUser.objects.filter(token=token).exists(): + try: + session = VkSession(access_token=token, driver=await get_driver(token)) + api = API(session) + vkuserinfo = await api('account.getProfileInfo', name_case='gen') + vkuser, vkuser_created = VkUser.objects.update_or_create( + defaults={'token': token, 'is_polling': True}, owner=user) + existing_polling = next((task for task in TASKS if task['token'] == vkuser.token), None) + if existing_polling: + existing_polling['task'].cancel() + driver = DRIVERS.get(vkuser.token, '') + if driver: + driver.close() + TASKS.append({'token': vkuser.token, 'task': asyncio.ensure_future(vk_polling(vkuser))}) + await msg.reply( + 'Вход выполнен в аккаунт {} {}!\n[Использование](https://asergey.me/tgvkbot/usage/)'.format( + vkuserinfo['first_name'], vkuserinfo.get('last_name', '')), parse_mode='Markdown') + except VkAuthError: + await msg.reply('Неверная ссылка, попробуйте ещё раз!') + else: + await msg.reply('Вход уже выполнен!\n/stop для выхода.') + return + if await logged(msg.from_user.id, msg.message_id, msg.chat.id): + if msg.reply_to_message and msg.reply_to_message.text == 'Поиск беседы 🔍': + if msg.chat.type == 'private' or not Message.objects.filter(tg_id=msg.reply_to_message.message_id, + tg_chat=msg.reply_to_message.chat.id).exists(): + await search_dialogs(msg, user) + return + vk_user = VkUser.objects.filter(owner=user).first() + forward = Forward.objects.filter(tgchat=tgchat).first() + forward_messages_exists, message = await is_forwarding(msg.text) + message_options = await generate_send_options(msg, forward, forward_messages_exists, message) + if message_options != {}: + vk_message = await vk_sender(vk_user.token, msg, **message_options) + if not vk_message: + await msg.reply('Произошла ошибка при отправке', parse_mode=ParseMode.HTML) + + +@dp.message_handler(content_types=['contact']) +async def handle_contact(msg: types.Message): + new_text = msg.contact.first_name + if msg.contact.last_name: + new_text += ' ' + msg.contact.last_name + new_text += '\n' + new_text += msg.contact.phone_number + msg.text = new_text + await handle_text(msg) + + +@dp.message_handler(content_types=['photo']) +async def handle_photo(msg: types.Message): + user, user_created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + if await logged(msg.from_user.id, msg.message_id, msg.chat.id): + vk_user = VkUser.objects.filter(owner=user).first() + forward = Forward.objects.filter(tgchat=tgchat).first() + forward_messages_exists, message = await is_forwarding(msg.caption) + message_options = await generate_send_options(msg, forward, forward_messages_exists, message) + file_id = msg.photo[-1].file_id + if message_options: + message_options['attachment'] = await upload_attachment(msg, vk_user, file_id, message_options['peer_id'], + attachment_type='photo', + upload_field='photo', + upload_method='photos.getMessagesUploadServer', + on_server_field='photo', + save_method='photos.saveMessagesPhoto') + if message_options['attachment']: + vk_message = await vk_sender(vk_user.token, msg, **message_options) + if not vk_message: + await msg.reply('Произошла ошибка при отправке', parse_mode=ParseMode.HTML) + else: + msg.reply('Ошибка при загрузке файла. Сообщение не отправлено!', parse_mode=ParseMode.HTML) + + +@dp.message_handler(content_types=['document', 'voice', 'audio', 'sticker']) +async def handle_documents(msg: types.Message): + user, user_created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + if await logged(msg.from_user.id, msg.message_id, msg.chat.id): + vk_user = VkUser.objects.filter(owner=user).first() + if tgchat: + forward = Forward.objects.filter(tgchat=tgchat).first() + else: + forward = None + forward_messages_exists, message = await is_forwarding(msg.caption) + message_options = await generate_send_options(msg, forward, forward_messages_exists, message) + file_id = getattr(msg, msg.content_type).file_id + if message_options: + upload_attachment_options = { + 'attachment_type': 'doc', + 'upload_field': 'file', + 'upload_method': 'docs.getUploadServer', + 'on_server_field': 'file', + 'save_method': 'docs.save', + } + if hasattr(getattr(msg, msg.content_type), 'file_name') and getattr(msg, msg.content_type).file_name: + upload_attachment_options['title'] = getattr(msg, msg.content_type).file_name + + if msg.content_type == 'voice': + upload_attachment_options['upload_type'] = 'audio_message' + + if msg.content_type == 'sticker': + upload_attachment_options['upload_type'] = 'graffiti' + upload_attachment_options['rewrite_name'] = True + upload_attachment_options['default_name'] = 'graffiti.png' + + if msg.content_type == 'audio': + audioname = '' + if msg.audio.performer and msg.audio.title: + audioname += msg.audio.performer + ' - ' + msg.audio.title + elif msg.audio.performer: + audioname += msg.audio.performer + elif msg.audio.title: + audioname += msg.audio.title + else: + audioname = f'tgvkbot_audio_{file_id}' + upload_attachment_options['title'] = audioname + + message_options['attachment'] = await upload_attachment(msg, vk_user, file_id, message_options['peer_id'], + **upload_attachment_options) + if message_options['attachment']: + vk_message = await vk_sender(vk_user.token, msg, **message_options) + if not vk_message: + await msg.reply('Произошла ошибка при отправке', parse_mode=ParseMode.HTML) + else: + await msg.reply('Ошибка при загрузке файла. Сообщение не отправлено!', parse_mode=ParseMode.HTML) + + +@dp.message_handler(content_types=['video', 'video_note']) +async def handle_videos(msg: types.Message): + user, user_created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + if await logged(msg.from_user.id, msg.message_id, msg.chat.id): + vk_user = VkUser.objects.filter(owner=user).first() + if tgchat: + forward = Forward.objects.filter(tgchat=tgchat).first() + else: + forward = None + forward_messages_exists, message = await is_forwarding(msg.caption) + message_options = await generate_send_options(msg, forward, forward_messages_exists, message) + file_id = getattr(msg, msg.content_type).file_id + if message_options: + upload_attachment_options = { + 'attachment_type': 'video', + 'upload_field': 'video_file', + 'upload_method': 'video.save', + } + if hasattr(getattr(msg, msg.content_type), 'file_name') and getattr(msg, msg.content_type).file_name: + upload_attachment_options['title'] = getattr(msg, msg.content_type).file_name + + message_options['attachment'] = await upload_attachment(msg, vk_user, file_id, message_options['peer_id'], + **upload_attachment_options) + if message_options['attachment']: + vk_message = await vk_sender(vk_user.token, msg, **message_options) + if not vk_message: + await msg.reply('Произошла ошибка при отправке', parse_mode=ParseMode.HTML) + else: + await msg.reply('Ошибка при загрузке файла. Сообщение не отправлено!', parse_mode=ParseMode.HTML) + + +@dp.message_handler(content_types=['new_chat_members'], func=is_bot_in_iterator) +async def handle_join(msg: types.Message, edit=False, chat_id=None, message_id=None, text='', exclude=False): + user, user_created = await update_user_info(msg.from_user) + tgchat, tgchat_created = await update_chat_info(msg.chat) + forward = Forward.objects.filter(tgchat=tgchat).first() + await bot.send_chat_action(msg.chat.id, 'typing') + vk_user = VkUser.objects.filter(owner=user).first() + pages = None + if vk_user: + if forward: + text = text or 'Этот чат уже привязан к диалогу ВКонтакте, Вы можете выбрать новый диалог' + else: + text = text or 'Выберите диалог ВКонтакте к которому будет привязан этот чат' + markup = InlineKeyboardMarkup() + excluded_ids = [] + if exclude: + excluded_ids = [forward.vkchat.cid for forward in Forward.objects.filter(owner=user)] + pages = await get_dialogs(vk_user.token, excluded_ids) + if pages: + for buttons_row in pages[0]: + markup.row(*buttons_row) + await get_pages_switcher(markup, 0, pages) + else: + me = await bot.me + text = 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота' + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton('ВХОД', url=f'https//t.me/{me.username}?start=login'), + InlineKeyboardButton('✅ Я залогинился', callback_data=f'logged-{msg.from_user.id}')) + if edit: + msg_with_markup = await bot.edit_message_text(text=text, chat_id=chat_id, message_id=message_id, + reply_markup=markup, parse_mode=ParseMode.HTML) + else: + msg_with_markup = await bot.send_message(msg.chat.id, text=text, reply_markup=markup, parse_mode=ParseMode.HTML) + if pages: + for page in pages: + for row in page: + for button in range(len(row)): + row[button] = row[button].to_python() + + MessageMarkup.objects.create( + message_id=msg_with_markup.message_id, + chat_id=msg_with_markup.chat.id, + buttons=ujson.dumps(pages) + ) + + +@dp.message_handler(content_types=types.ContentType.ANY, func=lambda msg: msg.group_chat_created is True) +async def handle_new_group(msg: types.Message): + await handle_join(msg) + + +@dp.message_handler(func=lambda msg: msg.migrate_to_chat_id is not None) +async def handle_chat_migration(msg: types.Message): + forwards = Forward.objects.filter(tgchat__cid=msg.migrate_from_chat_id) + for forward in forwards: + forward.tgchat.cid = msg.migrate_to_chat_id + forward.tgchat.save() + + +async def on_startup(app): + webhook = await bot.get_webhook_info() + + # If URL is bad + if webhook.url != WEBHOOK_URL: + # If URL doesnt match current - remove webhook + if not webhook.url: + await bot.delete_webhook() + + # Set new URL for webhook + await bot.set_webhook(WEBHOOK_URL) + + +if __name__ == '__main__': + TASKS = vk_polling_tasks() + asyncio.gather(*[task['task'] for task in TASKS]) + app = get_new_configured_app(dispatcher=dp, path=WEBHOOK_URL_PATH) + app.on_startup.append(on_startup) + + web.run_app(app, host=WEBAPP_HOST, port=WEBAPP_PORT) diff --git a/vk_messages.py b/vk_messages.py new file mode 100644 index 0000000..51b7379 --- /dev/null +++ b/vk_messages.py @@ -0,0 +1,897 @@ +from concurrent.futures._base import CancelledError + +from aiovk.longpoll import LongPoll + +from bot import * + +log = logging.getLogger('vk_messages') + + +################### Честно взято по лицензии 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['body'] + 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): + 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 {} + + 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])) + if full_msg.get('items'): + for vk_msg in full_msg['items']: + disable_notify = bool(vk_msg.get('push_settings', False)) + attaches_scheme = [] + if vk_msg.get('attachments'): + attaches_scheme = [await process_attachment(attachment) for attachment in + vk_msg['attachments']] + if vk_msg.get('geo'): + location = vk_msg['geo']['coordinates'].split(' ') + 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' + if 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} @ {vk_msg["title"]}' + '\n' + to_tg_chat = vkuser.owner.uid + + body_parts = [] + body = vk_msg.get('body', '') + 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) + + if body_parts: + for body_part in range(len(body_parts)): + await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) + 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) + 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): + await bot.send_chat_action(to_tg_chat, ChatActions.TYPING) + 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) + 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) + 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: + 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) + 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) + 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) + if 'content' in attachment: + attachment['content'].close() + os.remove(os.path.join(attachment['temp_path'], + attachment['file_name'] + attachment['custom_ext'])) + 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) + 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) + 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) + 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) + + 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) + fwd_ptr = tg_message = await bot.send_message(vkuser.owner.uid, header + 'Пересланные сообщения', + parse_mode=ParseMode.HTML, + reply_to_message_id=main_message, + disable_notification=disable_notify) + 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 fwd_message in vk_msg['fwd_messages']: + await process_message(msg, token=token, is_multichat=is_multichat, vk_chat_id=vk_chat_id, + user_id=fwd_message['user_id'], + forward_settings=forward_settings, vk_msg_id=vk_msg_id, vkchat=vkchat, + full_msg={'items': [fwd_message]}, forwarded=True, + main_message=fwd_ptr.message_id, known_users=known_users) + + +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): + try: + tg_message = await method(*args, **kwargs) + return tg_message + except RetryAfter as e: + asyncio.sleep(e.timeout) + tgsend(method, *args, **kwargs) + except Exception: + log.exception(msg='Error in message sending', exc_info=True) + + +async def process_event(msg): + pass + + +async def process_attachment(attachment): + atype = attachment.get('type') + if atype == 'photo': + photo_url = attachment[atype][await get_max_photo(attachment[atype])] + return {'content': photo_url, 'type': 'photo'} + + elif atype == 'audio': + pass + + 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/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': + size = attachment[atype]['preview']['video']['file_size'] + gif_url = attachment[atype]['url'] + '&mp4=1' + if size > MAX_FILE_SIZE: + return {'content': f'GIF', 'type': 'text'} + return {'content': gif_url, 'type': 'document'} + 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 == 'sticker': + sticker_url = attachment[atype][await get_max_photo(attachment[atype])] + 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][await 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'][await get_max_photo(attachment[atype]['photo'])] + 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'][await get_max_photo(attachment[atype])] + 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): + while True: + try: + session = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token)) + session.API_VERSION = API_VERSION + log.warning('Starting polling for: id ' + str(vkuser.pk)) + 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.debug('Longpoll: ' + 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)) + asyncio.sleep(5) + except VkAuthError: + log.error('Auth Error! {}'.format(vkuser.pk)) + vkuser.is_polling = False + vkuser.save() + break + except CancelledError: + log.warning('Stopped polling for: id ' + str(vkuser.pk)) + break + except Exception: + log.exception(msg='Error in longpolling', exc_info=True) + 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