# coding: utf-8

from __future__ import annotations

import asyncio
import json
from typing import TYPE_CHECKING, Literal, Optional, cast

import aiohttp
import cachetools
from aiocouch import Document
from aiogram import Bot
from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import Audio, BufferedInputFile
from aiogram.types import Document as TelegramDocument
from aiogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
                           InputMediaAudio, InputMediaDocument,
                           InputMediaPhoto, InputMediaVideo, Message,
                           PhotoSize, Sticker, Video, VideoNote, Voice)
from aiogram.utils.chat_action import ChatActionSender
from loguru import logger
from pydantic import SecretStr
from pyrate_limiter import Limiter, RequestRate

import utils
from config import config
from DB import get_user
from services.service_api_base import (BaseTelehooperServiceAPI,
                                       ServiceDialogue,
                                       ServiceDisconnectReason,
                                       TelehooperServiceUserInfo)
from services.vk.exceptions import (AccessDeniedException,
                                    TokenRevokedException,
                                    TooManyRequestsException)
from services.vk.utils import (create_message_link, get_attachment_key,
                               prepare_sticker)
from services.vk.vk_api.api import VKAPI
from services.vk.vk_api.longpoll import (BaseVKLongpollEvent,
                                         LongpollMessageEditEvent,
                                         LongpollMessageFlagsEdit,
                                         LongpollNewMessageEvent,
                                         LongpollTypingEvent,
                                         LongpollTypingEventMultiple,
                                         LongpollVoiceMessageEvent,
                                         VKAPILongpoll)

if TYPE_CHECKING:
	from api import TelehooperMessage, TelehooperSubGroup, TelehooperUser

class VKServiceAPI(BaseTelehooperServiceAPI):
	"""
	Service API для ВКонтакте. Данный Service API привязан к одному пользователю, и может работать только с его аккаунтом.
	"""

	token: SecretStr
	"""Токен для доступа к API ВКонтакте."""
	vkAPI: VKAPI
	"""Объект для доступа к API ВКонтакте."""

	_cachedDialogues: list = []
	"""Кэшированный список диалогов."""
	_longPollTask: asyncio.Task | None = None
	"""Задача, выполняющая longpoll."""
	_lastOnlineStatus: int = 0
	"""UNIX-timestamp последнего обновления статуса онлайна через бота. Используется для настройки `Security.StoreTokens`."""
	_cachedUsersInfo: cachetools.TLRUCache[int, TelehooperServiceUserInfo] # 80 элементов, 5 минут хранения.
	"""Кэшированные данные о пользователях ВКонтакте для быстрого повторного получения."""

	def __init__(self, token: SecretStr, vk_user_id: int, user: "TelehooperUser", limiter: Limiter = Limiter(RequestRate(2, 1), RequestRate(20, 60))) -> None:
		super().__init__("VK", vk_user_id, user)

		self.token = token
		self.user = user

		self.vkAPI = VKAPI(self.token)

		self.limiter = limiter
		self._cachedUsersInfo = cachetools.TLRUCache(maxsize=80, ttu=lambda _, value, now: now + 5 * 60)

	async def start_listening(self, bot: Bot | None = None) -> asyncio.Task:
		async def handle_updates() -> None:
			try:
				longpoll = VKAPILongpoll(self.vkAPI, user_id=self.service_user_id)

				async for event in longpoll.listen_for_updates():
					await self.handle_longpoll_update(event)
			except TokenRevokedException as e:
				# Отправляем сообщение, если у нас есть объект бота.
				if bot:
					try:
						await bot.send_message(
							chat_id=self.user.telegramUser.id,
							text=(
								"⚠ Потеряно соединение с ВКонтакте.\n"
								"\n"
								"Telehooper потерял соединение со страницей «ВКонтакте», поскольку владелец страницы отозвал доступ к ней через настройки «Приватности» страницы.\n"
								"\n"
								"ℹ Вы можете повторно подключиться к «ВКонтакте», используя команду /connect.\n"
							)
						)
					except:
						pass

				# Совершаем отключение.
				await self.disconnect_service(ServiceDisconnectReason.ERRORED)
			except Exception as error:
				logger.exception(f"Глобальная ошибка (start_listening) обновления ВКонтакте, со связанным Telegram-пользователем {utils.get_telegram_logging_info(self.user.telegramUser)}:", error)

				# Отправляем сообщение, если у нас есть объект бота.
				if bot:
					try:
						await bot.send_message(
							chat_id=self.user.telegramUser.id,
							text=(
								"⚠ Ошибка при работе с ВКонтакте.\n"
								"\n"
								"Что-то пошло не так, и Telehooper потерял соединение с серверами сообщений «ВКонтакте», либо произошла другая необработанная ошибка.\n"
								"Если бот отказывается пересылать сообщения из ВКонтакте в Telegram, то попробуйте вручную переподключить свою страницу ВКонтакте к боту.\n"
								"\n"
								"Текст ошибки, если Вас попросили его отправить:\n"
								f"{error.__class__.__name__}: {error}.\n"
								"\n"
								f"ℹ Если проблема не проходит через время - попробуйте попросить помощи либо создать баг-репорт (Github Issue), по ссылке в команде /help."

							)
						)
					except:
						pass

		self._longPollTask = asyncio.create_task(handle_updates())
		return self._longPollTask

	async def handle_longpoll_update(self, event: BaseVKLongpollEvent) -> None:
		"""
		Метод, обрабатывающий события VK Longpoll.

		:param event: Событие, полученное с longpoll-сервера.
		"""

		logger.debug(f"[VK] Новое событие {event.__class__.__name__}: {event.event_data}")

		if type(event) is LongpollNewMessageEvent:
			await self.handle_vk_message(event)
		elif type(event) is LongpollTypingEvent or type(event) is LongpollTypingEventMultiple or type(event) is LongpollVoiceMessageEvent:
			await self.handle_vk_typing(event)
		elif type(event) is LongpollMessageEditEvent:
			await self.handle_vk_message_edit(event)
		elif type(event) is LongpollMessageFlagsEdit:
			await self.handle_vk_message_flags_change(event)
		else:
			logger.warning(f"[VK] Метод handle_update столкнулся с неизвестным событием {event.__class__.__name__}: {event.event_data}")

	async def handle_vk_message(self, event: LongpollNewMessageEvent) -> None:
		"""
		Обработчик полученных новых сообщений во ВКонтакте.

		:param event: Событие типа `LongpollNewMessageEvent`, полученное с longpoll-сервера.
		"""

		from api import TelehooperAPI

		subgroup = TelehooperAPI.get_subgroup_by_service_dialogue(self.user, ServiceDialogue(service_name=self.service_name, id=event.peer_id))

		# Проверяем, что у пользователя есть подгруппа, в которую можно отправить сообщение.
		if not subgroup:
			return

		async def handle_message_events() -> None:
			issuer_name_with_link = None
			issuer_male = True
			victim_name_with_link = None
			victim_name = True

			if event.from_id:
				issuer_info = await self.get_user_info(event.from_id)

				issuer_name_with_link = f"{utils.compact_name(issuer_info.name) if use_compact_names else issuer_info.name}"
				issuer_male = issuer_info.male or False

			if event.source_mid:
				victim_info = await self.get_user_info(event.source_mid)

				victim_name_with_link = f"{utils.compact_name(victim_info.name) if use_compact_names else victim_info.name}"
				victim_male = victim_info.male or False

			event_action = cast(str, event.source_act)

			# Во ВКонтакте, событие "X вернулся/вышел из беседы" работает как
			# "X пригласил/исключил X из беседы", поэтому здесь такая проверка.
			if event.from_id == event.source_mid and event_action in ["chat_invite_user", "chat_kick_user"]:
				event_action = "chat_return" if event_action == "chat_invite_user" else "chat_leave"

			messages = {
				"chat_photo_update": f"{issuer_name_with_link} обновил{'' if issuer_male else 'а'} фотографию беседы",
				"chat_photo_remove": f"{issuer_name_with_link} удалил{'' if issuer_male else 'а'} фотографию беседы",
				"chat_create": f"{issuer_name_with_link} создал{'' if issuer_male else 'а'} новую беседу: «{event.source_text}»",
				"chat_title_update": f"{issuer_name_with_link} изменил{'' if issuer_male else 'а'} имя беседы на «{event.source_text}»",
				"chat_invite_user": f"{issuer_name_with_link} добавил{'' if issuer_male else 'а'} пользователя {victim_name_with_link}",
				"chat_kick_user": f"{issuer_name_with_link} удалил{'' if issuer_male else 'а'} пользователя {victim_name_with_link} из беседы",
				"chat_invite_user_by_link": f"{victim_name_with_link} присоеденил{'ся' if issuer_male else 'ась'} к беседе используя пригласительную ссылку",
				"chat_invite_user_by_message_request": f"{victim_name_with_link} присоденил{'ся' if issuer_male else 'ась'} к беседе используя запрос на вступление",
				"chat_pin_message": f"{issuer_name_with_link} закрепил{'' if issuer_male else 'а'} сообщение",
				"chat_unpin_message": f"{issuer_name_with_link} открепил{'' if issuer_male else 'а'} закреплённое сообщение",
				"chat_screenshot": f"{victim_name_with_link} сделал{'' if issuer_male else 'а'} скриншот чата",
				"conversation_style_update": f"{issuer_name_with_link} обновил стиль чата",
				"chat_leave": f"{issuer_name_with_link} покинул{'' if issuer_male else 'а'} беседу",
				"chat_return": f"{issuer_name_with_link} вернул{'ся' if issuer_male else 'ась'} в беседу",
				# "call_ended": f"{victim_name_with_link} начал{'' if issuer_male else 'а'} вызов ВКонтакте. Присоедениться можно по ссылке"
			}
			message = messages.get(event_action)

			if not message:
				logger.warning(f"[VK] Неизвестное событие беседы: {event_action}")

				message = f"Неизвестное действие: «{event_action}»"

				return

			# Здесь мы не сохраняем ID сообщения, поскольку с таковыми в любом случае нельзя взаимодействовать.
			await subgroup.send_message_in(f"ℹ  {message}", disable_web_preview=True)

			# Если пользователь разрешил синхронизацию изменений в беседе, то делаем их.
			if await self.user.get_setting("Services.VK.SyncGroupInfo"):
				if event_action == "chat_title_update":
					assert event.source_text, "Новое имя беседы не было получено"

					title = event.source_text
					if config.debug and await self.user.get_setting("Debug.DebugTitleForDialogues"):
						title = f"[DEBUG] {title}"

					await subgroup.parent.set_title(title)
				elif event_action == "chat_photo_update":
					assert message_extended, "Не удалось получить информацию о сообщении"

					async with aiohttp.ClientSession() as client:
						async with client.get(message_extended["attachments"][0]["photo"]["sizes"][-1]["url"]) as response:
							assert response.status == 200, "Не удалось загрузить фотографию беседы"

							photo_bytes = await response.read()

					await subgroup.parent.set_photo(BufferedInputFile(photo_bytes, filename="VK chat photo.jpg"))
				elif event_action == "chat_photo_remove":
					await subgroup.parent.remove_photo()
				elif event_action in ["chat_pin_message", "chat_unpin_message"]:
					assert event.source_chat_local_id, "ID сообщения, которое было прикреплено не было получено"

					# Пытаемся найти полную информацию о сообщении.
					message_data = (await self.vkAPI.messages_getByConversationMessageId(peer_id=event.peer_id, conversation_message_ids=event.source_chat_local_id))["items"]

					if message_data:
						vk_message_id = message_data[0]["id"]

						telegram_message = await subgroup.service.get_message_by_service_id(self.service_user_id, vk_message_id)
						if not telegram_message:
							return

						if event_action == "chat_pin_message":
							# Открепляем старое сообщение.
							try:
								await subgroup.parent.unpin_message()
							except:
								pass

							await subgroup.parent.pin_message(telegram_message.telegram_message_ids[0])
						else:
							await subgroup.parent.unpin_message(telegram_message.telegram_message_ids[0])

		logger.debug(f"[VK] Сообщение с текстом \"{event.text}\", для подгруппы \"{subgroup.service_dialogue_name}\"")

		message_url = None
		keyboard = None
		try:
			attachment_media: list[InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo] = []
			attachment_items: list[str] = []
			use_compact_names = await self.user.get_setting("Services.VK.CompactNames")
			use_mobile_vk = await self.user.get_setting("Services.VK.MobileVKURLs")
			message_url = create_message_link(event.peer_id, event.message_id, use_mobile=use_mobile_vk)
			ignore_outbox_debug = config.debug and await self.user.get_setting("Debug.SentViaBotInform")
			is_outbox = event.flags.outbox
			is_inbox = not is_outbox
			is_group = event.peer_id < 0
			is_convo = event.peer_id > 2e9
			is_user = not is_group and not is_convo
			from_self = (not is_convo and is_outbox) or (is_convo and event.from_id and event.from_id == self.service_user_id)

			# Проверяем, стоит ли боту обрабатывать исходящие сообщения.
			if is_outbox and not (ignore_outbox_debug or await self.user.get_setting("Services.VK.ViaServiceMessages")):
				return

			# Проверяем, не было ли отправлено сообщение через бота.
			# Для этого нужно получить информацию о полученном сообщении.
			#
			# Здесь происходит очень забавная вещь:
			# На моём личном сервере, бот получает Longpoll-событие о новом сообщении ранее
			# момента получения ответа API запроса messages.send с ID отправленного сообщения.
			#
			# Выглядит это примерно так:
			# [messages.send req] <...> [longpoll new msg event] <...> [messages.send response]
			#
			# Ввиду этого, бот получает новое событие, и думает, что оно не было
			# отправлено через бота (поскольку ID сообщения ещё не был получен).
			#
			# Что бы конпенсировать это, бот вынужден проверить наличие текста сообщения в preMessageCache,
			# который пополняется текстами отправленных через бота сообщений, и если такое сообщение
			# и вправду есть, то бот считает, что это сообщение было отправлено через бота.
			# В ином случае, приходится делать небольшую задержку, что бы убедиться, что messages.send,
			# выполняемый асинхронно, вернул ID сообщения.
			#
			# Спонсор сегодняшних костылей - асинхронность.
			if event.text in subgroup.preMessageCache:
				sent_via_bot = True

				# Удаляем сообщение из кэша, поскольку проверка уже была проведена.
				subgroup.preMessageCache.pop(event.text)
			else:
				# Причина существования этого sleep описана выше.
				await asyncio.sleep(0.2)

				# Проверяем на наличие сохранённого сообщения.
				msg_saved = await subgroup.service.get_message_by_service_id(self.service_user_id, event.message_id)

				sent_via_bot = msg_saved and msg_saved.sent_via_bot

			if sent_via_bot and not ignore_outbox_debug:
				return

			# Получаем ID сообщения с ответом, а так же парсим вложения сообщения.
			reply_to = None

			# Парсим вложения.
			message_extended = None
			if event.attachments or is_group:
				attachments = event.attachments.copy()

				# Добываем полную информацию о сообщении.
				message_extended = (await self.vkAPI.messages_getById(event.message_id))["items"][0]

				# Обрабатываем ответы (reply).
				if "reply" in attachments or ("fwd_messages" in message_extended and len(message_extended["fwd_messages"]) == 1 and await self.user.get_setting("Services.VK.FWDAsReply")):
					reply_vk_message_id: int | None = message_extended["reply_message"].get("id") if "reply" in attachments else None
					fwd_vk_message_id: int | None = message_extended["fwd_messages"][0].get("id") if "reply" not in attachments else None
					fwd_vk_conversation_message_id: int | None = message_extended["fwd_messages"][0].get("conversation_message_id") if "reply" not in attachments else None

					# В некоторых случаях ID reply может отсутствовать,
					# вместо него либо ничего нет, либо есть conversation message id.
					if not (reply_vk_message_id or fwd_vk_message_id or fwd_vk_conversation_message_id):
						logger.warning(f"[VK] Пользователь сделал Reply на сообщение во ВКонтакте, однако API ВКонтакте не вернул ID сообщения, на который был сделан Reply. Сообщение: {message_extended}")
					elif fwd_vk_conversation_message_id:
						# Нам дан Conversation Message ID, ищем "реальный" ID сообщения.
						message_data = (await self.vkAPI.messages_getByConversationMessageId(event.peer_id, fwd_vk_conversation_message_id))["items"]

						if message_data:
							reply_vk_message_id = message_data[0]["id"]
					elif fwd_vk_message_id:
						# Была сделана пересылка сообщения, в таком случае используем ID данного сообщения.
						reply_vk_message_id = fwd_vk_message_id

					# Настоящий ID сообщения, на которое был дан ответ, получен. Получаем информацию о сообщении с БД бота.
					if reply_vk_message_id:
						telegram_message = await subgroup.service.get_message_by_service_id(self.service_user_id, reply_vk_message_id)

						# Если информация о данном сообщении есть, то мы можем получить ID сообщения в Telegram.
						if telegram_message:
							reply_to = telegram_message.telegram_message_ids[0]

				# Обрабатываем клавиатуру.
				if "keyboard" in message_extended:
					buttons = []

					for row in message_extended["keyboard"]["buttons"]:
						current_row = []

						for button in row:
							button_type = button["action"]["type"]

							if button_type == "text":
								current_row.append(InlineKeyboardButton(text=button["action"]["label"], callback_data=button["action"]["payload"] or "do-nothing"))
							else:
								logger.warning(f"[VK] Неизвестный тип action для кнопки: \"{button_type}\"")

								current_row.append(InlineKeyboardButton(text=f"❔ Кнопка типа {button_type}", callback_data=button["action"]["payload"] or "do-nothing"))

						buttons.append(current_row)

					keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)

				# Обрабатываем пересланные.
				if message_extended.get("fwd_messages") and not reply_to:
					fwd_messages = message_extended["fwd_messages"]

					attachment_items.append(f"🔁 {'Пересланное сообщение' if len(fwd_messages) == 1 else str(len(fwd_messages)) + ' пересланных сообщений'}")

				# Обрабатываем гео-вложения.
				if "geo" in attachments:
					attachment = message_extended["geo"]

					# Высылаем сообщение.
					msg = await subgroup.send_geo(
						latitude=attachment["coordinates"]["latitude"],
						longitude=attachment["coordinates"]["longitude"],
						silent=is_outbox,
						reply_to=reply_to
					)

					# Если произошёл rate limit, то msg будет None.
					if not msg:
						return

					await TelehooperAPI.save_message("VK", self.service_user_id, msg[0].message_id, event.message_id, False)

					return

				# Проходимся по всем вложениям.
				if message_extended and "attachments" in message_extended:
					for attch_index, attachment in enumerate(message_extended["attachments"]):
						attachment_type = attachment["type"]
						attachment = attachment[attachment["type"]]

						if attachment_type == "photo":
							# Проходимся по всем размерам фотографии и выбираем самый большой.
							sizes_sorted = sorted(attachment["sizes"], key=lambda size: size["width"] * size["height"], reverse=True)

							attachment_media.append(InputMediaPhoto(type="photo", media=sizes_sorted[0]["url"]))
						elif attachment_type == "video":
							# Так как ВК не выдают прямую ссылку на видео, необходимо её извлечь из API.
							# Что важно, передать ссылку напрямую не получается, поскольку ВК проверяет
							# UserAgent и IP адрес, с которого был сделан запрос.

							# Проверяем, видеосообщение (кружочек) ли это?
							is_video_note = attachments.get(f"attach{attch_index + 1}_kind") == "video_message"

							async with ChatActionSender(chat_id=subgroup.parent.chat.id, action="upload_video", bot=subgroup.parent.bot):
								video = (await self.vkAPI.video_get(videos=get_attachment_key(attachment)))["items"][0]
								if "files" not in video:
									# В случаях, если видео помечено как "доступно только подписчикам", ВК не даёт ссылок на скачивание.
									# В таких случаях мы просто отображаем видео как ссылку на него.

									attachment_items.append(f"📹 Видео с закрытым доступом")

									continue

								video = video["files"]

								# Если это внешнее видео (т.е., ссылка на Youtube или подобное),
								# то в таком случае прямых ссылок на файлы будут отсутствовать.
								#
								# В таком случае нужно просто добавить ссылку на видео как вложение.
								if "external" in video:
									attachment_items.append(f"📹 Внешнее видео")

									continue

								# Делаем список из возможных качеств для видео.
								video_quality_list = ["mp4_1080", "mp4_720", "mp4_480", "mp4_360", "mp4_240", "mp4_144"]
								video_hd_quality_list = ["mp4_1080"]

								# Составляем список качеств видео, которые были переданы ВКонтакте.
								present_video_quality_list = [quality for quality in video_quality_list if quality in video]

								# Узнаём, разрешены ли HD-видео.
								hd_video_allowed = await self.user.get_setting("Services.VK.HDVideo")

								for quality in present_video_quality_list:
									is_last = quality == present_video_quality_list[-1]
									is_hd = quality in video_hd_quality_list

									# Нам нужно убедиться, что если это HD видео, и есть видео другого качества,
									# то в таком случае нужно пропустить такой формат.
									if is_hd and not is_last and not hd_video_allowed:
										continue

									logger.debug(f"Найдено видео с качеством {quality}: {video[quality]}")

									# Загружаем видео.
									async with aiohttp.ClientSession() as client:
										async with client.get(video[quality]) as response:
											assert response.status == 200, f"Не удалось загрузить видео с качеством {quality}"

											audio_bytes = b""

											while True:
												chunk = await response.content.read(1024)
												if not chunk:
													break

												audio_bytes += chunk

												# Пытаемся найти самое большое видео, которое не превышает 50 МБ.
												if len(audio_bytes) > 50 * 1024 * 1024:
													if is_last:
														raise Exception("Не было найдено видео меньшего размера")

													logger.debug(f"Файл размером {quality} оказался слишком большой ({len(audio_bytes)} байт).")

													continue

									# Если мы получили видеосообщение (кружочек), то нужно отправить его как сообщение.
									if is_video_note:
										# Отправляем видеосообщение.
										msg = await subgroup.send_video_note(
											input=BufferedInputFile(audio_bytes, filename=f"VK video note {attachment['id']}.mp4"),
											silent=is_outbox,
											reply_to=reply_to
										)

										# Если произошёл rate limit, то msg будет None.
										if not msg:
											return

										# Сохраняем в память.
										await TelehooperAPI.save_message("VK", self.service_user_id, msg[0].message_id, event.message_id, False)

										assert msg[0].video_note, "Видеосообщение не было отправлено"

										return

									# Прикрепляем видео.
									attachment_media.append(InputMediaVideo(type="video", media=BufferedInputFile(audio_bytes, filename=f"{attachment['title'].strip()} {quality[4:]}p.mp4")))

									break
								else:
									raise Exception("ВКонтакте не вернул ссылку на видео")
						elif attachment_type == "audio_message":
							attachment_media.append(InputMediaAudio(type="audio", media=attachment["link_ogg"]))
						elif attachment_type == "sticker":
							# В данный момент, поддержка анимированных стикеров отсутствует из-за возможного бага в библиотеке gzip.

							is_animated = "animation_url" in attachment and False # TODO: Настройка для отключения анимированных стикеров.
							attachment_cache_name = f"sticker{attachment['sticker_id']}{'anim' if is_animated else 'static'}"

							# Пытаемся получить информацию о данном стикере из кэша вложений.
							sticker_bytes = None
							cached_sticker = await TelehooperAPI.get_attachment("VK", attachment_cache_name)

							# Если стикер был найден в кэше, то скачиваем его.
							if not cached_sticker:
								logger.debug(f"Не был найден кэш для стикера с ID {attachment_cache_name}")

								# Достаём URL анимации стикера, либо статичное изображение-"превью" этого стикера.
								sticker_url = attachment.get("animation_url") if is_animated else attachment["images"][-1]["url"]

								# Загружаем стикер.
								async with aiohttp.ClientSession() as client:
									async with client.get(sticker_url) as response:
										assert response.status == 200, f"Не удалось загрузить стикер с ID {attachment_cache_name}"

										sticker_bytes = await response.read()

								# Делаем манипуляции над стикером, если он анимированный.
								if is_animated:
									# Этот кусок кода не используется.

									sticker_bytes = await utils.convert_to_tgs_sticker(sticker_bytes)

							# Отправляем стикер.
							msg = await subgroup.send_sticker(
								sticker=cached_sticker if cached_sticker else BufferedInputFile(
									file=cast(bytes, sticker_bytes),
									filename="sticker.tgs" if is_animated else f"VK sticker {attachment['sticker_id']}.png"
								),
								silent=is_outbox,
								reply_to=reply_to
							)

							# Если произошёл rate limit, то msg будет None.
							if not msg:
								return

							assert msg[0].sticker, "Стикер не был отправлен"

							# Сохраняем в память.
							await TelehooperAPI.save_message("VK", self.service_user_id, msg[0].message_id, event.message_id, False)

							# Кэшируем стикер, если настройка у пользователя это позволяет.
							if await self.user.get_setting("Security.MediaCache"):
								await TelehooperAPI.save_attachment("VK", attachment_cache_name, msg[0].sticker.file_id)

							return
						elif attachment_type == "doc":
							async with ChatActionSender(chat_id=subgroup.parent.chat.id, action="upload_document", bot=subgroup.parent.bot):
								async with aiohttp.ClientSession() as client:
									async with client.get(attachment["url"]) as response:
										assert response.status == 200, f"Не удалось загрузить документ с ID {attachment['id']}"

										file_bytes = b""
										while True:
											chunk = await response.content.read(1024)
											if not chunk:
												break

											file_bytes += chunk

											if len(file_bytes) > 50 * 1024 * 1024:
												logger.debug(f"Файл оказался слишком большой ({len(file_bytes)} байт).")

												raise Exception("Размер файла слишком большой")

								# Прикрепляем документ.
								attachment_media.append(InputMediaDocument(type="document", media=BufferedInputFile(file=file_bytes, filename=attachment["title"])))
						elif attachment_type == "audio":
							# Обрабатываем музыку.

							# В некоторых случаях, ВК может не передавать ссылку на аудио.
							# В таком случае, бот просто прикрепит музыку как текстовое вложение.
							if not attachment.get("url"):
								attachment_items.append(f"🎵 {attachment['artist']} - {attachment['title']}")

								continue

							async with ChatActionSender(chat_id=subgroup.parent.chat.id, action="upload_audio", bot=subgroup.parent.bot):
								async with aiohttp.ClientSession() as client:
									async with client.get(attachment["url"]) as response:
										assert response.status == 200, f"Не удалось загрузить аудио с ID {attachment['id']}"

										audio_bytes = b""

										while True:
											chunk = await response.content.read(1024)
											if not chunk:
												break

											audio_bytes += chunk

											if len(audio_bytes) > 50 * 1024 * 1024:
												logger.debug(f"Файл оказался слишком большой ({len(audio_bytes)} байт).")

												raise Exception("Размер файла слишком большой")

								attachment_media.append(InputMediaAudio(
									type="audio",
									media=BufferedInputFile(
										file=audio_bytes,
										filename=f"{attachment['artist']} - {attachment['title']}.mp3"
									),
									title=attachment["title"],
									performer=attachment["artist"]
								))
						elif attachment_type == "graffiti":
							attachment_media.append(InputMediaPhoto(type="photo", media=attachment["url"]))
						elif attachment_type == "wall":
							# TODO: Имя группы/юзера откуда был пост.
							#   В данный момент почти нереализуемо из-за того, что ВК не передаёт такую информацию, и нужно делать отдельный запрос.
							# TODO: Настройка, что бы показывать содержимое поста, а не ссылку на него.

							attachment_items.append(f"🔄 Запись от {'пользователя' if attachment['owner_id'] > 0 else 'группы'}")
						elif attachment_type == "link":
							# TODO: Проверить, какая первая ссылка есть в сообщении, и если она не совпадает с этой - сделать невидимую ссылку в самом начале.

							pass
						elif attachment_type == "poll":
							attachment_items.append(f"📊 Опрос: «{attachment['question']}»")
						elif attachment_type == "gift":
							attachment_media.append(InputMediaPhoto(type="photo", media=attachment["thumb_256"]))

							attachment_items.append(f"🎁 Подарок")
						elif attachment_type == "market":
							attachment_items.append(f"🛒 Товар: «{attachment['title']}»")
						elif attachment_type == "market_album":
							pass
						elif attachment_type == "wall_reply":
							attachment_items.append(f"📝 Комментарий к записи")
						else:
							raise TypeError(f"Неизвестный тип вложения \"{attachment_type}\"")

			# Проверяем, не было ли это событие беседы из ВК.
			if is_convo and event.source_act:
				await handle_message_events()

				return

			# Подготавливаем текст сообщения, который будет отправлен.
			full_message_text = ""
			msg_prefix = ""
			msg_suffix = ""

			if from_self or is_convo:
				msg_prefix = "["

				if from_self:
					msg_prefix += "Вы"

					if sent_via_bot and ignore_outbox_debug:
						msg_prefix += " debug-пересылка"
				elif is_convo:
					assert event.from_id, "from_id не был получен, хотя должен присутствовать"

					# Получаем информацию о пользователе, который отправил сообщение.
					sent_user_info = await self.get_user_info(event.from_id)

					msg_prefix += f"{utils.compact_name(sent_user_info.name) if use_compact_names else sent_user_info.name}"

				msg_prefix += "]"

				if event.text:
					msg_prefix += ": "

			if attachment_items:
				msg_suffix += "\n\n————————\n"

				msg_suffix += "  |  ".join(attachment_items) + "."

			full_message_text = msg_prefix + utils.telegram_safe_str(event.text) + msg_suffix

			# Отправляем готовое сообщение, и сохраняем его ID в БД бота.
			async def _send_and_save() -> None:
				# Высылаем сообщение.
				#
				# К сожалению, Telegram не позволяет отправлять сообщения с одновременно
				# аудио и другими видами вложений. Что бы избежать ошибки,
				# данный код отдельно отправляет сообщение без аудио, а потом - с аудио.
				non_audio_attachments = [i for i in attachment_media if not isinstance(i, InputMediaAudio)]
				audio_attachments = [i for i in attachment_media if isinstance(i, InputMediaAudio)]
				separate_audio = non_audio_attachments and audio_attachments

				sent_message_ids = []
				msg_non_audio = await subgroup.send_message_in(
					full_message_text,
					attachments=non_audio_attachments or audio_attachments, # type: ignore
					silent=is_outbox,
					reply_to=reply_to,
					keyboard=keyboard
				)

				# Если произошёл rate limit, то ответ на отправку сообщений будет равен None.
				if not msg_non_audio:
					return

				sent_message_ids.extend(msg_non_audio)

				# Если нам нужно по-отдельности отправить аудио, то отправляем их.
				#
				# Текст сообщения передаётся только в том случае, если
				if separate_audio:
					msg_audio = await subgroup.send_message_in(
						"",
						attachments=audio_attachments, # type: ignore
						silent=is_outbox,
						reply_to=msg_non_audio[0]
					)

					if msg_audio:
						sent_message_ids.extend(msg_audio)

				# Если произошёл rate limit, то бот не сможет выслать сообщения,
				# и список из отправленных сообщений будет пуст.
				if not sent_message_ids:
					return

				await TelehooperAPI.save_message("VK", self.service_user_id, sent_message_ids, event.message_id, False)

			# Отправляем сообщения.
			# Если у сообщения есть вложения, то нам нужно сделать "печать" во время отправки такового сообщения.
			if attachment_media:
				async with ChatActionSender.upload_document(chat_id=subgroup.parent.chat.id, bot=subgroup.parent.bot, initial_sleep=1):
					await _send_and_save()
			else:
				await _send_and_save()
		except TelegramForbiddenError:
			await TelehooperAPI.delete_group_data(subgroup.parent.chat.id, fully_delete=True, bot=subgroup.parent.bot)
		except Exception as e:
			logger.exception(f"Ошибка отправки сообщения Telegram-пользователю {utils.get_telegram_logging_info(self.user.telegramUser)}:", e)

			try:
				await subgroup.send_message_in(
					(
						"⚠ Ошибка при отправке сообщения.\n"
						"\n"
						"Упс! Что-то пошло не так, и бот столкнулся с ошибкой при попытке переслать сообщение из ВКонтакте. 😓\n"
						f"Вы можете прочитать сообщение во ВКонтакте, перейдя по ссылке.\n"
						"\n"
						"Текст ошибки, если Вас попросили его отправить:\n"
						f"{e.__class__.__name__}: {e}.\n"
						"\n"
						f"ℹ Пожалуйста, подождите, перед тем как попробовать снова. Если проблема не проходит через время - попробуйте попросить помощи либо создать баг-репорт (Github Issue), по ссылке в команде /help."
					),
					silent=True,
					keyboard=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(
						text="Открыть сообщение во ВКонтакте",
						url=message_url
					)]])
				)
			except:
				pass

	async def handle_vk_typing(self, event: LongpollTypingEvent | LongpollTypingEventMultiple | LongpollVoiceMessageEvent) -> None:
		"""
		Обработчик события начала "печати" либо записи голосового сообщения во ВКонтакте.

		:param event: Событие типа `LongpollTypingEvent`, `LongpollTypingEventMultiple` или `LongpollVoiceMessageEvent`, полученное с longpoll-сервера.
		"""

		from api import TelehooperAPI

		subgroup = TelehooperAPI.get_subgroup_by_service_dialogue(self.user, ServiceDialogue(service_name=self.service_name, id=event.peer_id))

		# Проверяем, что у пользователя есть подгруппа, в которой нужно начать событие печати.
		if not subgroup:
			return

		logger.debug(f"[VK] Событие печати для подгруппы \"{subgroup.service_dialogue_name}\"")

		try:
			await subgroup.start_activity("record_audio" if type(event) is LongpollVoiceMessageEvent else "typing")
		except TelegramForbiddenError:
			await TelehooperAPI.delete_group_data(subgroup.parent.chat.id, fully_delete=True, bot=subgroup.parent.bot)

		# TODO: Если пользователей несколько, и в группе несколько Telehooper-ботов, то начать событие печати от имени разных ботов.

	async def handle_vk_message_edit(self, event: LongpollMessageEditEvent) -> None:
		"""
		Обработчик события редактирования сообщения во ВКонтакте.

		:param event: Событие типа `LongpollMessageEditEvent`, полученное с longpoll-сервера.
		"""

		from api import TelehooperAPI

		subgroup = TelehooperAPI.get_subgroup_by_service_dialogue(self.user, ServiceDialogue(service_name=self.service_name, id=event.peer_id))

		# Проверяем, что у пользователя есть подгруппа, в которую можно отправить сообщение.
		if not subgroup:
			return

		# Уродливая проверка, поскольку ВК по какой-то причине "редактирует" сообщение при его закрепе.
		#
		# В моём случае, ничего не будет происходить, если "редактирование" имеет
		# поле "pinned_at", и разница между текущим и этим временем менее 2 секунды.
		# Возможно, это не самый лучший способ, но он работает.
		if event.pinned_at and (utils.time_since(event.pinned_at)) < 2:
			return

		# Проверка на то, какой тип сообщения был отредактирован. Если отредактировано было голосовое сообщение, то пропускаем обновление.
		#
		# Понятия не имею почему, но в ВК решили, что при добавлении текста расшифровки голосового сообщения, оно будет редактироваться.
		for i in range(int(event.attachments.get("attachments_count", 0))):
			attachment_type = event.attachments.get(f"attach{i + 1}_type")
			attachment_kind = event.attachments.get(f"attach{i + 1}_kind")

			if attachment_type == "doc" and attachment_kind == "audiomsg":
				return

		logger.debug(f"[VK] Событие редактирования сообщения для подгруппы \"{subgroup.service_dialogue_name}\"")

		# Пытаемся получить ID сообщения в Telegram, которое нужно отредактировать.
		telegram_message = await subgroup.service.get_message_by_service_id(self.service_user_id, event.message_id)

		if not telegram_message:
			return

		# Если это самоуничтожающееся сообщение, которое, ну, самоуничтожилось, то просто удаляем его вместо редактирования.
		if event.is_expired:
			try:
				await subgroup.delete_message(telegram_message.telegram_message_ids)
			except TelegramForbiddenError:
				await TelehooperAPI.delete_group_data(subgroup.parent.chat.id, fully_delete=True, bot=subgroup.parent.bot)
			except Exception:
				pass

			return

		# Редактируем сообщение.
		try:
			await subgroup.edit_message(f"{event.new_text}   (ред.)", telegram_message.telegram_message_ids[0])
		except TelegramForbiddenError:
			await TelehooperAPI.delete_group_data(subgroup.parent.chat.id, fully_delete=True, bot=subgroup.parent.bot)
		except Exception:
			pass

		# TODO: При редактировании сообщения теряются префиксы и суфиксы от Telehooper.

	async def handle_vk_message_flags_change(self, event: LongpollMessageFlagsEdit) -> None:
		"""
		Обработчик события изменения флагов у уже существующего сообщения во ВКонтакте.

		:param event: Событие типа `LongpollMessageFlagsEdit`, полученное с longpoll-сервера.
		"""

		from api import TelehooperAPI

		if not event.new_flags.outbox:
			return

		if not event.new_flags.delete_for_all:
			return

		subgroup = TelehooperAPI.get_subgroup_by_service_dialogue(self.user, ServiceDialogue(service_name=self.service_name, id=event.peer_id))

		# Проверяем, что у пользователя есть подгруппа, в которую можно отправить сообщение.
		if not subgroup:
			return

		logger.debug(f"[VK] Событие удаления сообщения для подгруппы \"{subgroup.service_dialogue_name}\"")

		# Пытаемся получить ID сообщения в Telegram, которое нужно отредактировать.
		telegram_message = await subgroup.service.get_message_by_service_id(self.service_user_id, event.message_id)

		if not telegram_message:
			return

		# Удаляем сообщение.
		try:
			await subgroup.delete_message(telegram_message.telegram_message_ids)
		except TelegramForbiddenError:
			await TelehooperAPI.delete_group_data(subgroup.parent.chat.id, fully_delete=True, bot=subgroup.parent.bot)

	async def get_list_of_dialogues(self, force_update: bool = False, max_amount: int = 800, skip_ids: list[int] = []) -> list[ServiceDialogue]:
		if not force_update and self._cachedDialogues:
			return self._cachedDialogues

		extendedInfo = {}
		retrieved_dialogues = 0
		total_dialogues = 0
		result = []

		while True:
			response = await self.vkAPI.messages_getConversations(offset=retrieved_dialogues)
			if not total_dialogues:
				total_dialogues = response["count"]

			# ВК очень странно возвращает информацию о списке диалогов,
			# вместо
			#   {информации о диалоге, имя юзера и всё такое}
			# ВК возвращает
			#   {{информация о диалоге}, [{id: имя юзера и всё такое}, ...]}
			#
			# Данный кусок кода немного упрощает это, создавая словарь с информацией.
			for group in response.get("groups", []):
				extendedInfo[-group["id"]] = group

			for user in response.get("profiles", []):
				extendedInfo[user["id"]] = user

			for dialogue in response["items"]:
				convo = dialogue["conversation"]

				conversation_id = convo["peer"]["id"]
				conversation_type = convo["peer"]["type"]
				convo_extended = extendedInfo.get(conversation_id)

				# Про тип "email" можно почитать здесь:
				# https://dev.vk.com/reference/objects/message
				#
				# К сожалению, данный тип бесед имеет отличия от обычных диалогов.
				# Смысла добавлять поддержку такого типа нету, поскольку
				# такие беседы создавать уже не является возможным во ВКонтакте.
				if conversation_type == "email":
					total_dialogues -= 1

					continue

				if conversation_id in skip_ids:
					total_dialogues -= 1

					continue

				full_name = ""
				image_url = "https://vk.com/images/camera_200.png"

				if not convo_extended and conversation_type != "chat":
					raise Exception(f"Не удалось найти расширенную информацию о диалоге {conversation_id}, хотя она обязана быть.")

				if conversation_type == "chat":
					full_name = convo["chat_settings"]["title"]

					if "photo" in convo["chat_settings"]:
						image_url = convo["chat_settings"]["photo"]["photo_200"]
				elif conversation_type == "user":
					convo_extended = cast(dict, convo_extended)

					full_name = f"{convo_extended['first_name']} {convo_extended['last_name']}"
					image_url = convo_extended.get("photo_max")
				elif conversation_type == "group":
					convo_extended = cast(dict, convo_extended)

					full_name = convo_extended["name"]
					image_url = convo_extended.get("photo_max")
				else:
					raise Exception(f"Неизвестный тип диалога {conversation_type}.")

				result.append(
					ServiceDialogue(
						service_name=self.service_name,
						id=conversation_id,
						name=full_name,
						profile_url=image_url,
						is_multiuser=conversation_type == "chat",
						is_pinned=convo["sort_id"]["major_id"] > 0,
						is_muted="push_settings" in convo and convo["push_settings"]["disabled_forever"],
						incoming_messages=convo.get("unread_count", 0),
						multiuser_count=convo["chat_settings"].get("members_count") if conversation_type == "chat" else None
					)
				)

				retrieved_dialogues += 1

			if retrieved_dialogues + 200 >= total_dialogues or retrieved_dialogues + 200 >= max_amount:
				break

		self._cachedDialogues = result
		return result

	def has_cached_list_of_dialogues(self) -> bool:
		return bool(self._cachedDialogues)

	async def disconnect_service(self, reason: ServiceDisconnectReason = ServiceDisconnectReason.INITIATED_BY_USER) -> None:
		db_user = await get_user(self.user.telegramUser)

		assert "VK" in db_user["Connections"]

		try:
			if reason not in [ServiceDisconnectReason.INITIATED_BY_USER, ServiceDisconnectReason.ISSUED_BY_ADMIN]:
				raise Exception

			await self.send_message(
				chat_id=db_user["Connections"]["VK"]["ID"],
				text="ℹ Telegram-бот «Telehooper» был отключён от Вашей страницы ВКонтакте."
			)
		except:
			pass

		# Удаляем из БД.
		del db_user["Connections"]["VK"]

		await db_user.save()

		# Удаляем VKServiceAPI из памяти.
		self.user.remove_vk_connection()

		# Отключаем longpoll.
		if self._longPollTask:
			logger.debug("Cancelling longpoll task...")

			self._longPollTask.cancel()

		# Удаляем из памяти.
		try:
			del self.token
			del self.vkAPI
			del self._longPollTask
		except:
			pass

	async def get_current_user_info(self) -> TelehooperServiceUserInfo:
		self_info = await self.vkAPI.get_self_info()

		return TelehooperServiceUserInfo(
			service_name=self.service_name,
			id=self_info["id"],
			name=f"{self_info['first_name']} {self_info['last_name']}",
			profile_url=self_info.get("photo_max_orig"),
		)

	async def get_user_info(self, user_id: int, force_update: bool = False) -> TelehooperServiceUserInfo:
		"""
		Возвращает информацию о пользователе ВКонтакте.

		:param user_id: ID пользователя ВКонтакте.
		:param force_update: Нужно ли обновить информацию о пользователе, если она уже есть в кэше.
		"""

		if not force_update and user_id in self._cachedUsersInfo:
			return self._cachedUsersInfo[user_id]

		# Во ВКонтакте, пользователи имеют положительный ID,
		# пока как группы (т.е., боты) имеют отрицательный.
		#
		# Здесь, в зависимости от знака ID используются разные API-запросы.
		user_info_class: TelehooperServiceUserInfo
		if user_id > 0:
			# Пользователь.

			user_info = (await self.vkAPI.users_get(user_ids=[user_id]))[0]
			assert user_info, f"Данные о пользователе с ID {user_id} не были получены от API ВКонтакте, хотя обязаны были быть"

			user_info_class = TelehooperServiceUserInfo(
				service_name=self.service_name,
				id=user_info["id"],
				name=f"{user_info['first_name']} {user_info['last_name']}",
				profile_url=user_info.get("photo_max_orig"),
				male=user_info.get("sex", 2) == 2 # Судя по документации ВК, может быть и третий вариант с ID 0, "пол не указан". https://dev.vk.com/ru/reference/objects/user#sex
			)
		else:
			# Группа (бот).

			group_info = (await self.vkAPI.groups_getByID(user_ids=[abs(user_id)]))[0]
			assert group_info, f"Данные о группе с ID {user_id} не были получены от API ВКонтакте, хотя обязаны были быть"

			user_info_class = TelehooperServiceUserInfo(
				service_name=self.service_name,
				id=-group_info["id"],
				name=group_info["name"],
				profile_url=group_info.get("photo_200_orig"),
				male=None
			)

		self._cachedUsersInfo[user_id] = user_info_class

		return user_info_class

	async def get_service_dialogue(self, chat_id: int, force_update: bool = False) -> ServiceDialogue:
		dialogues = await self.get_list_of_dialogues(force_update=force_update)

		for dialogue in dialogues:
			if dialogue.id == chat_id:
				return dialogue

		raise TypeError(f"Диалог с ID {chat_id} не найден")

	async def set_online(self) -> None:
		await self.vkAPI.account_setOnline()

	async def start_chat_activity(self, peer_id: int, type: Literal["typing", "audiomessage"] = "typing") -> None:
		await self.vkAPI.messages_setActivity(peer_id=peer_id, type=type)

	async def read_message(self, peer_id: int) -> None:
		await self.vkAPI.messages_markAsRead(peer_id=peer_id)

	async def send_message(self, chat_id: int, text: str, reply_to_message: int | None = None, attachments: list[str] | str | None = None, latitude: float | None = None, longitude: float | None = None, bypass_queue: bool = False) -> int | None:
		if not bypass_queue and not await self.acquire_queue("message"):
			return None

		return await self.vkAPI.messages_send(peer_id=chat_id, message=text, reply_to=reply_to_message, attachment=attachments, lat=latitude, long=longitude)

	async def find_real_chat_id(self, user: "TelehooperUser", subgroup: "TelehooperSubGroup") -> int | None:
		"""
		Возвращает реальный ID беседы в ВКонтакте, если пользователь не является её создателем.

		:param user: Пользователь, для которого нужно найти реальный ID беседы.
		:param subgroup: Подгруппа, для которой нужно найти реальный ID беседы.
		"""

		logger.debug("Сообщение отправил не владелец этой группы, пытаюсь узнать ID группы относительно отправителя...")

		# TODO: Каким-то хитрым образом кэшировать этот ID?

		for chat in await self.get_list_of_dialogues():
			if not chat.is_multiuser:
				continue

			if chat.name != subgroup.service_dialogue_name:
				continue

			logger.debug(f"Найден реальный ID беседы: {chat.id}")

			return chat.id

		return None

	async def handle_telegram_message(self, msg: Message, subgroup: "TelehooperSubGroup", user: "TelehooperUser", attachments: list[PhotoSize | Video | Audio | TelegramDocument | Voice | Sticker | VideoNote]) -> None:
		from api import TelehooperAPI

		try:
			message_text = msg.text or msg.caption or ""

			logger.debug(f"[TG] Обработка сообщения в Telegram: \"{message_text}\" в \"{subgroup}\" {'с вложениями' if attachments else ''}")

			# Если это статусное сообщение, то обрабатываем его.
			if msg.left_chat_member and msg.left_chat_member.id == subgroup.parent.bot.id:
				await TelehooperAPI.delete_group_data(subgroup.parent.chat.id, fully_delete=True, bot=subgroup.parent.bot)

				return

			# Обрабатываем "ответы" на сообщение.
			reply_message_id = None
			if msg.reply_to_message:
				saved_message = await self.get_message_by_telegram_id(self.service_user_id, msg.reply_to_message.message_id)

				reply_message_id = saved_message.service_message_ids[0] if saved_message else None

			# Получаем ID беседы. Используется, если отправитель сообщения - не владелец группы.
			peer_id = subgroup.service_chat_id
			sent_by_owner = True

			if subgroup.parent.creatorID != user.telegramUser.id:
				peer_id = await self.find_real_chat_id(user, subgroup)
				sent_by_owner = False

				if not peer_id:
					return

			attachments_to_send: str | None = None
			if attachments:
				attachments_vk = cast(PhotoSize | Video | Audio | TelegramDocument | Voice | Sticker | VideoNote | str, attachments.copy())

				for attch_type in ["PhotoSize", "Video", "Audio", "Document", "Voice", "Sticker", "VideoNote"]:
					allow_multiple_uploads = attch_type != "Document"
					multiple_uploads_amount = 5 if allow_multiple_uploads else 1

					attchs_of_same_type = [attch for attch in attachments_vk if attch.__class__.__name__ == attch_type]

					if not attchs_of_same_type:
						continue

					# "Готовое" значение вложения. Если тут есть значение, то мы не должны по-новой загружать вложение.
					attachment_value: str | None = None

					# По 5 элементов.
					attachments_results: list[dict] = []
					filenames: list[str] = []
					for index in range(0, len(attchs_of_same_type), multiple_uploads_amount):
						attchs_of_same_type_part = attchs_of_same_type[index:index + multiple_uploads_amount]

						upload_url: str | None = None
						ext: str | None = None
						if attch_type == "PhotoSize":
							upload_url = (await self.vkAPI.photos_getMessagesUploadServer(peer_id=peer_id))["upload_url"]
							ext = "jpg"
						elif attch_type == "Voice":
							assert len(attachments) == 1, "Вложение типа Voice не может быть отправлено вместе с другими вложениями"

							upload_url = (await self.vkAPI.docs_getMessagesUploadServer(type="audio_message", peer_id=peer_id))["upload_url"]
							ext = "ogg"
						elif attch_type in ["Video", "VideoNote"]:
							upload_url = (await self.vkAPI.video_save(name="Video message", is_private=True, wallpost=False))["upload_url"]
							ext = "mp4"
						elif attch_type == "Sticker":
							assert len(attachments) == 1, "Вложение типа Sticker не может быть отправлено вместе с другими вложениями"

							# Проверяем в кэше.
							sticker_cache_name = f"sticker{attachments[0].file_unique_id}static"
							attachment_value = await TelehooperAPI.get_attachment("VK", sticker_cache_name)

							if not attachment_value:
								upload_url = (await self.vkAPI.docs_getMessagesUploadServer(type="graffiti", peer_id=peer_id))["upload_url"]
								ext = "png"
						elif attch_type == "Document":
							upload_url = (await self.vkAPI.docs_getMessagesUploadServer(type="doc", peer_id=peer_id))["upload_url"]

							for file_same_type in attchs_of_same_type_part:
								filenames.append(cast(TelegramDocument, file_same_type).file_name or "unknown-filename.txt")
						else:
							raise TypeError(f"Неизвестный тип вложения {attch_type}")

						logger.debug(f"URL для загрузки вложений типа {attch_type}: {upload_url}")

						# Выгружаем вложения на сервера ВК.
						if upload_url:
							assert ext or attch_type == "Document", f"Не дано расширение для вложения типа {attch_type}"

							async with aiohttp.ClientSession() as client:
								form_data = aiohttp.FormData()

								async def _download(index, file_id: str) -> tuple[int, bytes]:
									logger.debug(f"Загружаю вложение #{index} из Telegram с FileID {file_id}")

									file = await subgroup.parent.bot.download(file_id)
									assert file, "Не удалось загрузить вложение из Telegram"

									return index, file.read()

								# Подготавливаем список задач на загрузку вложений.
								tasks = []
								for index, attach in enumerate(attchs_of_same_type_part):
									attach = cast(PhotoSize | Audio | TelegramDocument | Video | Voice, attach)

									tasks.append(_download(index, attach.file_id))

								# Ожидаем загрузки, восстанавливаем преждний порядок.
								downloaded_results = await asyncio.gather(*tasks)
								downloaded_results.sort(key=lambda x: x[0])

								for index, file_bytes in downloaded_results:
									# ВКонтакте отправляет незадокументированную ошибку "no_file", если при отправке
									# документов (в т.ч. и голосовых сообщений) в FormData используется поле "file1" вместо "file".
									field_name = "file"
									if len(attchs_of_same_type_part) > 1:
										field_name = f"file{index}"

									# Если нам дан стикер, то изменяем его размера.
									if attch_type == "Sticker":
										with utils.CodeTimer("Время на изменение размера стикера: {time}"):
											file_bytes = await prepare_sticker(file_bytes)

									form_data.add_field(name=field_name, value=file_bytes, filename=f"file{index}.{ext}" if ext else filenames.pop(0))

								# Отправляем загруженные вложения на сервера ВК.
								async with client.post(upload_url, data=form_data) as response:
									assert response.status == 200, f"Не удалось загрузить вложение типа {attch_type}"
									response = VKAPI._parse_response(await response.json(content_type=None), "_get.server_")

									attachments_results.append(response)

					# Закончили отправлять все вложения пачками по 5 элементов.
					# Говорим ВК, что мы хотим отправить вложения в сообщении.
					attachment_str_list: list[str] = []

					# Если мы уже извлекли вложение из кэша, то нам нужно просто их добавить в список.
					if attachment_value:
						attachment_str_list.append(attachment_value)
					else:
						for attachment in attachments_results:
							if attch_type == "PhotoSize":
								assert attachment["photo"], "Объект photo является пустым"
								resp = await self.vkAPI.photos_saveMessagesPhoto(photo=attachment["photo"], server=attachment["server"], hash=attachment["hash"])

								for saved_attch in resp:
									attachment_str_list.append(get_attachment_key(saved_attch, type="photo"))
							elif attch_type == "Voice":
								saved_attch = (await self.vkAPI.docs_save(file=attachment["file"], title="Voice message"))["audio_message"]

								attachment_str_list.append(get_attachment_key(saved_attch, type="doc"))
							elif attch_type in ["Video", "VideoNote"]:
								attachment_str_list.append(get_attachment_key(attachment, type="video"))
							elif attch_type == "Sticker":
								saved_attch = (await self.vkAPI.docs_save(file=attachment["file"], title="Sticker"))["graffiti"]

								attachment_str = get_attachment_key(saved_attch, type="doc")
								attachment_str_list.append(attachment_str)

								# Стикеры нам нужно кэшировать, если пользователь это разрешил.
								if await self.user.get_setting("Security.MediaCache"):
									await TelehooperAPI.save_attachment("VK", f"sticker{attachments[0].file_unique_id}static", attachment_str)
							elif attch_type == "Document":
								saved_attch = (await self.vkAPI.docs_save(file=attachment["file"]))["doc"]

								attachment_str_list.append(get_attachment_key(saved_attch, type="doc"))

					# Теперь нам нужно заменить вложения в сообщении на те, что мы получили от ВК.
					for index, attch in enumerate(attachments_vk):
						if attch.__class__.__name__ != attch_type:
							continue

						attachments_vk[index] = attachment_str_list.pop(0) # type: ignore

				# Мы закончили работать с вложениями! Проверяем, что мы обработали все вложения.
				assert all(isinstance(attch, str) for attch in attachments_vk), "Не все вложения были обработаны"

				attachments_to_send = ",".join(cast(list[str], attachments_vk))

				logger.debug(f"Вложения для отправки: {attachments_to_send}")

			# Если у нас нет вложений, а так же нет текста сообщения, то мы не можем отправить сообщение.
			if not attachments_to_send and not message_text:
				return

			# Делаем статус "онлайн", если он не был обновлён в течении минуты.
			if utils.time_since(self._lastOnlineStatus) > 60 and await self.user.get_setting("Services.VK.SetOnline"):
				self._lastOnlineStatus = utils.get_timestamp()

				asyncio.create_task(self.set_online())

			# Делаем статус "печати" и прочитываем сообщение.
			if await self.user.get_setting("Services.VK.WaitToType") and len(message_text) > 3:
				# TODO: Использовать здесь execute для ускорения.
				await asyncio.gather(self.read_message(peer_id), self.start_chat_activity(peer_id))

				await asyncio.sleep(0.6 if len(message_text) <= 15 else 1)

			# Отправляем сообщение.
			#
			# Перед отправкой, мы сохраняем текст сообщения, дабы бот знал, что сообщение было и вправду отправлено через бота.
			# Пояснение: Иногда, longpoll возвращает событие о новом сообщении раньше, чем messages.send возвращает ID отправленного сообщения.
			if sent_by_owner:
				subgroup.preMessageCache[message_text] = None

			vk_message_id = await self.send_message(
				chat_id=peer_id,
				text=message_text,
				reply_to_message=reply_message_id,
				attachments=attachments_to_send,
				latitude=msg.location.latitude if msg.location else None,
				longitude=msg.location.longitude if msg.location else None
			)

			# В некоторых случаях сообщение может быть не отправлено из-за большой очереди.
			if not vk_message_id:
				return

			# Сохраняем в кэш информацию о том, что в эту же подгруппу было отправлено сообщение
			# с определённым текстом и указанным ID.
			#
			# Это нужно, что бы защититься от повторной пересылки сообщения (с префиксом "Вы").
			subgroup.preMessageCache[message_text] = vk_message_id

			# Сохраняем ID сообщения.
			await TelehooperAPI.save_message("VK", self.service_user_id, msg.message_id, vk_message_id, True)
		except TooManyRequestsException:
			await msg.reply(
				"⚠ Вы отправляете сообщения слишком быстро.\n"
				"\n"
				"Сервера ВКонтакте передали ошибку о том, что Вы отправляете слишком часто. Пожалуйста, старайтесь отправлять сообщения реже.\n"
				"\n"
				"ℹ Отправленное Вами сообщение в Telegram, вероятнее всего, будет пропущено из-за этой ошибки."
			)
		except Exception as error:
			logger.exception(f"[TG] Ошибка при пересылке Telegram-сообщения во ВКонтакте от пользователя {utils.get_telegram_logging_info(msg.from_user)}:", error)

			await msg.reply(
				"⚠ Ошибка при отправке сообщения.\n"
				"\n"
				"Упс! Что-то пошло не так, и бот столкнулся с ошибкой при попытке переслать сообщение во ВКонтакте. 😓\n"
				"\n"
				"Текст ошибки, если Вас попросили его отправить:\n"
				f"{error.__class__.__name__}: {error}.\n"
				"\n"
				f"ℹ Пожалуйста, подождите, перед тем как попробовать снова. Если проблема не проходит через время - попробуйте попросить помощи либо создать баг-репорт (Github Issue), по ссылке в команде /help."
			)

	async def handle_telegram_message_delete(self, msg: Message, subgroup: "TelehooperSubGroup", user: "TelehooperUser") -> None:
		from api import TelehooperAPI

		logger.debug(f"[TG] Обработка удаления сообщения в Telegram: \"{msg.text}\" в \"{subgroup}\"")

		saved_message = await self.get_message_by_telegram_id(self.service_user_id, msg.message_id)

		if not saved_message:
			await subgroup.send_message_in(
				"⚠ Ошибка удаления сообщения.\n"
				"\n"
				"Сообщение не было найдено ботом, поэтому оно не было удалено.",
				silent=True
			)

			return

		try:
			await self.vkAPI.messages_delete(saved_message.service_message_ids)
		except AccessDeniedException:
			# TODO: Уточнить причину ошибки в зависимости от типа диалога.

			reason = "Прошло более 24-х часов с момента отправки сообщения."
			if not saved_message.sent_via_bot:
				reason = "Вы попытались удалить сообщение, отправленное Вашим собедеседником, либо же Вы не являетесь Администратором в беседе."

			await subgroup.send_message_in(
				"⚠ Ошибка удаления сообщения.\n"
				"\n"
				f"{reason}",
				silent=True
			)

			return

		# Удаляем из кэша сообщений.
		await TelehooperAPI.delete_message(
			"VK",
			saved_message.service_message_ids
		)

	async def handle_telegram_message_edit(self, msg: Message, subgroup: "TelehooperSubGroup", user: "TelehooperUser") -> None:
		logger.debug(f"[TG] Обработка редактирования сообщения в Telegram: \"{msg.text}\" в \"{subgroup}\"")

		saved_message = await self.get_message_by_telegram_id(self.service_user_id, msg.message_id)

		if not saved_message:
			await subgroup.send_message_in(
				"⚠ Ошибка редактирования сообщения.\n"
				"\n"
				"Сообщение не было найдено ботом, поэтому оно не было отредактировано.",
				silent=True,
				reply_to=msg.message_id
			)

			return

		# Получаем ID беседы. Используется, если тот, кто отредактировал сообщение - не владелец группы.
		peer_id = subgroup.service_chat_id
		sent_by_owner = True

		if subgroup.parent.creatorID != user.telegramUser.id:
			peer_id = await self.find_real_chat_id(user, subgroup)
			sent_by_owner = False

			if not peer_id:
				return

		try:
			await self.vkAPI.messages_edit(
				message_id=saved_message.service_message_ids[0],
				peer_id=peer_id,
				message=msg.text or "[пустой текст сообщения]"
			)
		except AccessDeniedException:
			await subgroup.send_message_in(
				"⚠ Ошибка редактирования сообщения.\n"
				"\n"
				f"Сообщение слишком старое что бы его редактировать.",
				silent=True
			)

	async def handle_telegram_message_read(self, subgroup: "TelehooperSubGroup", user: "TelehooperUser") -> None:
		logger.debug(f"[TG] Обработка прочтения сообщения в Telegram в \"{subgroup}\"")

		# Получаем ID беседы. Используется, если тот, кто прочитал сообщение - не владелец группы.
		peer_id = subgroup.service_chat_id
		sent_by_owner = True

		if subgroup.parent.creatorID != user.telegramUser.id:
			peer_id = await self.find_real_chat_id(user, subgroup)
			sent_by_owner = False

			if not peer_id:
				return

		await self.read_message(peer_id)

	async def get_message_by_telegram_id(self, service_owner_id: int, message_id: int, bypass_cache: bool = False) -> Optional["TelehooperMessage"]:
		from api import TelehooperAPI

		return await TelehooperAPI.get_message_by_telegram_id("VK", message_id, service_owner_id, bypass_cache=bypass_cache)

	async def get_message_by_service_id(self, service_owner_id: int, message_id: int, bypass_cache: bool = False) -> Optional["TelehooperMessage"]:
		from api import TelehooperAPI

		return await TelehooperAPI.get_message_by_service_id("VK", message_id, service_owner_id, bypass_cache=bypass_cache)

	@staticmethod
	async def reconnect_on_restart(user: "TelehooperUser", db_user: Document, bot: Bot) -> "VKServiceAPI" | None:
		vkServiceAPI = None

		# Проверка, что токен установлен.
		# Токен может отсутствовать, если настройка Security.StoreTokens была выставлена в значение «выключено».
		if not db_user["Connections"]["VK"]["Token"]:
			# Удаляем сервис из БД.
			db_user["Connections"].pop("VK")
			await db_user.save()

			# Отправляем сообщение.
			await bot.send_message(
				chat_id=db_user["ID"],
				text=utils.replace_placeholders(
					"⚠ Потеряно соединение с ВКонтакте.\n"
					"\n"
					"Telehooper потерял соединение со страницей «ВКонтакте», поскольку настройка {{Security.StoreTokens}} была выставлена в значение «выключено».\n"
					"\n"
					"ℹ Вы можете повторно подключиться к «ВКонтакте», используя команду /connect.\n"
				)
			)

			return

		# Создаём Longpoll.
		try:
			vkServiceAPI = VKServiceAPI(
				token=SecretStr(utils.decrypt_with_env_key(db_user["Connections"]["VK"]["Token"])),
				vk_user_id=db_user["Connections"]["VK"]["ID"],
				user=user
			)
			user.save_connection(vkServiceAPI)

			# Запускаем Longpoll.
			await vkServiceAPI.start_listening(bot)

			# Возвращаем ServiceAPI.
			return vkServiceAPI
		except Exception as error:
			logger.exception(f"Не удалось запустить LongPoll для пользователя {utils.get_telegram_logging_info(user.telegramUser)}:", error)

			# В некоторых случаях, сам объект VKServiceAPI может быть None,
			# например, если не удалось расшифровать токен.
			# В таких случаях нам необходимо сделать отключение, при помощи
			# фейкового объекта VKServiceAPI.
			if vkServiceAPI is None:
				vkServiceAPI = VKServiceAPI(
					token=None, # type: ignore
					vk_user_id=db_user["Connections"]["VK"]["ID"],
					user=user
				)

			# Совершаем отключение.
			await vkServiceAPI.disconnect_service(ServiceDisconnectReason.ERRORED)

			# Отправляем сообщение.
			await bot.send_message(
				chat_id=db_user["ID"],
				text=(
					"⚠ Произошла ошибка при работе с ВКонтакте.\n"
					"\n"
					"К сожалению, ввиду ошибки бота, у Telehooper не удалось востановить соединение с Вашей страницей «ВКонтакте».\n"
					"Вы не сможете отправлять или получать сообщения из этого сервиса до тех пор, пока Вы не переподключитесь к нему.\n"
					"\n"
					"ℹ Вы можете повторно подключиться к сервису «ВКонтакте», используя команду /connect.\n"
				)
			)


from flask import Flask, request

app = Flask(__name__)

@app.route("/", methods=['GET','POST'])
def index():
    return "Welcome to the flask world!"

#Enviar um post ou get na http://url/shutdown faz o sistema dar shutdown e desligar o flask
@app.route("/shutdown", methods=['GET','POST'])
def shutdown_server():
    print("Shutdown context hit with POST!")
    shutdown = request.environ.get('werkzeug.server.shutdown')
    if shutdown is None:
        raise RuntimeError('The function is unavailable!')
    else:
        shutdown()
        return "The server is shutting down!"

def main():
    app.run(port = 8081, host = '127.0.0.1', threaded = True)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        #shutdown_server()
        print('Server is shutdown!')
    except Exception as e:
        #shutdown_server()
        print('Exception error: Server is shutdown!')


import socket
from datetime import datetime
import threading
import time
import random
import busio
import digitalio
import board
import adafruit_mcp3xxx.mcp3008 as MCP
from adafruit_mcp3xxx.analog_in import AnalogIn

#SERVER details=======================
SERVER_IP = '192.168.1.196'
SERVER_PORT = 5005
#20 is sufficiently large for our format
BUFF_SIZE = 20
#Flag for turning sending on/off
send_flag = True
#Stores time of last sample (sent with status)
lastsample = None
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#=======================

#Global rate to be sampled by the thread method
rate = 10
#Global channels to be sampled by the thread method
chan = None
chan1 = None

#======================
def main():
    global chan, chan1, rate,s,send_flag

    btn_num = 25

    #Server connection
    s.connect((SERVER_IP, SERVER_PORT))

    #HARDWARE============================================================
    # create the spi bus
    spi = busio.SPI(clock=board.SCLK, MISO=board.MISO, MOSI=board.MOSI)

    # create the cs (chip select)
    cs = digitalio.DigitalInOut(board.D5)

    #create button instance as an digital input
    btn = digitalio.DigitalInOut(board.D25)
    btn.direction = digitalio.Direction.INPUT
    btn.pull = digitalio.Pull.UP

    #Create the mcp object
    mcp = MCP.MCP3008(spi, cs)

    # create an analog input channel on pin 2 - Light sensor
    chan = AnalogIn(mcp, MCP.P2)

    # create an analog input channe on pin 1 - Temperature sensor
    chan1 = AnalogIn(mcp, MCP.P1)

    #THREADING===========================================================
    #Start thread with dedicated method, no args
    thread = threading.Thread(target=sample_thread)
    thread.daemon = True
    thread.start()

    #RECEIVING==============================================================
    while True:
        #Wait to receive server responses
        server_msg = s.recv(BUFF_SIZE)
        #Decode responses to text
        server_msg = server_msg.decode('utf-8')
        #Retreive command character
        cmd = server_msg[0]

        if(cmd == "O"):
            #O - turn sending/sensor on
            send_flag = True
        elif(cmd == "F"):
            #F - turn sending/sensor off
            send_flag = False
        elif(cmd == "K"):
            #K - call method to return current status
            sendstatus()
        elif(cmd == "X"):
            #X - server closing -> client closes
            print("Client exiting...")
            quit()
    #====================================================================

def sample_thread():
    #Thread method to sample values and send data over TCP repetitively
    while(True):
        lgt = chan.value
        tmp = round(chan1.voltage*100)
        #Send sampled values if flag enabled (sensor 'on')
        if(send_flag):
            senddata(lgt,tmp)

        #Wait for 10s
        time.sleep(rate)

#SENDING=================================================================
#Send state of send_flag variable (sensor status) and time of last sample
def sendstatus():
    #T - message code of 'STATUS' message
    char = "T"
    #Default value if no previous sample
    if lastsample == None: message = str(send_flag) + "#XX:XX:XX"
    else: message = str(send_flag) + "#" + lastsample
    length = len(message)
    message = char + " " + str(length) + " " + message
    #Message format: <cmd> <length> <flag>#<time>
    s.send(str.encode(message))

def senddata(lgt,tmp):
    global lastsample
    #S - message code of 'SENSOR' data message
    char = "S"
    message = str(lgt) + "#" + str(tmp)
    length = len(message)
    message = char + " " + str(length) + " " + message
    #Message format: <cmd> <length> <light>#<temp>
    s.send(str.encode(message))

    #Note time as the new latest sample
    lastsample = str(datetime.now().time())[0:8]
#==================================================================

if __name__ == '__main__':
    main()


import tensorflow as tf
from keras.optimizer_v2 import optimizer_v2
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import control_flow_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.ops import state_ops
class AccAdam(optimizer_v2.OptimizerV2):
  def __init__(self,
               accumulation_steps=1,
               learning_rate=0.001,
               beta_1=0.9,
               beta_2=0.999,
               epsilon=1e-7,
               name='AccAdam',
               **kwargs):
    super(AccAdam, self).__init__(name, **kwargs)
    self._set_hyper('accumulation_steps', accumulation_steps)
    self._set_hyper('learning_rate', kwargs.get('lr', learning_rate))
    self._set_hyper('decay', self._initial_decay)
    self._set_hyper('beta_1', beta_1)
    self._set_hyper('beta_2', beta_2)
    self._set_hyper('epsilon', epsilon)

  def _create_slots(self, var_list):
    for var in var_list: self.add_slot(var, 'm')
    for var in var_list: self.add_slot(var, 'v')
    for var in var_list: self.add_slot(var, 'g')

  def _prepare_local(self, var_device, var_dtype, apply_state):
    super(AccAdam, self)._prepare_local(var_device, var_dtype, apply_state)

    accumulation_steps = self._get_hyper('accumulation_steps', self.iterations.dtype)

    update_cond = tf.equal((self.iterations + 1) % accumulation_steps, 0)
    sub_step = self.iterations % accumulation_steps + 1
    local_step = math_ops.cast(self.iterations // accumulation_steps + 1, var_dtype)

    learning_rate = array_ops.identity(self._get_hyper('learning_rate', var_dtype))
    epsilon = array_ops.identity(self._get_hyper('epsilon', var_dtype))
    beta_1_t = array_ops.identity(self._get_hyper('beta_1', var_dtype))
    beta_2_t = array_ops.identity(self._get_hyper('beta_2', var_dtype))

    beta_1_power = math_ops.pow(beta_1_t, local_step)
    beta_2_power = math_ops.pow(beta_2_t, local_step)
    lr = learning_rate * math_ops.sqrt(1-beta_2_power)/(1-beta_1_power)

    apply_state[(var_device, var_dtype)].update(
        dict(
            lr=lr,
            update_cond=update_cond,
            sub_step=sub_step,
            epsilon=epsilon,
            beta_1_t=beta_1_t,
            beta_1_power=beta_1_power,
            one_minus_beta_1_t=1 - beta_1_t,
            beta_2_t=beta_2_t,
            beta_2_power=beta_2_power,
            one_minus_beta_2_t=1 - beta_2_t))

  def _resource_apply_dense(self, grad, var, apply_state=None):
    var_device, var_dtype = var.device, var.dtype.base_dtype
    coefficients = ((apply_state or {}).get((var_device, var_dtype))
                    or self._fallback_apply_state(var_device, var_dtype))

    accumulation_steps = self._get_hyper('accumulation_steps', self.iterations.dtype)
    update_cond = coefficients['update_cond']
    sub_step = coefficients['sub_step']
    eps = coefficients['epsilon']

    m = self.get_slot(var, 'm')
    v = self.get_slot(var, 'v')
    g = self.get_slot(var, 'g')

    grad = grad / math_ops.cast(accumulation_steps, var_dtype)
    g_t = tf.where(tf.equal(sub_step,1), grad, g + (grad-g)/math_ops.cast(sub_step, var_dtype))
    m_t = tf.where(update_cond, m * coefficients['beta_1_t'] + g_t * coefficients['one_minus_beta_1_t'], m)
    v_t = tf.where(update_cond, v * coefficients['beta_2_t'] + (g_t * g_t) * coefficients['one_minus_beta_2_t'], v)
    var_t = tf.where(update_cond, coefficients['lr'] * m_t/(math_ops.sqrt(v_t) + eps), tf.zeros_like(grad))

    var_update = state_ops.assign_sub(var, var_t, use_locking=self._use_locking)
    m_update = state_ops.assign(m, m_t, use_locking=self._use_locking)
    v_update = state_ops.assign(v, v_t, use_locking=self._use_locking)
    g_update = state_ops.assign(g, g_t, use_locking=self._use_locking)
    return control_flow_ops.group(*[var_update, m_update, v_update, g_update])

  def get_config(self):
    config = super(AccAdam, self).get_config()
    config.update({
        'accumulation_steps': self._serialize_hyperparameter('accumulation_steps'),
        'learning_rate': self._serialize_hyperparameter('learning_rate'),
        'decay': self._serialize_hyperparameter('decay'),
        'beta_1': self._serialize_hyperparameter('beta_1'),
        'beta_2': self._serialize_hyperparameter('beta_2'),
        'epsilon':self._serialize_hyperparameter('epsilon'),
    })
    return config


#!/usr/bin/env python
from create_nrPDB_GO_annot import read_fasta
from biotoolbox.structure_file_reader import build_structure_container_for_pdb
from biotoolbox.contact_map_builder import DistanceMapBuilder
from Bio.PDB import PDBList

import numpy as np
import argparse
import time

def make_distance_maps(pdbfile, chain=None, sequence=None):
    pdb_handle = open(pdbfile, 'r')
    structure_container = build_structure_container_for_pdb(pdb_handle.read(), chain).with_seqres(sequence)

    mapper = DistanceMapBuilder(atom="CA", glycine_hack=-1)
    ca = mapper.generate_map_for_pdb(structure_container)
    cb = mapper.set_atom("CB").generate_map_for_pdb(structure_container)
    pdb_handle.close()

    return ca.chains, cb.chains

import csv
from collections import defaultdict

def load_GO_annot(filename):
    onts = ['molecular_function', 'biological_process', 'cellular_component']
    prot2annot = {}
    goterms = {ont: [] for ont in onts}
    gonames = {ont: [] for ont in onts}
    with open(filename, mode='r') as tsvfile:
        reader = csv.reader(tsvfile, delimiter='\t')

        for ont in onts:
            next(reader, None)
            goterms[ont] = next(reader)
            next(reader, None)
            gonames[ont] = next(reader)

        next(reader, None)
        for row in reader:
            prot, prot_goterms = row[0], row[1:]
            prot2annot[prot] = {ont: [] for ont in onts}
            for i in range(3):
                prot2annot[prot][onts[i]] = [goterm for goterm in prot_goterms[i].split(',') if goterm != '']
    return prot2annot, goterms, gonames

def retrieve_pdb(pdb, chain, chain_seqres, pdir):
    pdb_list = PDBList()
    pdb_list.retrieve_pdb_file(pdb, pdir=pdir)
    ca, cb = make_distance_maps(pdir + '/' + pdb+'.cif', chain=chain, sequence=chain_seqres)
    return ca[chain]['contact-map'], cb[chain]['contact-map']

def load_list(fname):
    pdb_chain_list = set()
    fRead = open(fname, 'r')
    for line in fRead:
        pdb_chain_list.add(line.strip())
    fRead.close()
    return pdb_chain_list

def write_annot_npz(prot, prot2seq, out_dir):
    pdb, chain = prot.split('-')
    print('pdb=', pdb, 'chain=', chain)
    try:
        A_ca, A_cb = retrieve_pdb(pdb.lower(), chain, prot2seq[prot], pdir=out_dir + 'tmp_PDB_files_dir')
        np.savez_compressed(out_dir + '/' + prot,
                            C_alpha=A_ca,
                            C_beta=A_cb,
                            seqres=prot2seq[prot],
                            )
    except Exception as e:
        print(e)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-annot', type=str, default='./data/nrPDB-GO_annot.tsv', help="Input file (*.tsv) with preprocessed annotations.")
    parser.add_argument('-seqres', type=str, default='./data/pdb_seqres.txt.gz', help="PDB chain seqres fasta.")
    parser.add_argument('-num_threads', type=int, default=20, help="Number of threads (CPUs) to use in the computation.")
    parser.add_argument('-bc', type=str, default='./data/clusters-by-entity-95.txt', help="Clusters of PDB chains computed by Blastclust.")
    parser.add_argument('-out_dir', type=str, default='./data/annot_pdb_chains_npz/', help="Output directory with distance maps saved in *.npz format.")
    args = parser.parse_args()

    prot2goterms, _, _ = load_GO_annot(args.annot)
    print ("### number of annotated proteins: %d" % (len(prot2goterms)))

    prot2seq = read_fasta(args.seqres)
    print ("### number of proteins with seqres sequences: %d" % (len(prot2seq)))

    import glob
    npz_pdb_chains = glob.glob(args.out_dir + '*.npz')
    npz_pdb_chains = [chain.split('/')[-1].split('.')[0] for chain in npz_pdb_chains]

    to_be_processed = list(prot2goterms.keys())
    to_be_processed = list(set(to_be_processed).difference(npz_pdb_chains))
    print ("Number of pdbs to be processed=", len(to_be_processed))
    print (to_be_processed)

    nprocs = args.num_threads
    out_dir = args.out_dir
    import multiprocessing
    nprocs = min(nprocs, multiprocessing.cpu_count())

    if nprocs > 4:
        pool = multiprocessing.Pool(processes=nprocs)
        pool.starmap(write_annot_npz, zip(to_be_processed, [prot2seq]*len(to_be_processed), [out_dir]*len(to_be_processed)))
    else:
        for prot in to_be_processed:
            write_annot_npz(prot, prot2seq, out_dir)


import requests
import json
import time
import os
import shutil
import random

from utils import read_file, walk_dir, log_notify

class WallSetter:
    def __init__(self, api_key):
        self.api_key = api_key
        self.files = []
        self.image_dict = {}
        self.max_retries = 5
        self.curr_retries = 1
        self.output_file = os.path.join(os.path.dirname(__file__), "../download.txt")
        self.paths = [
            "../../../../../mnt/Files/Important Files/Wallpapers/2020-6-10/",
            "../../../../../mnt/Files/Important Files/Wallpapers/Anime Wallpapers/My Walls",
            "../../../../../home/genuine/.config/wallhaven_pics/",
        ]

    def request(self, request_url: str, retry=False):
        if retry:
            self.curr_retries += 1
            if self.curr_retries > self.max_retries:
                print("Max retries reached")
                return

        r = requests.get(request_url)

        if r.status_code == 429:
            print("Rate limit reached. Sleeping for 1 minute")
            time.sleep(60)

            print("Retrying")
            self.request(request_url, retry=True)

        elif r.status_code == 200:
            data = json.loads(r.content)
            image_urls = [value.get("path") for value in data.get("data")]

            print(f"Total images: {len(image_urls)}")

            for image in image_urls:
                image_id = image.split("/")[-1]
                if image_id not in self.files:
                    self.image_dict[image_id] = image

        else:
            print("Request failed with status code: " + r.status_code)

    def generate_wallpaper_index(self, paths: list[str]):
        file_list = []

        for path in paths:
            files = walk_dir(path)
            file_list.extend(files)

        return list(set(file_list))

    def generate_wallhaven_index(self, pages: int):
        self.files = self.generate_wallpaper_index(self.paths)

        for i in range(1, pages + 1):
            print("Scanning page: " + str(i))

            request_url = f"https://wallhaven.cc/api/v1/search?q=@rootkit&categories=010&purity=111&atleast=1920x1080&sorting=views&order=desc&ratios=landscape&ai_art_filter=1&page={i}&apikey={self.api_key}"
            self.request(request_url)

        self.write_index(self.output_file)

    def write_index(self, filename):
        with open(filename, mode="w") as file:
            for id, value in self.image_dict.items():
                if id not in self.files:
                    file.write(f"{id}:{value}\n")

    def get_random_wallpaper(self) -> str:
        images = read_file(self.output_file)

        while True:
            counter = 0
            idx = random.randint(0, len(images))
            image = images[idx]
            image_id = image.split(":", 1)[0]
            if image_id not in self.files:
                return image
            else:
                counter += 1
                log_notify("Skipping wallpaper" + image_id)

                if counter > 100:
                    log_notify("Many files in downloads.txt are already downloaded")
                    break

        return "Invalid"

    def download_and_save_wallpaper(self, image: str):
        image_id, url = image.split(":", 1)

        self.files = self.generate_wallpaper_index(self.paths)

        home = os.path.expanduser("~")
        file = f"{home}/.config/wallhaven_pics/{image_id}"

        if image_id not in self.files:
            self.request_wallpaper(url, file, image_id)
        else:
            log_notify(f"Skipping download of" + image_id)

        return file

    def request_wallpaper(self, url: str, path: str, image_id: str):
        max_tries = 5

        for i in range(0, max_tries):
            print("Downloading image:" + image_id)

            r = requests.get(
                f"{url}?apikey={self.api_key}",
                stream=True,
                headers={
                    "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
                },
            )

            if r.status_code == 200:
                with open(path, "wb+") as f:
                    r.raw.decode_content = True
                    shutil.copyfileobj(r.raw, f)

                    return
            elif r.status_code == 429:
                print("Rate limit reached. Sleeping for 1 minute")
                time.sleep(60)

                print("Retrying")
            else:
                print(f"Failed request with code: {r.status_code}")
                return

        print("Max retries reached. Skipping wallpaper")

    def set_wallpaper(self, file: str):
        os.system(f"wal -i {file} && qtile cmd-obj -o cmd -f reload_config")

    def bulk_image_download(self):
        home = os.path.expanduser("~")
        download_path = f"{home}/.config/wallhaven_pics/"

        for image_id, url in self.image_dict.items():
            if image_id not in self.files:
                self.request_wallpaper(url, f"{download_path}{image_id}", image_id)


import torch
import torch.nn as nn
import numpy as np
from torch.distributions.multivariate_normal import MultivariateNormal
from torch.distributions.gamma import Gamma
from torch.distributions.categorical import Categorical
from torch.distributions.bernoulli import Bernoulli
import torch.optim as optim
from sklearn.manifold import MDS
from sklearn.cluster import KMeans
from helper import *
from sklearn.model_selection import KFold
from scipy.special import logit
from plot_helper import *

class Model(nn.Module):
    def __init__(self, met_locs, microbe_locs, temp_grouper = 1, temp_selector = 1, L = 2, K = 2, beta_var = 10, c_var = 1,
                 mu_var_met = 10, mu_var_bug = 10, r_scale_met = 10, r_scale_bug = 10, seed = 0):
        super(Model, self).__init__()
        self.L = L
        self.K = K
        self.beta_var = beta_var
        self.c_var = c_var
        self.mu_var_met = mu_var_met
        self.mu_var_bug = mu_var_bug
        self.r_scale_met = r_scale_met
        self.r_scale_bug = r_scale_bug
        self.temp_grouper = temp_grouper
        self.temp_selector = temp_selector
        self.met_locs = met_locs
        self.microbe_locs = microbe_locs
        self.embedding_dim = met_locs.shape[1]
        self.seed = seed
        self.initialize(self.seed)

    def initialize(self, seed):
        torch.manual_seed(seed)
        self.range_dict = {}
        self.beta = nn.Parameter(torch.normal(0, self.beta_var, size = (self.L+1, self.K), requires_grad=True))
        sampler = torch.normal(0, self.beta_var, size = [100])
        self.range_dict['beta'] = (sampler.min(), sampler.max())
        self.alpha = nn.Parameter(Bernoulli(0.5).sample([self.K, self.L]), requires_grad=True)
        self.range_dict['alpha'] = (-0.1, 1.1)
        mv_normal = MultivariateNormal(torch.zeros(self.embedding_dim), self.mu_var_met*torch.eye(self.embedding_dim))
        self.range_dict['mu_met'] = (mv_normal.sample([100]).min(), mv_normal.sample([100]).max())
        self.range_dict['mu_bug'] = (mv_normal.sample([100]).min(), mv_normal.sample([100]).max())
        self.mu_met = nn.Parameter(mv_normal.sample(sample_shape = torch.Size([self.K])),
                                        requires_grad = True)
        self.mu_bug = nn.Parameter(mv_normal.sample(sample_shape = torch.Size([self.L])),
                                        requires_grad = True)

        range_x = np.max(self.met_locs[:,0]) - np.min(self.met_locs[:,0])
        range_y = np.max(self.met_locs[:,1]) - np.min(self.met_locs[:,1])
        euc = np.sqrt(range_x**2 + range_y**2)
        self.r0_met = euc / self.K
        # r0_met = self.met_locs[np.random.choice(np.arange(self.met_locs.shape[0]), size = self.K),:]
        gamma = Gamma(self.r_scale_met/2, torch.tensor((self.r_scale_met*(self.r0_met)))/2)
        r_temp = gamma.sample([self.K])
        self.range_dict['r_met'] = (0, (1/gamma.sample([100])).max())
        self.r_met = nn.Parameter(1/r_temp, requires_grad = True)

        range_x = np.max(self.microbe_locs[:,0]) - np.min(self.microbe_locs[:,0])
        range_y = np.max(self.microbe_locs[:,1]) - np.min(self.microbe_locs[:,1])
        euc = np.sqrt(range_x**2 + range_y**2)
        self.r0_bug = euc / self.K
        gamma = Gamma(self.r_scale_bug/2, torch.tensor((self.r_scale_bug*(self.r0_bug))/2))
        self.range_dict['r_bug'] = (0, (1/gamma.sample([100])).max())
        r_temp = gamma.sample([self.L])
        self.r_bug = nn.Parameter(1/r_temp, requires_grad=True)

        self.e_bug = []
        len_st = self.microbe_locs.shape[0]
        for i in range(self.L-1):
            samp = np.random.choice(np.arange(len_st - self.L),1)
            self.e_bug.append(samp[0])
            len_st = len_st - samp[0]
        self.e_bug.append(len_st)
        self.e_met = []
        len_st = self.met_locs.shape[0]
        for i in range(self.K-1):
            samp = np.random.choice(np.arange(len_st-self.K),1)
            self.e_met.append(samp[0])
            len_st = len_st - samp[0]
        self.e_met.append(len_st)
        self.pi_bug = nn.Parameter(torch.Tensor(st.dirichlet(self.e_bug).rvs()), requires_grad=True)
        self.pi_met = nn.Parameter(torch.Tensor(st.dirichlet(self.e_met).rvs()), requires_grad=True)
        self.range_dict['pi_bug'] = (0,1)
        self.range_dict['pi_met'] = (0,1)

        cat_bug = Categorical(self.pi_bug).sample([self.microbe_locs.shape[0]])
        self.w = nn.Parameter(nn.functional.one_hot(cat_bug.squeeze(), num_classes = self.L).type(torch.FloatTensor), requires_grad=True)
        self.range_dict['w'] = (-0.1,1.1)

        cat_met = Categorical(self.pi_met).sample([self.met_locs.shape[0]])
        self.z = nn.Parameter(nn.functional.one_hot(cat_met.squeeze(), num_classes = self.K).type(torch.FloatTensor), requires_grad=True)
        self.range_dict['z'] = (-0.1,1.1)

    def forward(self, x):
        # Transform z and w with softmax to be on simplex
        # Will have to do this with the prior anyways
        # Add alpha to simplex
        self.alpha_act = torch.sigmoid(self.alpha/self.temp_selector)
        self.w_act = torch.softmax(self.w / self.temp_grouper, 1)
        self.z_act = torch.softmax(self.z / self.temp_grouper, 1)

        g = torch.matmul(torch.Tensor(x), self.w_act.float())
        # K
        out_clusters = self.beta[0,:] + torch.matmul(g, self.beta[1:,:]*self.alpha_act)
        out = torch.matmul(out_clusters, self.z_act.float().T)
        return out

# Clusters need to be not correlated to eachother - if too similar to eachother, correlation in the data
# Linear relationship is with sums - i.e. aggregation of clusters and metabolites
# All y_j's in the same cluster will have basically the same estimation
# 2 clusters metabolites, 2 clusters microbes
# Make sure betas are separate enough
# Toy example where the sums of x create y
# Main thing - do we get the clusters right?
def generate_synthetic_data(N_met = 20, N_bug = 15, N_samples = 200, N_met_clusters = 2, N_bug_clusters = 2, state = 1,
                            beta_var = 10):
    np.random.seed(state)
    dist_met = np.random.randint(1,10,size = (N_met, N_met))
    dist_met = (dist_met.T@dist_met)/15
    np.fill_diagonal(dist_met, 0)

    dist_bug = np.random.randint(1,10,size = (N_bug, N_bug))
    dist_bug = (dist_bug.T@dist_bug)/15
    np.fill_diagonal(dist_bug, 0)

    embedding = MDS(n_components=2, dissimilarity='precomputed', random_state=state)
    met_locs = embedding.fit_transform(dist_met)
    embedding = MDS(n_components=2, dissimilarity='precomputed', random_state=state)
    bug_locs = embedding.fit_transform(dist_bug)

    km_met = KMeans(n_clusters = N_met_clusters, random_state=state)
    kmeans_met = km_met.fit(met_locs)
    u_met_gen = kmeans_met.cluster_centers_
    z_gen = np.array([get_one_hot(kk, l = N_met_clusters) for kk in kmeans_met.labels_])

    km_bug = KMeans(n_clusters = N_bug_clusters, random_state=state)
    kmeans_bug = km_bug.fit(bug_locs)
    u_bug_gen = kmeans_bug.cluster_centers_
    w_gen = np.array([get_one_hot(kk, l = N_bug_clusters) for kk in kmeans_bug.labels_])

    betas = np.random.normal(0, np.sqrt(beta_var), size = (N_bug_clusters+1, N_met_clusters))
    # betas = np.array([[0,0],[1,0],[0,1]])
    alphas = st.bernoulli(0.5).rvs((N_bug_clusters, N_met_clusters))
    # alphas = np.array([[1,1],[1,1]])
    cluster_means = np.random.choice(np.arange(np.int(-N_bug_clusters*2), np.int(N_bug_clusters*2)+1, 4), N_bug_clusters, replace = False)
    X = np.zeros((N_samples, N_bug))
    for i in range(N_bug_clusters):
        ixs = np.where(w_gen[:,i]==1)[0]
        X[:, ixs] = np.random.normal(cluster_means[i], 1, size = (N_samples, len(ixs)))
    g = X@w_gen
    y = (betas[0,:] + g@(betas[1:,:]*alphas))@z_gen.T
    return X, y, betas, alphas, w_gen, z_gen, bug_locs, met_locs, kmeans_bug, kmeans_met

if __name__ == "__main__":
    params2learn = ['w']
    n_splits = 2
    path = 'results_ML/learn_' + '_'.join(params2learn) + '/'
    if not os.path.isdir(path):
        os.mkdir(path)
    x, y, gen_beta, gen_alpha, gen_w, gen_z, gen_bug_locs, gen_met_locs, kmeans_bug, kmeans_met = generate_synthetic_data()
    r_bug = [np.max([np.sqrt(np.sum((kmeans_bug.cluster_centers_[i,:] - l)**2)) for l in gen_bug_locs[gen_w[:,i]==1,:]]) for i in
             range(kmeans_bug.cluster_centers_.shape[0])]
    r_met = [np.max([np.sqrt(np.sum((kmeans_met.cluster_centers_[i,:] - l)**2)) for l in gen_met_locs[gen_z[:,i]==1,:]]) for i in
             range(kmeans_met.cluster_centers_.shape[0])]
    true_vals = {'y':y, 'beta':gen_beta, 'alpha':gen_alpha, 'mu_bug': kmeans_bug.cluster_centers_, 'mu_met': kmeans_met.cluster_centers_,
                 'r_bug': r_bug, 'r_met': r_met, 'z': gen_z, 'w': gen_w, 'pi_met':np.sum(gen_z,0)/np.sum(np.sum(gen_z)),
                 'pi_bug':np.sum(gen_w,0)/np.sum(np.sum(gen_w))}
    plot_syn_data(path, x, y, gen_w, gen_z, gen_bug_locs, gen_met_locs)

    dataset = (x,y)
    net = Model(gen_met_locs, gen_bug_locs, L = gen_w.shape[1], K = gen_z.shape[1])

    kfold = KFold(n_splits = n_splits, shuffle = True)
    iterations = 10000

    train_x = x
    test_out_vec = []
    train_out_vec = []
    loss_vec = []
    test_loss = []
    fig_dict = {}
    ax_dict = {}
    fig_dict2, ax_dict2 = {},{}
    fig_dict3, ax_dict3 = {},{}
    for fold, (train_ids, test_ids) in enumerate(kfold.split(x)):
        param_dict = {}
        net.initialize(net.seed)
        for name, parameter in net.named_parameters():
            if name not in params2learn and 'all' not in params2learn:
                print(name)
                setattr(net, name, nn.Parameter(torch.Tensor(true_vals[name]), requires_grad=False))
                parameter = nn.Parameter(torch.Tensor(true_vals[name]), requires_grad=False)
            param_dict[name] = [parameter.clone().detach().numpy()]
        criterion = nn.L1Loss()
        optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9)
        x_train, targets = x[train_ids,:], y[train_ids]
        x_test, test_targets = x[test_ids,:], y[test_ids]
        loss_vec = []
        test_loss = []
        running_loss = 0.0
        for epoch in range(iterations):
            optimizer.zero_grad()
            outputs = net(x_train)
            loss = criterion(outputs, torch.Tensor(targets))
            loss.backward()
            optimizer.step()

            loss_vec.append(loss.item())
            for name, parameter in net.named_parameters():
                if name == 'w' or name == 'z' or name == 'alpha':
                    parameter = getattr(net, name + '_act')
                elif name == 'r_bug' or name == 'r_met':
                    parameter = torch.exp(parameter)

                param_dict[name].append(parameter.clone().detach().numpy())

            if epoch%1000 == 0 and epoch != 0:
                print(epoch)
                # train_out_vec.append(outputs)
                fig_dict2[fold], ax_dict2[fold] = plt.subplots(y.shape[1], 1, figsize=(8, 4 * y.shape[1]))
                fig_dict3[fold], ax_dict3[fold] = plt.subplots(gen_z.shape[1], 1, figsize=(8, 4 * gen_z.shape[1]))
                plot_param_traces(path, param_dict, params2learn, true_vals, net, fold)
                plot_output(path, test_loss, test_out_vec, test_targets, gen_z, fig_dict2, ax_dict2, fig_dict3,
                            ax_dict3, fold)

            if epoch%100==0:
                train_out_vec.append(outputs)
                with torch.no_grad():
                    test_out = net(x_test)
                    test_out_vec.append(test_out)
                    test_loss.append(criterion(test_out, torch.Tensor(test_targets)))
                if epoch != 0:
                    fig3, ax3 = plt.subplots(figsize=(8, 4 * n_splits))
                    fig3, ax3 = plot_loss(fig3, ax3, fold, epoch+1, loss_vec, test_loss)
                    fig3.tight_layout()
                    fig3.savefig(path + 'loss_fold_' + str(fold) + '.pdf')
                    plt.close(fig3)

    #     if fold == 0:
    #         plot_param_traces(path, param_dict, params2learn, true_vals, net, fig_dict, ax_dict)
    #
    #     plot_output(path, test_loss, test_out_vec, test_targets, gen_z, fig_dict2, ax_dict2, fig_dict3, ax_dict3, fold)
    #
    #     fig3, ax3 = plot_loss(fig3, ax3, fold, iterations, loss_vec, test_loss)
    # fig3.tight_layout()
    # fig3.savefig(path + 'train_test_loss.pdf')


from tkinter import *
from tkinter import ttk
import pyttsx3
import random
from lists import *

def shakespeare_insult():
    engine = pyttsx3.init('espeak')
    coin = random.randint(1, 2)

    if coin ==1:
        ins=str("You " + random.choice(s_verb)+" "+ random.choice(s_adjective)+" " + random.choice(s_noun) + "!")
        engine.say(ins)
    elif coin == 2:
        ins=str("You " + random.choice(s_adverb)+" "+ random.choice(s_verb)+" " + random.choice(s_noun) + "!")
        engine.say(ins)
    engine.runAndWait()

root= Tk()
root.title("Insult Generator")
root.geometry("330x330")

gen_ins_button= Button(root, text="Shakesperean Insult", command=shakespeare_insult)
gen_ins_button.grid(row=0, column=0, padx=10, pady=10,sticky=N, ipadx=70)

root.mainloop()


from menu import MainQueriesThread, prepare_messages
from observer import Observer, Event
from common import Logger

def main():
    logger = Logger("logs.txt")

    Observer("add", logger.print_to_file)
    Observer("remove", logger.print_to_file)

    Event.add_observer(Observer)

    menu_messages = prepare_messages("lang_uk.json")
    queries_thread_object = MainQueriesThread(menu_messages)
    queries_thread_object.run_thread()

if __name__ == "__main__":
    main()


from Eurecom import *
from pca                     import *
import argparse
from helper.functions import standartParametrization

if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='Process and extract EURECOM with PCA')
    parser = standartParametrization(parser)
    args = parser.parse_args()

    faceDataset = []
    sets = ['s1','s2']
    for s in sets:
        ek = EurecomKinect(args.pathdatabase,s,args.typeoffile,args.faceVariation.split('_'))
        ek.feedTemplates()

        if args.loadNewDepth:
            ek.loadNewDepthImage()
        elif args.loadSymmImages:
            ek.loadSymmFilledImages()

        if args.angles:
            ek.loadRotatedFaces(args.angles.split('_'),args.axis.split('_'))

        faceDataset.append(ek)

    pcaFeature = PCAImpl(faceDataset,args.pathImages)
    pcaFeature.featureExtraction()