Лонг-поллинг ТГ
Актуальный API

Протестированы:
ВК → ТГ
- Текст
- Фото, альбомы (+ фото документом)
- Документы
- Стикеры (анимированные только превью)
- Войсы + их текстовый транскрипт из ВК
- Групповые сообщения

ТГ → ВК
- Текст
- Фото, (+ фото документом)
- Документы
- Стикеры (анимированные только малюсеньким превью)
- Войсы
- Групповые сообщения
This commit is contained in:
Sergey 2020-10-04 17:24:44 +03:00
parent e144fe9bb7
commit 6af43f76b0
8 changed files with 101 additions and 137 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ config.py
*.pem
env_file
openssl_config
venv/

View File

@ -1,23 +0,0 @@
upstream tgbot {
ip_hash;
server tgbot:7777;
}
server {
listen 8443 ssl;
ssl_certificate /src/webhook_cert.pem;
ssl_certificate_key /src/webhook_pkey.pem;
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_pass http://tgbot/;
}
}

View File

@ -1,27 +1,5 @@
import os
def get_external_host():
import urllib.request
host = urllib.request.urlopen('https://api.ipify.org').read().decode('utf8')
return host
HOST = ''
WEBHOOK_HOST = os.environ.get('WEBHOOK_HOST', HOST or get_external_host())
WEBHOOK_PORT = os.environ.get('WEBHOOK_PORT', 8443)
WEBHOOK_URL_PATH = os.environ.get('WEBHOOK_URL_PATH', '/tgwebhook')
WEBHOOK_URL = "https://{WEBHOOK_HOST}:{WEBHOOK_PORT}{WEBHOOK_URL_PATH}".format(WEBHOOK_HOST=WEBHOOK_HOST,
WEBHOOK_PORT=WEBHOOK_PORT,
WEBHOOK_URL_PATH=WEBHOOK_URL_PATH)
WEBHOOK_SSL_CERT = './webhook_cert.pem'
WEBAPP_HOST = os.environ.get('WEBAPP_HOST', '0.0.0.0')
WEBAPP_PORT = os.environ.get('WEBAPP_PORT', 7777)
DATABASE_USER = os.environ.get('POSTGRES_USER', 'postgres')
DATABASE_PASSWORD = os.environ.get('POSTGRES_PASSWORD', 'postgres')
DATABASE_HOST = os.environ.get('DATABASE_HOST', 'db')
@ -47,7 +25,7 @@ SETTINGS_VAR = os.environ.get('SETTINGS_VAR', 'DJANGO_TGVKBOT_SETTINGS_MODULE')
MAX_FILE_SIZE = os.environ.get('MAX_FILE_SIZE', 52428800)
API_VERSION = os.environ.get('API_VERSION', '5.71')
API_VERSION = os.environ.get('API_VERSION', '5.124')
AUDIO_API_VERSION = os.environ.get('API_VERSION', '5.78')
# https://www.miniwebtool.com/django-secret-key-generator/

View File

@ -23,27 +23,10 @@ services:
restart: always
networks:
- db_nw
- web_nw
depends_on:
- db
nginx:
image: "nginx:1.13.5"
ports:
- "8443:8443"
restart: always
volumes:
- .:/src
- ./conf.d:/etc/nginx/conf.d
env_file:
- env_file
networks:
- web_nw
depends_on:
- tgbot
networks:
db_nw:
driver: bridge
web_nw:
driver: bridge
volumes:
dbdata:

View File

@ -5,6 +5,5 @@ sudo usermod -aG docker $(whoami) && \
sudo curl -L https://github.com/docker/compose/releases/download/1.20.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose && \
sudo chmod +x /usr/local/bin/docker-compose && \
python3 set_env.py && \
python3 obtaincert.py && \
sudo docker-compose build && \
sudo docker-compose up -d
sudo docker-compose up -d

View File

@ -1,35 +0,0 @@
from subprocess import call
from config import WEBHOOK_HOST
OPENSSL_CONFIG_TEMPLATE = """
prompt = no
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
C = RU
ST = Saint-Petersburg
L = Saint-Petersburg
O = tgvkbot
OU = tgvkbot
CN = %(domain)s
emailAddress = tgvkbot@gmail.com
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = %(domain)s
DNS.2 = *.%(domain)s
"""
call([
'openssl', 'genrsa', '-out', 'webhook_pkey.pem', '2048'
])
config = open('openssl_config', 'w')
config.write(OPENSSL_CONFIG_TEMPLATE % {'domain': WEBHOOK_HOST})
config.close()
call([
'openssl', 'req', '-new', '-x509', '-days', '3650', '-key', 'webhook_pkey.pem', '-out', 'webhook_cert.pem',
'-config', 'openssl_config'
])

View File

@ -1,7 +1,6 @@
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 aiogram.utils import executor
from aiohttp.client_exceptions import ContentTypeError
from bot import *
@ -73,8 +72,16 @@ async def is_bot_in_iterator(msg: types.Message):
return False
import secrets
def generate_random_id():
return secrets.randbelow(2_147_483_647)
async def vk_sender(token, tg_message, **kwargs):
session = VkSession(access_token=token, driver=await get_driver(token))
kwargs['random_id'] = generate_random_id()
try:
api = API(session)
vk_msg_id = await api('messages.send', **kwargs)
@ -199,10 +206,12 @@ async def upload_attachment(msg, vk_user, file_id, peer_id, attachment_type, upl
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"]}'
return f'{attachment_type}{attachment[attachment["type"]]["owner_id"]}_{attachment[attachment["type"]]["id"]}'
async def get_dialogs(token, exclude=list()):
async def get_dialogs(token, exclude=None):
if not exclude:
exclude = []
session = VkSession(access_token=token, driver=await get_driver(token))
api = API(session)
dialogs = await api('messages.getDialogs', count=200)
@ -491,6 +500,7 @@ async def choose_chat(call: types.CallbackQuery):
tg_id=tg_message.message_id,
vk_chat=vk_chat_id
)
await bot.answer_callback_query(call.id)
else:
forward = Forward.objects.filter(tgchat=tgchat).first()
vkchat = (await get_vk_chat(int(vk_chat_id)))[0]
@ -787,6 +797,8 @@ async def handle_documents(msg: types.Message):
upload_attachment_options['upload_type'] = 'audio_message'
if msg.content_type == 'sticker':
if msg.sticker.to_python()['is_animated']:
file_id = msg.sticker.thumb.file_id
upload_attachment_options['upload_type'] = 'graffiti'
upload_attachment_options['rewrite_name'] = True
upload_attachment_options['default_name'] = 'graffiti.png'
@ -914,15 +926,8 @@ async def handle_chat_migration(msg: types.Message):
forward.tgchat.save()
async def on_startup(app):
# Set new URL for webhook
await bot.set_webhook(WEBHOOK_URL, certificate=open(WEBHOOK_SSL_CERT, 'rb'))
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)
executor.start_polling(dp)

View File

@ -1,10 +1,10 @@
import urllib
from concurrent.futures._base import CancelledError, TimeoutError
from aiogram.utils.markdown import quote_html, hlink
from aiovk.longpoll import LongPoll
from bot import *
from aiogram.utils.markdown import quote_html, hlink
import urllib
log = logging.getLogger('vk_messages')
inline_link_re = re.compile('\[([a-zA-Z0-9_]*)\|(.*?)\]', re.MULTILINE)
@ -102,7 +102,7 @@ class MessageEventData(object):
data.user_id = int(obj['user_id'])
data.true_user_id = int(obj['user_id'])
data.full_text = obj['body']
data.full_text = obj['text']
data.time = int(obj['date'])
data.is_out = obj.get('out', False)
data.is_forwarded = False
@ -525,7 +525,7 @@ async def process_longpoll_event(api, new_event):
async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, user_id=None, forward_settings=None,
vkchat=None,
full_msg=None, forwarded=False, vk_msg_id=None, main_message=None, known_users=None,
force_disable_notify=None):
force_disable_notify=None, full_chat=None):
token = token or msg.api._session.access_token
is_multichat = is_multichat or msg.is_multichat
vk_msg_id = vk_msg_id or msg.msg_id
@ -553,6 +553,10 @@ async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, u
forward_setting = forward_settings or Forward.objects.filter(owner=vkuser.owner, vkchat=vkchat).first()
full_msg = full_msg or await msg.api('messages.getById', message_ids=', '.join(str(x) for x in [vk_msg_id]))
# Узнаем title чата
if is_multichat:
full_chat = await msg.api('messages.getChat', chat_id=vk_chat_id - 2000000000)
if full_msg.get('items'):
for vk_msg in full_msg['items']:
disable_notify = force_disable_notify or bool(vk_msg.get('push_settings', False))
@ -579,11 +583,11 @@ async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, u
if forwarded or not is_multichat:
header = f'<b>{name}</b>' + '\n'
elif is_multichat:
header = f'<b>{name} @ {quote_html(vk_msg["title"])}</b>' + '\n'
header = f'<b>{name} @ {quote_html(full_chat["title"])}</b>' + '\n'
to_tg_chat = vkuser.owner.uid
body_parts = []
body = quote_html(vk_msg.get('body', ''))
body = quote_html(vk_msg.get('text', ''))
if body:
if (len(header) + len(body)) > MAX_MESSAGE_LENGTH:
@ -602,6 +606,14 @@ async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, u
body += first_text_attach['content']
attaches_scheme.remove(first_text_attach)
first_voice_attach = next(
(attach for attach in attaches_scheme if attach and attach['type'] == 'audio_message'),
None)
if first_voice_attach:
# Будем отправлять только те войсы, в которых завершен транскрипт сообщений
if first_voice_attach.get('transcript_state') != 'done':
return
if body_parts:
for body_part in range(len(body_parts)):
m = inline_link_re.finditer(body_parts[body_part])
@ -687,9 +699,17 @@ async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, u
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']))
try:
# Иногда тут появляется url, лень проверять откуда растут ноги
attachment['content'].close()
except:
pass
try:
# Тут вообще не оч понятно, почему не удаляет
os.remove(os.path.join(attachment['temp_path'],
attachment['file_name'] + attachment['custom_ext']))
except:
pass
elif attachment['type'] == 'video':
await bot.send_chat_action(to_tg_chat, ChatActions.UPLOAD_VIDEO)
tg_message = await tgsend(bot.send_video, to_tg_chat, attachment['content'],
@ -714,6 +734,22 @@ async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, u
title=attachment.get('title', None),
reply_to_message_id=main_message, disable_notification=disable_notify,
parse_mode='HTML')
elif attachment['type'] == 'audio_message':
await bot.send_chat_action(to_tg_chat, ChatActions.RECORD_AUDIO)
tg_message = await tgsend(bot.send_voice, to_tg_chat, voice=attachment['content'])
if attachment.get('transcript'):
transcript_text = '<i>Войс:</i> ' + attachment['transcript']
transcript_message = await tgsend(bot.send_message, to_tg_chat, text=transcript_text,
reply_to_message_id=tg_message.message_id,
parse_mode=ParseMode.HTML)
Message.objects.create(
vk_chat=vk_chat_id,
vk_id=vk_msg_id,
tg_chat=transcript_message.chat.id,
tg_id=transcript_message.message_id
)
Message.objects.create(
vk_chat=vk_chat_id,
vk_id=vk_msg_id,
@ -724,7 +760,7 @@ async def process_message(msg, token=None, is_multichat=None, vk_chat_id=None, u
await bot.send_chat_action(to_tg_chat, ChatActions.TYPING)
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'],
user_id=fwd_message['from_id'],
forward_settings=forward_settings, vk_msg_id=vk_msg_id, vkchat=vkchat,
full_msg={'items': [fwd_message]}, forwarded=True,
main_message=header_message.message_id if header_message else None,
@ -749,7 +785,7 @@ async def tgsend(method, *args, **kwargs):
tg_message = await method(*args, **kwargs)
return tg_message
except RetryAfter as e:
asyncio.sleep(e.timeout)
await asyncio.sleep(e.timeout)
await tgsend(method, *args, **kwargs)
except Exception:
log.exception(msg='Error in message sending', exc_info=True)
@ -786,9 +822,19 @@ def form_audio_title(data: dict, delimer=' '):
async def process_attachment(attachment, token=None):
atype = attachment.get('type')
if atype == 'photo':
photo_url = attachment[atype][await get_max_photo(attachment[atype])]
photo_url = attachment[atype]['sizes'][-1]['url']
return {'content': photo_url, 'type': 'photo'}
elif atype == 'audio_message':
voice_url = attachment[atype]['link_ogg']
res = {'content': voice_url, 'type': 'audio_message'}
if attachment[atype].get('transcript'):
res['transcript'] = attachment[atype]['transcript']
if attachment[atype].get('transcript_state'):
res['transcript_state'] = attachment[atype]['transcript_state']
return res
elif atype == 'audio':
if attachment[atype].get('url') and AUDIO_PROXY_URL:
try:
@ -884,15 +930,15 @@ async def process_attachment(attachment, token=None):
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'}
# 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'
@ -907,8 +953,18 @@ async def process_attachment(attachment, token=None):
else:
return {'content': f'<a href="{doc_url}">📄 {content["docname"]}</a>', 'type': 'text'}
elif atype == 'graffiti':
graffiti_url = attachment[atype]['url']
with aiohttp.ClientSession() as session:
img = await (await session.request('GET', graffiti_url)).read()
imgdata = Image.open(io.BytesIO(img))
webp = io.BytesIO()
imgdata.save(webp, format='WebP')
file_bytes = webp.getvalue()
return {'content': file_bytes, 'type': 'sticker'}
elif atype == 'sticker':
sticker_url = attachment[atype][await get_max_photo(attachment[atype])]
sticker_url = attachment[atype]['images'][-1]['url']
with aiohttp.ClientSession() as session:
img = await (await session.request('GET', sticker_url)).read()
imgdata = Image.open(io.BytesIO(img))
@ -1004,7 +1060,7 @@ async def vk_polling(vkuser: VkUser):
break
except VkLongPollError:
log.error('Longpoll error! {}'.format(vkuser.pk))
asyncio.sleep(5)
await asyncio.sleep(5)
except VkAuthError:
log.error('Auth Error! {}'.format(vkuser.pk))
vkuser.is_polling = False
@ -1012,7 +1068,7 @@ async def vk_polling(vkuser: VkUser):
break
except TimeoutError:
log.warning('Polling timeout')
asyncio.sleep(5)
await asyncio.sleep(5)
except CancelledError:
log.warning('Stopped polling for id: ' + str(vkuser.pk))
break
@ -1023,7 +1079,7 @@ async def vk_polling(vkuser: VkUser):
pass
except Exception:
log.exception(msg='Error in longpolling', exc_info=True)
asyncio.sleep(5)
await asyncio.sleep(5)
def vk_polling_tasks():