This commit is contained in:
Kylmakalle 2018-04-09 15:43:57 +03:00
commit d7c1ebc498
9 changed files with 2031 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea/
__pycache__/
data/migrations/*
!data/migrations/__init__.py
config.py

102
bot.py Normal file
View File

@ -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)

0
data/__init__.py Normal file
View File

View File

105
data/models.py Normal file
View File

@ -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)

24
manage.py Normal file
View File

@ -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)

17
settings.py Normal file
View File

@ -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',
)

881
telegram.py Normal file
View File

@ -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('Файл слишком большой, максимально допустимый размер <b>20мб!</b>', 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('<b>Ошибка</b> Не удалось загрузить файл. Файл не должен быть исполняемым.',
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'<b>Результат поиска по</b> <i>{msg.text}</i>'
else:
text = f'<b>Результат поиска по</b> <i>{msg.text}</i>'
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'<a href="tg://user?id={call.from_user.id}">Ping!</a>',
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('<b>Что-то пошло не так, нет информации в Базе Данных</b>',
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 <i>{dialog_info["title"]}</i>'
elif dialog_info['type'] == 'group':
text = f'Чат успешно привязан к диалогу с сообществом <i>{dialog_info["title"]}</i>'
else:
text = f'Чат успешно привязан к диалогу <i>{dialog_info["title"]}</i>'
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'Сообщение для <i>{dialog_info["title"]}</i>'
elif dialog_info['type'] == 'group':
text = f'Сообщение сообществу <i>{dialog_info["title"]}</i>'
else:
text = f'Сообщение в диалог <i>{dialog_info["title"]}</i>'
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, '<b>Поиск беседы 🔍</b>', 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, '<i>Диалог прочитан</i>', 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, '<i>Диалог прочитан</i>', 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, '<b>Поиск беседы 🔍</b>', 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('<b>Произошла ошибка при отправке</b>', 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('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
else:
msg.reply('<b>Ошибка при загрузке файла. Сообщение не отправлено!</b>', 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('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
else:
await msg.reply('<b>Ошибка при загрузке файла. Сообщение не отправлено!</b>', 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('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
else:
await msg.reply('<b>Ошибка при загрузке файла. Сообщение не отправлено!</b>', 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 '<i>Этот чат уже привязан к диалогу ВКонтакте, Вы можете выбрать новый диалог</i>'
else:
text = text or '<i>Выберите диалог ВКонтакте к которому будет привязан этот чат</i>'
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 = '<i>Вход не выполнен! Сперва нужно выполнить вход в ВК через бота</i>'
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)

897
vk_messages.py Normal file
View File

@ -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("&quot;", "\"") # 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):
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'<b>{name}</b>' + '\n'
if 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} @ {vk_msg["title"]}</b>' + '\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 + '<i>Пересланные сообщения</i>',
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'<i>🎥 Видеозапись</i> <a href="{video_url}">{title}</a>', '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'<a href="{gif_url}">GIF</a>', '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'<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 == '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'<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'][await get_max_photo(attachment[atype]['photo'])]
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'][await get_max_photo(attachment[atype])]
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):
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