class VkBot(CommonBot, Thread): def __init__(self): Thread.__init__(self) CommonBot.__init__(self, Platform.VK) self.token = env.str('VK_BOT_TOKEN') self.group_id = env.str('VK_BOT_GROUP_ID') vk_session = VkApi(token=self.token, api_version="5.107", config_filename="secrets/vk_bot_config.json") self.longpoll = MyVkBotLongPoll(vk_session, group_id=self.group_id) self.upload = VkUpload(vk_session) self.vk = vk_session.get_api() self.vk_user = VkUser() self.test_chat = Chat.objects.get(chat_id=env.str("VK_TEST_CHAT_ID")) def set_activity(self, peer_id, activity='typing'): if activity not in ['typing', 'audiomessage']: raise PWarning("Не знаю такого типа активности") self.vk.messages.setActivity(type=activity, peer_id=peer_id, group_id=self.group_id) def get_user_by_id(self, user_id): vk_user = self.user_model.filter(user_id=user_id) if len(vk_user) > 0: vk_user = vk_user.first() else: # Прозрачная регистрация user = self.vk.users.get(user_id=user_id, lang='ru', fields='sex, bdate, city, screen_name')[0] vk_user = Users() vk_user.user_id = user_id vk_user.name = user['first_name'] vk_user.surname = user['last_name'] vk_user.platform = self.platform.name if 'sex' in user: vk_user.gender = user['sex'] if 'bdate' in user: vk_user.birthday = self.parse_date(user['bdate']) if 'city' in user: from apps.service.models import City city_name = user['city']['title'] city = City.objects.filter(name=city_name) if len(city) > 0: city = city.first() else: try: city = add_city_to_db(city_name) except Exception: city = None vk_user.city = city else: vk_user.city = None if 'screen_name' in user: vk_user.nickname = user['screen_name'] vk_user.save() group_user = Group.objects.get(name=Role.USER.name) vk_user.groups.add(group_user) vk_user.save() return vk_user def get_chat_by_id(self, chat_id): vk_chat = self.chat_model.filter(chat_id=chat_id) if len(vk_chat) > 0: vk_chat = vk_chat.first() else: vk_chat = Chat(chat_id=chat_id, platform=self.platform.name) vk_chat.save() return vk_chat @staticmethod def add_extra_group_to_user(user, chat): group = ScheduleGroup.objects.filter(conference=chat).first() if group: groups = user.groups group_student = Group.objects.get(name=Role.STUDENT.name) if group_student not in groups.all(): user.groups.add(group_student) user.save() def get_bot_by_id(self, bot_id): if bot_id > 0: bot_id = -bot_id bot = self.bot_model.filter(bot_id=bot_id) if len(bot) > 0: bot = bot.first() else: # Прозрачная регистрация vk_bot = self.vk.groups.getById(group_id=bot_id)[0] bot = Bot() bot.bot_id = bot_id bot.name = vk_bot['name'] bot.platform = self.platform.name bot.save() return bot def send_message(self, peer_id, msg="ᅠ", attachments=None, keyboard=None, dont_parse_links=False, **kwargs): if attachments is None: attachments = [] if isinstance(attachments, str): attachments = [attachments] if attachments and msg == "ᅠ": msg = "" if keyboard: keyboard = json.dumps(keyboard) msg = str(msg) if len(msg) > 4096: msg = msg[:4092] msg += "\n..." try: self.vk.messages.send(peer_id=peer_id, message=msg, access_token=self.token, random_id=get_random_id(), attachment=','.join(attachments), keyboard=keyboard, dont_parse_links=dont_parse_links ) except vk_api.exceptions.ApiError as e: if e.code == 901: pass else: print("Ошибка отправки сообщения\n" f"{e}") def _setup_event(self, event): vk_event = { 'platform': self.platform, 'from_user': event.from_user, 'chat_id': event.chat_id, 'user_id': event.message.from_id, 'peer_id': event.message.peer_id, 'message': { # 'id': event.message.id, 'text': event.message.text, 'payload': event.message.payload, 'attachments': event.message.attachments, 'action': event.message.action }, 'fwd': None } if (vk_event['message'].get('action', None) and vk_event['message']['action']['type'] in ['chat_invite_user', 'chat_invite_user_by_link']): vk_event['message']['action']['member_ids'] = [vk_event['message']['action'].pop('member_id')] return vk_event def listen(self): for event in self.longpoll.listen(): try: # Если пришло новое сообщение if event.type == VkBotEventType.MESSAGE_NEW: vk_event = self._setup_event(event) if self.DEVELOP_DEBUG: from_test_chat = vk_event['chat_id'] == self.test_chat.id from_me = str(vk_event['user_id']) == env.str('VK_ADMIN_ID') if not from_test_chat or not from_me: continue # Сообщение либо мне в лс, либо упоминание меня, либо есть аудиосообщение, либо есть экшн if not self.need_a_response(vk_event): continue # Обработка вложенных сообщений в event['fwd']. reply и fwd для вк это разные вещи. if event.message.reply_message: vk_event['fwd'] = [event.message.reply_message] elif len(event.message.fwd_messages) != 0: vk_event['fwd'] = event.message.fwd_messages # Узнаём пользователя if vk_event['user_id'] > 0: vk_event['sender'] = self.get_user_by_id(vk_event['user_id']) else: self.send_message(vk_event['peer_id'], "Боты не могут общаться с Петровичем :(") continue # Узнаём конфу if vk_event['chat_id']: vk_event['chat'] = self.get_chat_by_id(int(vk_event['peer_id'])) if vk_event['sender'] and vk_event['chat']: self.add_chat_to_user(vk_event['sender'], vk_event['chat']) self.add_extra_group_to_user(vk_event['sender'], vk_event['chat']) else: vk_event['chat'] = None vk_event_object = VkEvent(vk_event) thread = threading.Thread(target=self.menu, args=(vk_event_object,)) thread.start() except Exception as e: print(str(e)) tb = traceback.format_exc() print(tb) @staticmethod def _prepare_obj_to_upload(file_like_object, allowed_exts_url=None): # bytes array if isinstance(file_like_object, bytes): obj = io.BytesIO(file_like_object) obj.seek(0) # bytesIO elif isinstance(file_like_object, io.BytesIO): obj = file_like_object obj.seek(0) # url elif urlparse(file_like_object).hostname: if allowed_exts_url: extension = file_like_object.split('.')[-1].lower() is_default_extension = extension not in allowed_exts_url is_vk_image = any(extension.startswith(x) for x in allowed_exts_url) if is_default_extension and not is_vk_image: raise PWarning(f"Загрузка по URL доступна только для {' '.join(allowed_exts_url)}") try: response = requests.get(file_like_object, stream=True, timeout=3) except SSLError: raise PWarning("SSLError") except requests.exceptions.ConnectionError: raise PWarning("ConnectionError") obj = response.raw # path else: obj = file_like_object return obj def _get_attachment_by_id(self, _type, group_id, _id): if group_id is None: group_id = f'-{self.group_id}' return f"{_type}{group_id}_{_id}" def upload_photos(self, images, max_count=10): if not isinstance(images, list): images = [images] attachments = [] images_to_load = [] for image in images: try: image = self._prepare_obj_to_upload(image, ['jpg', 'jpeg', 'png']) except PWarning: continue except ReadTimeout: continue except ConnectTimeout: continue # Если Content-Length > 50mb bytes_count = None if isinstance(image, io.BytesIO): bytes_count = image.getbuffer().nbytes elif isinstance(image, urllib3.response.HTTPResponse) or isinstance(image, requests.packages.urllib3.response.HTTPResponse): bytes_count = image.headers.get('Content-Length') elif os.path.exists(image): bytes_count = os.path.getsize(image) else: print("ШТО ТЫ ТАКОЕ", type(image)) if not bytes_count: continue if int(bytes_count) / 1024 / 1024 > 50: continue images_to_load.append(image) if len(images_to_load) >= max_count: break try: vk_photos = self.upload.photo_messages(images_to_load) for vk_photo in vk_photos: attachments.append(self._get_attachment_by_id('photo', vk_photo['owner_id'], vk_photo['id'])) except vk_api.exceptions.ApiError as e: print(e) return attachments def upload_document(self, document, peer_id=None, title='Документ'): document = self._prepare_obj_to_upload(document) vk_document = self.upload.document_message(document, title=title, peer_id=peer_id)['doc'] return self._get_attachment_by_id('doc', vk_document['owner_id'], vk_document['id']) def upload_audio(self, audio, artist, title): audio = self._prepare_obj_to_upload(audio) try: vk_audio = self.vk_user.upload.audio(audio, artist=artist, title=title) except vk_api.exceptions.ApiError as e: if e.code == 270: raise PWarning("Аудиозапись была удалена по просьбе правообладателя") raise PError("Ошибка загрузки аудиозаписи") return self.get_attachment_by_id('audio', vk_audio['owner_id'], vk_audio['id']) @staticmethod def get_inline_keyboard(command_text, button_text="Ещё", args=None): if args is None: args = {} return { 'inline': True, 'buttons': [[ { 'action': { 'type': 'text', 'label': button_text, "payload": json.dumps({"command": command_text, "args": args}, ensure_ascii=False) }, 'color': 'primary', } ]]} @staticmethod def get_group_id(_id): return 2000000000 + int(_id) @staticmethod def get_mention(user, name=None): name = name or str(user) return f"[id{user.user_id}|{name}]" def upload_video_by_link(self, link, name): values = { 'name': name, 'is_private': False, 'link': link, } response = self.vk_user.vk.video.save(**values) requests.post(response['upload_url']) return f"video{response['owner_id']}_{response['video_id']}" def get_attachment_by_id(self, _type, group_id, _id): if group_id is None: group_id = f'-{self.group_id}' return f"{_type}{group_id}_{_id}" def get_video(self, owner_id, _id): return self.vk_user.vk.video.get(videos=f'{owner_id}_{_id}') def set_chat_title(self, chat_id, title): self.vk.messages.editChat(chat_id=chat_id, title=title) def set_chat_title_if_not_equals(self, chat_id, title): if title != self.vk.messages.getConversationsById(peer_ids=chat_id)['items'][0]['chat_settings']['title']: self.set_chat_title(int(chat_id) - 2000000000, title) return True return False def get_conference_users(self, peer_id): try: return self.vk.messages.getConversationMembers(peer_id=peer_id, group_id=self.group_id, lang='ru')[ 'profiles'] except: raise PWarning("У бота нет админских прав для получения списка пользователей в конференции")
class VkBot(CommonBot): def __init__(self): CommonBot.__init__(self, Platform.VK) self.token = env.str('VK_BOT_TOKEN') self.group_id = env.str('VK_BOT_GROUP_ID') vk_session = VkApi(token=self.token, api_version="5.131", config_filename="secrets/vk_bot_config.json") self.longpoll = MyVkBotLongPoll(vk_session, group_id=self.group_id) self.upload = VkUpload(vk_session) self.vk = vk_session.get_api() # MAIN ROUTING AND MESSAGING def listen(self): """ Получение новых событий и их обработка """ for raw_event in self.longpoll.listen(): vk_event = VkEvent(raw_event, self) threading.Thread(target=self.handle_event, args=(vk_event, )).start() def send_response_message(self, rm: ResponseMessage): """ Отправка ResponseMessage сообщения Вовзращает список результатов отправки в формате [{success:bool, response:Response, response_message_item:ResponseMessageItem}] """ results = [] for rmi in rm.messages: try: response = self.send_response_message_item(rmi) results.append({ "success": True, "response": response, "response_message_item": rmi }) except vk_api.exceptions.ApiError as e: if e.code not in [901, 917]: error_rm = ResponseMessage(self.ERROR_MSG, rmi.peer_id).messages[0] self.logger.error({ 'message': self.ERROR_MSG, 'error': str(e) }) response = self.send_response_message_item(error_rm) results.append({ "success": False, "response": response, "response_message_item": error_rm }) return results def send_response_message_item(self, rm: ResponseMessageItem): """ Отправка ResponseMessageItem сообщения Возвращает Response платформы """ text = str(rm.text) if len(text) > 4096: text = text[:4092] text += "\n..." attachments = [] for att in rm.attachments: if isinstance(att, str): attachments.append(att) elif att.url: attachments.append(att.url) elif att.public_download_url: attachments.append(att.public_download_url) return self.vk.messages.send( peer_id=rm.peer_id, message=text, access_token=self.token, random_id=get_random_id(), attachment=','.join(attachments), keyboard=json.dumps(rm.keyboard), ) # END MAIN ROUTING AND MESSAGING # ATTACHMENTS def upload_photos(self, images, max_count=10, peer_id=None): """ Загрузка фотографий на сервер ТГ. images: список изображений в любом формате (ссылки, байты, файлы) При невозможности загрузки одной из картинки просто пропускает её """ atts = super().upload_photos(images, max_count) parsed_atts = [] for pa in atts: try: url, public_download_url = self.upload_photo_and_urls(pa) pa.url = url.replace(VK_URL, '') pa.public_download_url = public_download_url parsed_atts.append(pa) except Exception: continue return parsed_atts def upload_photo_and_urls(self, image: PhotoAttachment): """ Загрузка изображения на сервер VK Возвращает vk_url и public_download_url """ vk_photo = self.upload.photo_messages(image.get_bytes_io_content())[0] vk_url = f"{VK_URL}photo{vk_photo['owner_id']}_{vk_photo['id']}" vk_max_photo_url = sorted(vk_photo['sizes'], key=lambda x: x['height'])[-1]['url'] return vk_url, vk_max_photo_url def upload_document(self, document, peer_id=None, title='Документ', filename=None): """ Загрузка документа на сервер ВК. """ da = super().upload_document(document, peer_id, title, filename) content = da.download_content() vk_doc = self.upload.document_message(content, title=title, peer_id=peer_id)['doc'] return f"doc{vk_doc['owner_id']}_{vk_doc['id']}" # END ATTACHMENTS # USERS GROUPS BOTS def get_profile_by_user(self, user: User, is_new=False, _defaults: dict = None) -> Profile: """ Возвращает пользователя по его id Регистрирует если пользователя нет в БД """ if not user.profile: vk_user = self.get_user_info(int(user.user_id)) profile = Profile() profile.name = vk_user['first_name'] profile.surname = vk_user['last_name'] profile.platform = self.platform.name profile.set_avatar(vk_user['photo_max']) if 'sex' in vk_user: profile.gender = vk_user['sex'] if 'city' in vk_user: from apps.service.models import City city_name = vk_user['city']['title'] city = City.objects.filter(name__icontains=city_name) if len(city) > 0: city = city.first() else: try: city = add_city_to_db(city_name) except Exception: city = None profile.city = city else: profile.city = None if 'screen_name' in vk_user: user.nickname = vk_user['screen_name'] with self.lock: profile.save() user.profile = profile user.save() return super().get_profile_by_user(user, is_new=True) return super().get_profile_by_user(user) def get_user_info(self, user_id: int): """ Получение информации о пользователе """ return self.vk.users.get( user_id=user_id, lang='ru', fields='sex, bdate, city, screen_name, photo_max')[0] def update_profile_avatar(self, profile: Profile, user_id): """ Обновление аватара пользователя """ user_info = self.get_user_info(user_id) profile.set_avatar(user_info['photo_max']) def get_bot_by_id(self, bot_id: int) -> BotModel: """ Получение информации о боте """ try: bot = self.bot_model.get(bot_id=bot_id) except BotModel.DoesNotExist: bot = super().get_bot_by_id(bot_id) vk_bot = self.get_bot_info(bot_id) bot.name = vk_bot['name'] bot.set_avatar(vk_bot['photo_200']) bot.save() return bot def get_bot_info(self, bot_id): """ Получение информации о боте """ return self.vk.groups.getById(group_id=bot_id)[0] def update_bot_avatar(self, bot_id): """ Обновление аватара бота """ bot = self.get_bot_by_id(bot_id) bot_info = self.get_bot_info(bot_id) bot.name = bot_info['name'] bot.set_avatar(bot_info['photo_200']) # END USERS GROUPS BOTS # EXTRA def set_activity(self, peer_id, activity: ActivitiesEnum): """ Метод позволяет указать пользователю, что бот набирает сообщение или записывает голосовое Используется при длительном выполнении команд, чтобы был фидбек пользователю, что его запрос принят """ tg_activity = VK_ACTIVITIES.get(activity) if tg_activity: self.vk.messages.setActivity(type=tg_activity, peer_id=peer_id, group_id=self.group_id) @staticmethod def _get_keyboard_buttons(buttons): """ Определение структуры кнопок """ return [{ 'action': { 'type': 'text', 'label': button_item['button_text'], "payload": json.dumps( { "command": button_item['command'], "args": button_item.get('args'), }, ensure_ascii=False) }, 'color': 'primary', } for button_item in buttons] def get_inline_keyboard(self, buttons: list, cols=1): """ param buttons: ToDo: Получение инлайн-клавиатуры с кнопками В основном используется для команд, где нужно запускать много команд и лень набирать заново """ keyboard = super().get_inline_keyboard(buttons) return {'inline': True, 'buttons': keyboard} def get_mention(self, profile: Profile, name=None): """ Получение меншона пользователя """ user = profile.get_user_by_platform(self.platform) name = name or str(user) return f"[id{user.user_id}|{name}]" def remove_self_from_chat(self, chat_id): """ Удаление бота (себя) из чата """ self.vk.messages.removeChatUser(chat_id=chat_id, member_id=f"-{self.group_id}") def get_conversation_messages(self, peer_id, conversation_message_id): """ Получение полного сообщения """ response = self.vk.messages.getByConversationMessageId( peer_id=peer_id, conversation_message_ids=[conversation_message_id]) return response['items'][0]
def main(): """ работа чат-бота по запросам от пользователя :return: сообщения чат-бота с прогнозом из БД по случайному числу, выданное в личном чате сообщества, или ответы на запросы """ load_dotenv() # .env vk_session = vk_api.VkApi(token=env['VK_TOKEN']) # токен longpoll = VkBotLongPoll(vk_session, int(env['VK_BOT_ID'])) # ID vk = vk_session.get_api() # открытие сессии m = -1 # параметр для отслеживания выдачи ответов без бросания монет # запуск сессии чат-бота for event in longpoll.listen(): if event.type == VkBotEventType.MESSAGE_NEW: from_id = event.obj.message['from_id'] text = event.obj.message['text'].lower() print(f'Новое сообщение {text} от {from_id}' ) # оповещение о новом сообщении от пользователя # обработка сообщений пользователя # запрос на получение ответа if text in ['получить ответ', 'ответ', 'answer']: if m == -1: vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[5], ensure_ascii=False), message= f'Вы хотите получить ответ без бросания монет? Так ничего не получится. Нужно строго соблюдать ритуал! \n \n' f'Пожалуйста, наберите "Бросить" или "бросить" или нажмите на кнопку "Бросить монеты"', random_id=random.randint(0, 2**64)) else: # получение по числу m предсказания из БД mess = work_in_db(m) # печать картинки гексаграммы из БД attachments = [] from vk_api import VkUpload upload = VkUpload(vk_session) image = f'data/Img/{m}.png' photo = upload.photo_messages(photos=image)[0] attachments.append('photo{}_{}'.format( photo['owner_id'], photo['id'])) vk.messages.send(user_id=from_id, attachment=','.join(attachments), random_id=random.randint(0, 2**64)) # печать текста предсказания и выдача клавиатуры для дальнейших действий vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[1], ensure_ascii=False), message= f'Вам выпала гексаграмма {chr(mess[0][2])} {mess[0][0]} - {mess[0][1]} \n \n Ее значение: \n {mess[1]} \n \n' f'Если Вы хотите сохранить предсказание, то напишите "сохранить" или "Сохранить", или нажмите ' f'кнопку "Сохранить предсказание"', random_id=random.randint(0, 2**64)) m = -1 # запрос на сохранение ответа elif text in ['сохранить предсказание', 'сохранить', 'save']: f = open('prognoz.txt', 'w') st = ''.join( f'Вам выпала гексаграмма {mess[0][0]} - {mess[0][1]} \n \n Ее значение: \n {mess[1]}' ) f.write(st) f.close() # информация для пользователя о сохранении предсказания и выдача клавиатуры для дальнейших действий vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[2], ensure_ascii=False), message=f'Предсказание сохранено. \n \n' f'Если Вы хотите скачать сохраненное предсказание, то напишите "Скачать" или "скачать", или ' f'нажмите кнопку "Скачать файл с предсказанием"', random_id=random.randint(0, 2**64)) # запрос на скачивание файла с ответом elif text in [ 'скачать файл с предсказанием', 'скачать', 'download' ]: attachments = [] from vk_api import VkUpload upload = VkUpload(vk_session) pr = f'data/Prognoz/{mess[0][1]}.txt' document = upload.document_message(doc=pr, title='Ваше предсказание', peer_id=from_id) print(document) attachments.append('doc{}_{}'.format( document['doc']['owner_id'], document['doc']['id'])) # выдача файла с предсказанием пользователю vk.messages.send(keyboard=json.dumps(keyboards.keyboard[0], ensure_ascii=False), user_id=from_id, attachment=','.join(attachments), random_id=random.randint(0, 2**64)) # запрос, как задать вопрос или для задания нового вопроса elif text in [ 'вопрос', 'как задать свой вопрос?', 'задать новый вопрос', 'question' ]: # описание действия при формулировке вопроса и выдача клавиатуры для дальнейших действий vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[4], ensure_ascii=False), message= f'В гадании принято бросать шесть монет для формирования гексаграммы ответа. \n \n' f'Монеты падают случайным образом, каждая из них будет лежать одной стороной вверх -' f' орлом или решкой, это и сформирует нужный для предсказания код. \n \n' f'Если Вы первый раз зашли на ресурс и не знакомы с монетами И-Цзин, нажав на кнопку ' f'"Монеты", Вы получите информацию о сторонах монеты И-Цзин. \n \n' f'Хорошо продумайте свой вопрос, задайте его Высшим силам и нажмите кнопку "Бросить монеты".', random_id=random.randint(0, 2**64)) # запрос на бросание монет elif text in ['бросить монеты', 'бросить', 'toss coins']: # получение случайного числа от 0 до 63 m = random.randint(0, 63) # формирование его двоичного кода mm = bin(m)[2:] # дополнение двоичного кода до 6 знаков не значащими нулями if len(mm) < 6: mm = (6 - len(mm)) * '0' + mm # разбор кода и составление набора из 6 монет - орлы и решки attachments = [] for x in mm: if x == '0': from vk_api import VkUpload upload = VkUpload(vk_session) image = f'data/Img/moneta_reshka.png' photo = upload.photo_messages(photos=image)[0] attachments.append('photo{}_{}'.format( photo['owner_id'], photo['id'])) else: from vk_api import VkUpload upload = VkUpload(vk_session) image = f'data/Img/moneta_orel.png' photo = upload.photo_messages(photos=image)[0] attachments.append('photo{}_{}'.format( photo['owner_id'], photo['id'])) # печать картинки конфигурации монет vk.messages.send(user_id=from_id, attachment=','.join(attachments), random_id=random.randint(0, 2**64)) # пояснения к расшифровке картинки и выдача клавиатуры для дальнейших действий vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[3], ensure_ascii=False), message= f'Гексаграмма записывается снизу вверх, т.е. первая монета определяет нижнюю черту и так далее. \n' f'Сплошная линия соответствует орлу, прерванная линия - решке. \n \n' f'Нажмите кнопку "Получить ответ" или наберите текст "ответ" или "Ответ". \n \n' f'Вы получите предсказание по Вашему вопросу согласно комбинации выпавших монет.', random_id=random.randint(0, 2**64)) # запрос на получение команд бота elif text in ['правила', 'rules', 'commands']: # правила общения в сообществе vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[0], ensure_ascii=False), message=f'Правила для управления ботом: \n \n' f'Для демонстрации клавиатуры нужно набрать сообщение с текстом "Клавиатура" или "клавиатура".\n \n' f'Для повторного ознакомления с правилами наберите "правила" или "Правила", или нажмите кнопку "Правила" \n \n' f'Для того, чтобы получить указания, что делать дальше, наберите "вопрос" или "Вопрос", или ' f'нажмите кнопку "Как задать свой вопрос?"\n \n' f'Далее - следуйте инструкциям бота.\n \n' f'Удачи Вам и исполнения желаний!', random_id=random.randint(0, 2**64)) # запрос на получение стартовой клавиатуры - кнопок бота elif text in ['клавиатура', 'keyboard']: # настройка стартовой клавиатуры для пользователя vk.messages.send(user_id=from_id, keyboard=json.dumps(keyboards.keyboard[0], ensure_ascii=False), key=(env['VK_TOKEN']), random_id=random.randint(0, 2**64), message='Держи!') # запрос на получение информации о монетах И-Цзин elif text in ['coins', 'монеты', 'coin', 'монета']: # информация об орле и решке монет И-Цзин from vk_api import VkUpload upload = VkUpload(vk_session) image = f'data/Img/moneta_orel.png' photo = upload.photo_messages(photos=image)[0] attachments = [] attachments.append('photo{}_{}'.format(photo['owner_id'], photo['id'])) vk.messages.send(user_id=from_id, attachment=','.join(attachments), message=f'Лицевая сторона монеты - орел', random_id=random.randint(0, 2**64)) attachments = [] image = f'data/Img/moneta_reshka.png' photo = upload.photo_messages(photos=image)[0] attachments.append('photo{}_{}'.format(photo['owner_id'], photo['id'])) vk.messages.send(user_id=from_id, keyboard=json.dumps(keyboards.keyboard[5], ensure_ascii=False), attachment=','.join(attachments), message=f'Тыльная сторона монеты - решка.', random_id=random.randint(0, 2**64)) vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[5], ensure_ascii=False), message= f'Хорошо продумайте свой вопрос, задайте его Высшим силам и нажмите кнопку "Бросить монеты".', random_id=random.randint(0, 2**64)) # вступительный текст с инструкцией для нерегламентированных сообщений от пользователя else: vk.messages.send( user_id=from_id, keyboard=json.dumps(keyboards.keyboard[0], ensure_ascii=False), key=(env['VK_TOKEN']), message= f'Если Вы в первый раз пользуетесь нашим ресурсом, то нужно ознакомиться с Правилами сообщества. \n \n' f'Для этого введите "правила" или "Правила" в строку сообщения, или нажмите кнопку "Правила" \n \n' f'Для тех, кто уже знаком с правилами, - введите "вопрос" или "Вопрос", или нажмите кнопку "Как задать свой вопрос?', random_id=random.randint(0, 2**64)) # обработка событий, когда долго нет пользователей - для сохранения онлайн сессии от прерываний со стороны ВК elif event.type == VkBotEventType.MESSAGE_EVENT: event_id = event.obj.message['event_id'] vk.messages.send(user_id=from_id, event_id=event_id, keyboard=json.dumps(keyboards.keyboard[0], ensure_ascii=False), message=f'Жду вопросов...', random_id=random.randint(0, 2**64))
def main(): vk = vk_session.get_api() longpoll = VkLongPoll(vk_session) upload = VkUpload(vk_session) print("Started") for event in longpoll.listen(): if event.type == VkEventType.MESSAGE_NEW and event.to_me and event.text: pprint(event.__dict__) print('{}: id{}: "{}"'.format(event.message_id, event.user_id, event.text)) if (event.text == ''): vk.messages.send( user_id=event.user_id, random_id=get_random_id(), message= 'Message text is required. Please enter a name or archive and resend your message.' ) continue messages_data = vk_session.method( 'messages.getById', {'message_ids': set([ event.message_id, ])}) pprint(messages_data) messages_data = messages_data['items'][0] links = get_photos_links(messages_data) print(links) print("len = ", len(links)) if (len(links) == 0): vk.messages.send(user_id=event.user_id, random_id=get_random_id(), message='No photos') continue muid = str(event.user_id) + str(event.message_id) pics_path = os.path.join('data', muid) if not os.path.exists(pics_path): os.makedirs(pics_path) for c, plink in enumerate(links): print(c) with urllib.request.urlopen(plink) as response, open( os.path.join('data', muid, str(c) + '.jpg'), 'wb') as out_file: data = response.read() # a `bytes` object out_file.write(data) zip_name = os.path.join('data', str(muid) + ".zip") zipf = zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) zipdir(pics_path, zipf) zipf.close() doc = upload.document_message(doc=zip_name, title=event.text + str(muid) + ".zip", peer_id=event.peer_id) doc = doc['doc'] pprint(doc) vk.messages.send(user_id=event.user_id, attachment='doc{}_{}'.format( doc['owner_id'], doc['id']), random_id=get_random_id(), message='Ok. Done.') print('ok')
class VKPayloadProcessor: def __init__(self, data: dict, vk, peer_id: int): self.message = '' self.attachments = [] self.keyboard = None self.session = requests.Session() self.peer_id = peer_id if peer_id else None self.upload = VkUpload(vk) if data.get('text'): self.message = data['text'] if data.get('image_url'): self.get_image_from_url(data['image_url']) if data.get('keyboard'): self.parse_keyboard(data['keyboard']) if data.get('attachments'): self.get_attachments_from_urls(data['attachments']) def get_image_from_url(self, url: str): f = self.session.get(url, stream=True) photo = self.upload.photo_messages(photos=f.raw, peer_id=self.peer_id)[0] self.attachments.append(f"photo{photo['owner_id']}_{photo['id']}") def get_attachments_from_urls(self, urls: List[dict]): for file in urls: if file.get('vk_id') is not None: self.attachments.append(file.get('vk_id')) return f = self.session.get(file.get('url'), stream=True) # TODO really gotta find a way around writing the file to the disk to save it with open('/tmp/metadata.' + file.get('extension'), 'wb') as fp: for chunk in f.iter_content(chunk_size=8192): fp.write(chunk) doc = self.upload.document_message(doc='/tmp/metadata.pdf', title=file.get('title'), peer_id=self.peer_id)['doc'] self.attachments.append(f"doc{doc['owner_id']}_{doc['id']}") def parse_keyboard(self, data: Dict): self.keyboard = VkKeyboard() self.keyboard.one_time = data.get('one_time') or True self.keyboard.inline = data.get('inline') or True for btn in data.get('buttons'): btn = btn[0] action_type = btn.get('action').get('type') if action_type == 'open_link': self.keyboard.add_openlink_button( link=btn.get('action').get('link'), label=btn.get('action').get('label'), payload=btn.get('action').get('payload'), ) elif action_type == 'text': self.keyboard.add_button(label=btn.get('action').get('label'), color=btn.get('color')) else: raise NotImplementedError('Unsupported keyboard action type!') def get_json_data(self) -> Dict: values = { 'attachment': ','.join(self.attachments) if self.attachments else None, 'message': self.message, 'keyboard': self.keyboard.get_keyboard() if self.keyboard else None } return values
class VkBotClass(threading.Thread): def __init__(self): super().__init__() self._TOKEN = secrets['vk']['bot']['TOKEN'] self.group_id = secrets['vk']['bot']['group_id'] vk_session = vk_api.VkApi(token=self._TOKEN, api_version="5.107", config_filename="secrets/vk_bot_config.json") self.longpoll = MyVkBotLongPoll(vk_session, group_id=self.group_id) self.upload = VkUpload(vk_session) self.vk = vk_session.get_api() self.mentions = secrets['vk']['bot']['mentions'] self.vk_user = VkUserClass() self.BOT_CAN_WORK = True self.DEBUG = False self.DEVELOP_DEBUG = False @staticmethod def parse_date(date): date_arr = date.split('.') if len(date_arr) == 2: return f"1970-{date_arr[1]}-{date_arr[0]}" else: return f"{date_arr[2]}-{date_arr[1]}-{date_arr[0]}" def need_a_response(self, vk_event, _have_audio_message, have_action): message = vk_event['message']['text'] from_user = vk_event['from_user'] if _have_audio_message: return True if have_action: return True if len(message) == 0: return False if from_user: return True if message[0] == '/': return True for mention in self.mentions: if message.find(mention) != -1: return True return False def set_activity(self, peer_id, activity='typing'): if activity not in ['typing', 'audiomessage']: raise RuntimeWarning("Не знаю такого типа активности") self.vk.messages.setActivity(type=activity, peer_id=peer_id, group_id=self.group_id) def send_message(self, peer_id, msg="ᅠ", attachments=None, keyboard=None, dont_parse_links=False, **kwargs): if attachments is None: attachments = [] if isinstance(attachments, str): attachments = [attachments] if attachments and msg == "ᅠ": msg = "" if keyboard: keyboard = json.dumps(keyboard) msg = str(msg) if len(msg) > 4096: msg = msg[:4092] msg += "\n..." try: self.vk.messages.send(peer_id=peer_id, message=msg, access_token=self._TOKEN, random_id=get_random_id(), attachment=','.join(attachments), keyboard=keyboard, dont_parse_links=dont_parse_links) except vk_api.exceptions.ApiError as e: if e.code == 901: pass else: print("Ошибка отправки сообщения\n" f"{e}") # Отправляет сообщения юзерам в разных потоках def parse_and_send_msgs_thread(self, chat_ids, message): if not isinstance(chat_ids, list): chat_ids = [chat_ids] for chat_id in chat_ids: thread = threading.Thread(target=self.parse_and_send_msgs, args=( chat_id, message, )) thread.start() def parse_and_send_msgs(self, peer_id, result): if isinstance(result, str) or isinstance(result, int) or isinstance( result, float): result = {'msg': result} if isinstance(result, dict): result = [result] if isinstance(result, list): for msg in result: if isinstance(msg, str): msg = {'msg': msg} if isinstance(msg, dict): self.send_message(peer_id, **msg) def menu(self, vk_event, send=True): from apps.API_VK.command.initial import get_commands # Проверяем не остановлен ли бот, если так, то проверяем вводимая команда = старт? if not self.check_bot_working(): if not check_user_group(vk_event.sender, Role.ADMIN): return if vk_event.command in ['старт']: self.BOT_CAN_WORK = True # cameraHandler.resume() msg = "Стартуем!" self.send_message(vk_event.peer_id, msg) log_result = {'result': msg} logger.debug(log_result) return msg return group = vk_event.sender.groups.filter(name=Role.BANNED.name) if len(group) > 0: return if self.DEBUG and send: if hasattr(vk_event, 'payload') and vk_event.payload: debug_message = \ f"msg = {vk_event.msg}\n" \ f"command = {vk_event.command}\n" \ f"args = {vk_event.args}\n" \ f"payload = {vk_event.payload}\n" else: debug_message = \ f"msg = {vk_event.msg}\n" \ f"command = {vk_event.command}\n" \ f"args = {vk_event.args}\n" \ f"original_args = {vk_event.original_args}\n" self.send_message(vk_event.peer_id, debug_message) log_vk_event = {'vk_event': vk_event} logger.debug(log_vk_event) commands = get_commands() for command in commands: try: if command.accept(vk_event): result = command.__class__().check_and_start( self, vk_event) if send: self.parse_and_send_msgs(vk_event.peer_id, result) append_command_to_statistics(vk_event.command) log_result = {'result': result} logger.debug(log_result) return result except RuntimeWarning as e: msg = str(e) log_runtime_warning = {'result': msg} logger.warning(log_runtime_warning) if send: self.parse_and_send_msgs(vk_event.peer_id, msg) return msg except RuntimeError as e: exception = str(e) log_runtime_error = { 'exception': exception, 'result': exception } logger.error(log_runtime_error) if send: self.parse_and_send_msgs(vk_event.peer_id, exception) return exception except Exception as e: msg = "Непредвиденная ошибка. Сообщите разработчику в группе или команда /баг" tb = traceback.format_exc() log_exception = {'exception': str(e), 'result': msg} logger.error(log_exception, exc_info=tb) if send: self.parse_and_send_msgs(vk_event.peer_id, msg) return msg if vk_event.chat and not vk_event.chat.need_reaction: return None similar_command = commands[0].names[0] tanimoto_max = 0 user_groups = get_user_groups(vk_event.sender) for command in commands: # Выдача пользователю только тех команд, которые ему доступны command_access = command.access if isinstance(command_access, str): command_access = [command_access] if command_access.name not in user_groups: continue for name in command.names: if name: tanimoto_current = tanimoto(vk_event.command, name) if tanimoto_current > tanimoto_max: tanimoto_max = tanimoto_current similar_command = name msg = f"Я не понял команды \"{vk_event.command}\"\n" if tanimoto_max != 0: msg += f"Возможно вы имели в виду команду \"{similar_command}\"" logger_result = {'result': msg} logger.debug(logger_result) if send: self.send_message(vk_event.peer_id, msg) return msg def listen_longpoll(self): for event in self.longpoll.listen(): try: # Если пришло новое сообщение if event.type == VkBotEventType.MESSAGE_NEW: # from_user - сообщение пришло из диалога с пользователем # chat_id - присутствует только в чатах # text - текст сообщения # from_id - кто отправил сообщение (если значение отрицательное, то другой бот) # user_id - id пользователя # peer_id - откуда отправил сообщение (если там значение совпадает с from_id, то это from_user, # если нет и значение начинается на 200000000*, то это конфа # payload - скрытая информация, которая передаётся при нажатии на кнопку vk_event = { 'from_user': event.from_user, 'chat_id': event.chat_id, 'user_id': event.message.from_id, 'peer_id': event.message.peer_id, 'message': { # 'id': event.message.id, 'text': event.message.text, 'payload': event.message.payload, 'attachments': event.message.attachments, 'action': event.message.action }, 'fwd': None } # Если я запустился из под дебага, реагируй только на меня и только в моей конфе if self.DEVELOP_DEBUG: from_test_chat = vk_event['chat_id'] == TEST_CHAT_ID from_me = str( vk_event['user_id']) == secrets['vk']['admin_id'] if not from_test_chat or not from_me: continue # Обработка вложенных сообщений в vk_event['fwd']. reply и fwd для вк это разные вещи. if event.message.reply_message: vk_event['fwd'] = [event.message.reply_message] elif len(event.message.fwd_messages) != 0: vk_event['fwd'] = event.message.fwd_messages # Проверка есть ли аудиосообщения have_audio_message_flag = have_audio_message(vk_event) # Проверка есть ли в сообщении action have_action = vk_event['message']['action'] is not None # Сообщение либо мне в лс, либо упоминание меня, либо есть аудиосообщение, либо есть экшн if not self.need_a_response( vk_event, have_audio_message_flag, have_action): continue # Узнаём пользователя if vk_event['user_id'] > 0: vk_event['sender'] = self.get_user_by_id( vk_event['user_id']) else: self.send_message( vk_event['peer_id'], "Боты не могут общаться с Петровичем :(") continue # Узнаём конфу if vk_event['chat_id']: vk_event['chat'] = self.get_chat_by_id( int(vk_event['peer_id'])) if vk_event['sender'] and vk_event['chat']: self.add_group_to_user(vk_event['sender'], vk_event['chat']) else: vk_event['chat'] = None vk_event_object = VkEvent(vk_event) thread = threading.Thread(target=self.menu, args=(vk_event_object, )) thread.start() else: pass except Exception as e: tb = traceback.format_exc() log_exception = { 'exception': str(e), } logger.error(log_exception, exc_info=tb) def run(self): self.listen_longpoll() def get_chat_title(self, chat_id): return self.vk.messages.getconversationById( peer_ids=2000000000 + chat_id)['items'][0]['chat_settings']['title'] def set_chat_title(self, chat_id, title): self.vk.messages.editChat(chat_id=chat_id, title=title) def set_chat_title_if_not_equals(self, chat_id, title): if title != self.vk.messages.getconversationById( peer_ids=2000000000 + chat_id)['items'][0]['chat_settings']['title']: self.vk.messages.editChat(chat_id=chat_id, title=title) def get_user_by_id(self, user_id): vk_user = VkUser.objects.filter(user_id=user_id) if len(vk_user) > 0: vk_user = vk_user.first() else: # Прозрачная регистрация user = self.vk.users.get(user_id=user_id, lang='ru', fields='sex, bdate, city, screen_name')[0] vk_user = VkUser() vk_user.user_id = user_id vk_user.name = user['first_name'] vk_user.surname = user['last_name'] if 'sex' in user: vk_user.gender = user['sex'] if 'bdate' in user: vk_user.birthday = self.parse_date(user['bdate']) if 'city' in user: from apps.service.models import City city_name = user['city']['title'] city = City.objects.filter(name=city_name) if len(city) > 0: city = city.first() else: try: city = add_city_to_db(city_name) except Exception: city = None vk_user.city = city else: vk_user.city = None if 'screen_name' in user: vk_user.nickname = user['screen_name'] vk_user.save() group_user = Group.objects.get(name=Role.USER.name) vk_user.groups.add(group_user) vk_user.save() return vk_user @staticmethod def get_gamer_by_user(user): from apps.games.models import Gamer gamers = Gamer.objects.filter(user=user) if len(gamers) == 0: gamer = Gamer(user=user) gamer.save() return gamer elif len(gamers) > 1: raise RuntimeWarning("Два и более игрока подходит под поиск") else: return gamers.first() @staticmethod def get_user_by_name(args, filter_chat=None): if not args: raise RuntimeWarning("Отсутствуют аргументы") if isinstance(args, str): args = [args] vk_users = VkUser.objects if filter_chat: vk_users = vk_users.filter(chats=filter_chat) if len(args) >= 2: user = vk_users.filter(name=args[0].capitalize(), surname=args[1].capitalize()) else: user = vk_users.filter(nickname_real=args[0].capitalize()) if len(user) == 0: user = vk_users.filter(name=args[0].capitalize()) if len(user) == 0: user = vk_users.filter(surname=args[0].capitalize()) if len(user) == 0: user = vk_users.filter(nickname=args[0]) if len(user) == 0: user = vk_users.filter(user_id=args[0]) if len(user) > 1: raise RuntimeWarning("2 и более пользователей подходит под поиск") if len(user) == 0: raise RuntimeWarning( "Пользователь не найден. Возможно опечатка или он мне ещё ни разу не писал" ) return user.first() @staticmethod def get_chat_by_name(args): if not args: raise RuntimeWarning("Отсутствуют аргументы") if isinstance(args, str): args = [args] vk_chats = VkChat.objects for arg in args: vk_chats = vk_chats.filter(name__icontains=arg) if len(vk_chats) > 1: raise RuntimeWarning("2 и более чатов подходит под поиск") if len(vk_chats) == 0: raise RuntimeWarning("Чат не найден") return vk_chats.first() def get_bot_by_id(self, bot_id): if bot_id > 0: bot_id -= 1 vk_bot = VkBot.objects.filter(bot_id=bot_id) if len(vk_bot) > 0: vk_bot = vk_bot.first() else: # Прозрачная регистрация bot = self.vk.groups.getById(group_id=bot_id)[0] vk_bot = VkBot() vk_bot.bot_id = bot_id vk_bot.name = bot['name'] vk_bot.save() return vk_bot @staticmethod def get_chat_by_id(chat_id): vk_chat = VkChat.objects.filter(chat_id=chat_id) if len(vk_chat) > 0: vk_chat = vk_chat.first() else: vk_chat = VkChat(chat_id=chat_id) vk_chat.save() return vk_chat @staticmethod def add_group_to_user(vk_user, chat): chats = vk_user.chats if chat not in chats.all(): chats.add(chat) @staticmethod def remove_group_from_user(vk_user, chat): chats = vk_user.chats if chat in chats.all(): chats.remove(chat) @staticmethod def get_group_id(_id): return 2000000000 + int(_id) def get_short_link(self, long_link): result = self.vk.utils.getShortLink(url=long_link) if 'short_url' in result: return result['short_url'] else: return None def check_bot_working(self): return self.BOT_CAN_WORK @staticmethod def _prepare_obj_to_upload(file_like_object, allowed_exts_url=None): # bytes array if isinstance(file_like_object, bytes): obj = io.BytesIO(file_like_object) obj.seek(0) # bytesIO elif isinstance(file_like_object, io.BytesIO): obj = file_like_object obj.seek(0) # url elif urlparse(file_like_object).hostname: if allowed_exts_url: if file_like_object.split( '.')[-1].lower() not in allowed_exts_url: raise RuntimeWarning( f"Загрузка по URL доступна только для {' '.join(allowed_exts_url)}" ) try: response = requests.get(file_like_object, stream=True, timeout=3) except SSLError: raise RuntimeWarning("SSLError") except requests.exceptions.ConnectionError: raise RuntimeWarning("ConnectionError") obj = response.raw # path else: obj = file_like_object return obj def upload_photos(self, images, max_count=10): if not isinstance(images, list): images = [images] attachments = [] images_to_load = [] for image in images: try: image = self._prepare_obj_to_upload(image, ['jpg', 'jpeg', 'png']) except RuntimeWarning: continue except ReadTimeout: continue except ConnectTimeout: continue # Если Content-Length > 50mb bytes_count = None if isinstance(image, io.BytesIO): bytes_count = image.getbuffer().nbytes elif isinstance( image, urllib3.response.HTTPResponse) or isinstance( image, requests.packages.urllib3.response.HTTPResponse): bytes_count = image.headers.get('Content-Length') elif os.path.exists(image): bytes_count = os.path.getsize(image) else: print("ШТО ТЫ ТАКОЕ", type(image)) if not bytes_count: continue if int(bytes_count) / 1024 / 1024 > 50: continue images_to_load.append(image) if len(images_to_load) >= max_count: break try: vk_photos = self.upload.photo_messages(images_to_load) for vk_photo in vk_photos: attachments.append( self.get_attachment_by_id('photo', vk_photo['owner_id'], vk_photo['id'])) except vk_api.exceptions.ApiError as e: print(e) return attachments def upload_document(self, document, peer_id, title='Документ'): document = self._prepare_obj_to_upload(document) vk_document = self.upload.document_message(document, title=title, peer_id=peer_id)['doc'] return self.get_attachment_by_id('doc', vk_document['owner_id'], vk_document['id']) def upload_audio(self, audio, artist, title): audio = self._prepare_obj_to_upload(audio) try: vk_audio = self.vk_user.upload.audio(audio, artist=artist, title=title) except vk_api.exceptions.ApiError as e: if e.code == 270: raise RuntimeError( "Аудиозапись была удалена по просьбе правообладателя") raise RuntimeError("Ошибка загрузки аудиозаписи") return self.get_attachment_by_id('audio', vk_audio['owner_id'], vk_audio['id']) def upload_video_by_link(self, link, name): values = { 'name': name, 'is_private': True, 'link': link, } response = self.vk_user.vk.video.save(**values) response2 = requests.post(response['upload_url']).json() print(response2) return f"video{response['owner_id']}_{response['video_id']}" def get_attachment_by_id(self, _type, group_id, _id): if group_id is None: group_id = f'-{self.group_id}' return f"{_type}{group_id}_{_id}"
def send_message(uid, chat_id, msg=None, photo=None, documents=None, audio=None, voice=None, video=None, sticker=None): try: session = get_session(uid) api = get_api(uid) except IndexError: return 0 # Generate message id like in vk api msg_id = randint(1, 9223372036854775700) try: vchat_id = execute( f"select vchat_id from chats where chat_id = {chat_id}")[0][0] # if chat is conference if vchat_id is None: vchat_id = execute( f"select peer_id from chats where chat_id = {chat_id}")[0][0] except IndexError: # If telegram group not binded to vk chat stream return attachments = [] if photo or documents or audio or voice or video or sticker: if photo: attach = photo elif documents: attach = documents elif audio: attach = audio elif voice: attach = voice elif video: attach = video elif sticker: attach = sticker else: attach = None uploader = VkUpload(session) attach_file = attach.get_file().download() if sticker: new_name = attach_file.split('.')[0] + '.png' try: im = Image.open(attach_file).convert("RGBA") im.save(new_name, "png") except UnidentifiedImageError: # Animated sticker return attach_file = new_name attach_file = open(attach_file, 'rb') file_name = attach_file.name if photo: upload_response = uploader.photo_messages(photos=attach_file, peer_id=vchat_id) for file in upload_response: attachments.append(f"photo{file['owner_id']}_{file['id']}") if audio: _msg = "VK doesn't allow to upload music." if msg is None: msg = _msg else: msg += "\n" + _msg if voice: upload_response = uploader.audio_message(audio=attach_file, peer_id=vchat_id) audio = upload_response['audio_message'] attachments.append( f"audio_message{audio['owner_id']}_{audio['id']}") if documents: upload_response = uploader.document_message(attach_file, peer_id=vchat_id) doc = upload_response['doc'] attachments.append(f"doc{doc['owner_id']}_{doc['id']}") if video: video = uploader.video(attach_file, is_private=True) attachments.append(f"video{video['owner_id']}_{video['video_id']}") if sticker: upload_response = uploader.graffiti(attach_file, peer_id=vchat_id) graffiti = upload_response['graffiti'] attachments.append(f"doc{graffiti['owner_id']}_{graffiti['id']}") attach_file.close() remove(file_name) attachments = ','.join(attachments) if msg is not None: api.messages.send(random_id=msg_id, message=msg, peer_id=vchat_id, attachment=attachments) else: api.messages.send(random_id=msg_id, peer_id=vchat_id, attachment=attachments) api.account.setOffline()