diff --git a/README.md b/README.md index b8d1d97..11931b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # tgvkbot Бот позволяет получать и отправлять сообщения VK находясь в Telegram + [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) @@ -12,4 +13,4 @@ ## Stay Tuned! -_Полноценные комментарии к коду будут чуть позже_ +_Полноценные комментарии к коду будут чуть позже_ \ No newline at end of file diff --git a/bot.py b/bot.py index 57cd988..52ea4d3 100644 --- a/bot.py +++ b/bot.py @@ -9,14 +9,13 @@ import traceback import ujson import vk import wget +import time from PIL import Image from telebot import types from credentials import token, vk_app_id from vk_messages import VkMessage, VkPolling -logging.basicConfig(format='%(levelname)-8s [%(asctime)s] %(message)s', level=logging.WARNING, filename='vk.log') - vk_threads = {} vk_dialogs = {} @@ -28,6 +27,7 @@ vk_tokens = redis.from_url(os.environ.get("REDIS_URL")) currentchat = {} bot = telebot.AsyncTeleBot(token) +bot.remove_webhook() 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' \ @@ -67,6 +67,8 @@ def replace_shields(text): def request_user_dialogs(session, userid): order = [] users_ids = [] + group_ids = [] + positive_group_ids = [] dialogs = vk.API(session).messages.getDialogs(count=200) for chat in dialogs[1:]: if 'chat_id' in chat: @@ -77,14 +79,28 @@ def request_user_dialogs(session, userid): elif chat['uid'] > 0: order.append({'title': None, 'id': chat['uid']}) users_ids.append(chat['uid']) + elif chat['uid'] < 0: + order.append({'title': None, 'id': chat['uid']}) + group_ids.append(chat['uid']) + + for g in group_ids: + positive_group_ids.append(str(g)[1:]) + users = vk.API(session).users.get(user_ids=users_ids, fields=['first_name', 'last_name', 'uid']) + groups = vk.API(session).groups.getById(group_ids=positive_group_ids, fields=[]) for output in order: if output['title'] == ' ... ' or not output['title']: - for x in users: - if x['uid'] == output['id']: - current_user = x - break - output['title'] = '{} {}'.format(current_user['first_name'], current_user['last_name']) + if output['id'] > 0: + for x in users: + if x['uid'] == output['id']: + output['title'] = '{} {}'.format(x['first_name'], x['last_name']) + break + + else: + for f in groups: + if str(f['gid']) == str(output['id'])[1:]: + output['title'] = '{}'.format(f['name']) + break for button in range(len(order)): order[button] = types.InlineKeyboardButton(order[button]['title'], callback_data=str(order[button]['id'])) rows = [order[x:x + 2] for x in range(0, len(order), 2)] @@ -136,8 +152,13 @@ def search_users(message, text): def callback_buttons(call): if call.message: if 'page' in call.data: + try: + create_markup(call.message, call.from_user.id, int(call.data.split('page')[1]), True) + except: + session = VkMessage(vk_tokens.get(str(call.from_user.id))).session + request_user_dialogs(session, call.from_user.id) + create_markup(call.message, call.from_user.id, int(call.data.split('page')[1]), True) bot.answer_callback_query(call.id).wait() - create_markup(call.message, call.from_user.id, int(call.data.split('page')[1]), True) elif 'search' in call.data: markup = types.ForceReply(selective=False) bot.answer_callback_query(call.id, 'Поиск беседы 🔍').wait() @@ -154,24 +175,31 @@ def callback_buttons(call): 'Вы в беседе {}'.format(chat['title']), parse_mode='HTML').wait() currentchat[str(call.from_user.id)] = call.data - elif call.data.isdigit(): + elif call.data.lstrip('-').isdigit(): session = VkMessage(vk_tokens.get(str(call.from_user.id))).session - user = vk.API(session).users.get(user_ids=call.data, fields=[])[0] + if '-' in call.data: + user = vk.API(session).groups.getById(group_id=call.data.lstrip('-'), fields=[])[0] + user = {'first_name': user['name'], 'last_name': ''} + else: + user = vk.API(session).users.get(user_ids=call.data, fields=[])[0] bot.answer_callback_query(call.id, 'Вы в чате с {} {}'.format(user['first_name'], user['last_name'])).wait() bot.send_message(call.from_user.id, 'Вы в чате с {} {}'.format(user['first_name'], user['last_name']), parse_mode='HTML').wait() - currentchat[str(call.from_user.id)] = call.data + currentchat[str(call.from_user.id)] = {'title': user['first_name'] + ' ' + user['last_name'], + 'id': call.data} def create_thread(uid, vk_token): a = VkPolling() - t = threading.Thread(name='vk' + str(uid), target=a.run, args=(VkMessage(vk_token), bot, uid,)) + longpoller = VkMessage(vk_token) + t = threading.Thread(name='vk' + str(uid), target=a.run, args=(longpoller, bot, uid,)) t.setDaemon(True) t.start() vk_threads[str(uid)] = a vk_tokens.set(str(uid), vk_token) + vk.API(longpoller.session).account.setOffline() def check_thread(uid): @@ -181,11 +209,29 @@ def check_thread(uid): return True -# Creating VkPolling threads and dialogs info after bot reboot using existing tokens -for uid in vk_tokens.scan_iter(): - if check_thread(uid.decode("utf-8")): - create_thread(uid.decode("utf-8"), vk_tokens.get(uid)) - request_user_dialogs(VkMessage(vk_tokens.get(uid.decode("utf-8"))).session, uid.decode("utf-8")) +# Creating VkPolling threads and dialogs info after bot's reboot/exception using existing tokens +def thread_supervisor(): + while True: + for uid in vk_tokens.scan_iter(): + tries = 0 + while check_thread(uid.decode("utf-8")): + if tries < 6: + try: + create_thread(uid.decode("utf-8"), vk_tokens.get(uid)) + except: + tries = tries + 1 + else: + mark = types.InlineKeyboardMarkup() + login = types.InlineKeyboardButton('ВХОД', url=link) + mark.add(login) + bot.send_message(uid.decode("utf-8"), 'Непредвиденная ошибка, требуется повторный логин ВК!', + parse_mode='HTML', reply_markup=mark) + time.sleep(60) + + +supervisor = threading.Thread(name='supervisor', target=thread_supervisor) +supervisor.setDaemon(True) +supervisor.start() def stop_thread(message): @@ -222,21 +268,17 @@ def info_extractor(info): def chat_command(message): if logged(message): if str(message.from_user.id) in currentchat: - if 'group' in currentchat[str(message.from_user.id)]: - session = VkMessage(vk_tokens.get(str(message.from_user.id))).session - chat = vk.API(session).messages.getChat( - chat_id=currentchat[str(message.from_user.id)].split('group')[1], - fields=[]) + if 'group' in currentchat[str(message.from_user.id)]['id']: + chat = currentchat[str(message.from_user.id)] if chat['title'].replace('\\', ''): chat['title'] = chat['title'].replace('\\', '') bot.send_message(message.from_user.id, 'Вы в беседе {}'.format(chat['title']), parse_mode='HTML').wait() else: - session = VkMessage(vk_tokens.get(str(message.from_user.id))).session - user = vk.API(session).users.get(user_ids=currentchat[str(message.from_user.id)], fields=[])[0] + chat = currentchat[str(message.from_user.id)] bot.send_message(message.from_user.id, - 'Вы в чате с {} {}'.format(user['first_name'], user['last_name']), + 'Вы в чате с {}'.format(chat['title']), parse_mode='HTML').wait() else: bot.send_message(message.from_user.id, @@ -352,12 +394,12 @@ def vk_sender(message, method): elif str(message.from_user.id) in currentchat: info = [] - if 'group' in currentchat[str(message.from_user.id)]: + if 'group' in currentchat[str(message.from_user.id)]['id']: info.append('0') - info.append(currentchat[str(message.from_user.id)].split('group')[1]) + info.append(currentchat[str(message.from_user.id)]['id'].split('group')[1]) info.append('1') else: - info.append(currentchat[str(message.from_user.id)]) + info.append(currentchat[str(message.from_user.id)]['id']) info.append('0') info.append('0') form_request(message, method, info) @@ -381,9 +423,9 @@ def send_text(message, userid, group, forward_messages=None): def send_doc(message, userid, group, forward_messages=None): filetype = message.content_type session = VkMessage(vk_tokens.get(str(message.from_user.id))).session - file = wget.download( - FILE_URL.format(token, bot.get_file(getattr(message, filetype).file_id).wait().file_path)) - if filetype == 'document': + if filetype == 'document' and 'video' not in message.document.mime_type: + file = wget.download( + FILE_URL.format(token, bot.get_file(getattr(message, filetype).file_id).wait().file_path)) openedfile = open(file, 'rb') files = {'file': openedfile} fileonserver = ujson.loads(requests.post(vk.API(session).docs.getUploadServer()['upload_url'], @@ -395,6 +437,8 @@ def send_doc(message, userid, group, forward_messages=None): os.remove(file) elif filetype == 'voice': + file = wget.download( + FILE_URL.format(token, bot.get_file(getattr(message, filetype).file_id).wait().file_path)) openedfile = open(file, 'rb') files = {'file': openedfile} fileonserver = ujson.loads( @@ -405,7 +449,13 @@ def send_doc(message, userid, group, forward_messages=None): openedfile.close() os.remove(file) + elif filetype == 'document' and 'video' in message.document.mime_type: + vk_sender(message, send_video) + return + else: # filetype == 'audio': + file = wget.download( + FILE_URL.format(token, bot.get_file(getattr(message, filetype).file_id).wait().file_path)) newfile = file.split('.')[0] + '.aac' os.rename(file, newfile) openedfile = open(newfile, 'rb') @@ -605,9 +655,11 @@ def reply_text(message): code = extract_unique_code(m.group(0)) if check_thread(message.from_user.id): try: - verifycode(code) + user = verifycode(code) create_thread(message.from_user.id, code) - bot.send_message(message.from_user.id, 'Вход выполнен!').wait() + bot.send_message(message.from_user.id, + 'Вход выполнен в аккаунт {} {}!'.format(user['first_name'], user['last_name'])).wait() + bot.send_message(message.from_user.id, '[Использование](https://asergey.me/tgvkbot/usage/)', parse_mode='Markdown').wait() except: @@ -624,7 +676,24 @@ def reply_text(message): except Exception: bot.reply_to(message, 'Произошла неизвестная ошибка при отправке', parse_mode='Markdown').wait() - print('Error: {}'.format(traceback.format_exc())) - bot.polling(none_stop=True) +"""class WebhookServer(object): + # index равнозначно /, т.к. отсутствию части после ip-адреса (грубо говоря) + @cherrypy.expose + def index(self): + length = int(cherrypy.request.headers['content-length']) + json_string = cherrypy.request.body.read(length).decode("utf-8") + update = telebot.types.Update.de_json(json_string) + bot.process_new_updates([update]) + return '' + + +if __name__ == '__main__': + logging.basicConfig(format='%(levelname)-8s [%(asctime)s] %(message)s', level=logging.WARNING, filename='vk.log') + bot.remove_webhook() + bot.set_webhook('https://{}/{}/'.format(bot_url, token)) + cherrypy.config.update( + {'server.socket_host': '127.0.0.1', 'server.socket_port': local_port, 'engine.autoreload.on': False, + 'log.screen': False}) + cherrypy.quickstart(WebhookServer(), '/', {'/': {}})""" diff --git a/vk_messages.py b/vk_messages.py index d06c498..5a9a165 100644 --- a/vk_messages.py +++ b/vk_messages.py @@ -1,13 +1,18 @@ +import logging import os import redis import requests import time import vk +import ujson import wget + +logging.basicConfig(format='%(levelname)-8s [%(asctime)s] %(message)s', level=logging.WARNING, filename='vk.log') vk_tokens = redis.from_url(os.environ.get("REDIS_URL")) + class VkPolling: def __init__(self): self._running = True @@ -17,14 +22,15 @@ class VkPolling: def run(self, vk_user, bot, chat_id): while self._running: - updates = [] + timeout = 30 try: updates = vk_user.get_new_messages() - except requests.exceptions.ReadTimeout as e: - print('Error: {}'.format(e)) - if updates: - handle_updates(vk_user, bot, chat_id, updates) - for i in range(60): + if updates: + handle_updates(vk_user, bot, chat_id, updates) + except requests.exceptions.ReadTimeout: + logging.warning('Retrying VK Polling.') + timeout = 0 + for i in range(timeout): if self._running: time.sleep(0.1) else: @@ -32,7 +38,12 @@ class VkPolling: def handle_messages(m, vk_user, bot, chat_id, mainmessage=None): - user = vk.API(vk_user.session).users.get(user_ids=m["uid"], fields=[])[0] + if m['uid'] > 0: + user = vk.API(vk_user.session).users.get(user_ids=m["uid"], fields=[])[0] + else: + group = vk.API(vk_user.session).groups.getById(group_ids=str(m['uid'])[1:])[0] + user = {'first_name': group['name'], 'last_name': None} + if 'body' in m and not 'attachment' in m and not 'geo' in m and not 'fwd_messages' in m: data = add_user_info(m, user["first_name"], user["last_name"])[:-1] + add_reply_info(m) bot.send_message(chat_id, data, parse_mode='HTML', disable_web_page_preview=False, @@ -240,15 +251,29 @@ def add_reply_info(m): def add_user_info(m, first_name, last_name): if 'body' in m and m['body']: - if 'chat_id' in m: - return '{} {} @ {}:\n{}\n'.format(first_name, last_name, m['title'], m['body'].replace('
', '\n')) + if last_name: + if 'chat_id' in m: + return '{} {} @ {}:\n{}\n'.format(first_name, last_name, m['title'], + m['body'].replace('
', '\n')) + else: + return '{} {}:\n{}\n'.format(first_name, last_name, m['body'].replace('
', '\n')) else: - return '{} {}:\n{}\n'.format(first_name, last_name, m['body'].replace('
', '\n')) + if 'chat_id' in m: + return '{} @ {}:\n{}\n'.format(first_name, m['title'], + m['body'].replace('
', '\n')) + else: + return '{}:\n{}\n'.format(first_name, m['body'].replace('
', '\n')) else: - if 'chat_id' in m: - return '{} {} @ {}:\n'.format(first_name, last_name, m['title']) + if last_name: + if 'chat_id' in m: + return '{} {} @ {}:\n'.format(first_name, last_name, m['title']) + else: + return '{} {}:\n'.format(first_name, last_name) else: - return '{} {}:\n'.format(first_name, last_name) + if 'chat_id' in m: + return '{} @ {}:\n'.format(first_name, m['title']) + else: + return '{}:\n'.format(first_name) def check_notification(value): @@ -277,7 +302,17 @@ class VkMessage: def get_new_messages(self): api = vk.API(self.session) - new = api.messages.getLongPollHistory(ts=self.ts, pts=self.pts) + try: + ts_pts = ujson.dumps({"ts": self.ts, "pts": self.pts}) + new = api.execute(code='return API.messages.getLongPollHistory({});'.format(ts_pts)) + except vk.api.VkAPIError: + timeout = 3 + logging.warning('Retrying getLongPollHistory in {} seconds'.format(timeout)) + time.sleep(timeout) + self.ts, self.pts = get_tses(self.session) + ts_pts = ujson.dumps({"ts": self.ts, "pts": self.pts}) + new = api.execute(code='return API.messages.getLongPollHistory({});'.format(ts_pts)) + msgs = new['messages'] self.pts = new["new_pts"] count = msgs[0]