my_flask_app/
├── tg/
│   ├── __init__.py
from .bot import start_polling
│   ├── bot.py
from os import getenv
from dotenv import load_dotenv

from aiogram.types import Message, PollAnswer
from aiogram import Dispatcher, F, Bot

from vk.methods import send_message

load_dotenv()
host_id = int(getenv("TG_USER_ID"))

dp = Dispatcher()

@dp.message(F.from_user.id==host_id)
async def on_message(message: Message, bot: Bot) -> None:
    send_message(bot.peer_id, message.text)

async def start_polling(bot) -> None:
    await dp.start_polling(bot)
│   └── methods/
│       ├── __init__.py
from .send_message import send_message
│       └── send_message.py
from aiogram import Bot
from vk.types import Message as VkMessage
from aiogram.types import InputMediaPhoto, InputMediaDocument, InputMediaAudio, Message


def generate_tg_message(msg: VkMessage) -> tuple[dict, callable]:
    text = f"·{msg.chat_title}·\n{msg.get_tg_text()}"
    media = msg.media
    commands = []

    if media:
        types = {}

        #no pep8
        InputMedia = {
            "photo":InputMediaPhoto,
            "doc": InputMediaDocument,
            "audio": InputMediaAudio,
        }

        for attach in media:
            if attach[0] == "video":
                continue
            types[attach[0]] = types.get(attach[0], [])
            types[attach[0]].append(InputMedia[attach[0]](media=attach[1],
                                                            parse_mode="MarkdownV2"))
        
        types[list(types.keys())[0]][0].caption = text
        for i in types.keys():
            if len(types[i]) > 1:
                args = {"media": types[i]}
                command = Bot.send_media_group
                commands.append((args, command))
            else:
                command = None
                match types[i][0].type:
                    case "document":
                        command = Bot.send_document
                    case "photo":
                        command = Bot.send_photo

                kwargs = {
                    types[i][0].type: types[i][0].media,
                    "caption": types[i][0].caption,
                    "parse_mode": "MarkdownV2"
                    }
                commands.append((kwargs, command))  
    else:
        commands.append(({"text": text, "parse_mode": "MarkdownV2"}, Bot.send_message))
    return commands

async def send_message(bot: Bot, msg: VkMessage, tg_chat_id: int):
    commands = generate_tg_message(msg)
    for message in commands:
        message: Message = await message[1](bot, tg_chat_id, **message[0])
├── vk/
│   └── methods/
│       ├── __init__.py
from .get_attachments import get_attachments
from .get_credentials import get_credentials
from .get_history import get_history
from .get_members import get_members
from .get_user import get_user_credentials
from .get_message import get_message
from .send_message import send_message

__all__ = [
    get_attachments,
    get_credentials,
    get_history,
    get_members,
    get_user_credentials,
    get_message,
    send_message
]
│       ├── consts.py
lp_version = 19
v = "5.223"
│       ├── get_attachments.py
import requests
from .consts import v


def get_attachments(access_token, pts):
    body = {
        "extended": 1,
        "pts": pts,
        "fields":"id,first_name,last_name",
        "access_token": access_token
    }

    query = {
        "v": v
    }

    req = requests.post("https://api.vk.me/method/messages.getLongPollHistory",
                        data=body, params=query).json()
    
    if req["response"]["messages"].get("items"):
        req = req["response"]["messages"]["items"][-1]
    return (req["attachments"],\
            req["fwd_messages"])
│       ├── get_credentials.py
import requests
from .consts import v, lp_version
from vk.types import ServerCredentials


def get_credentials(access_token) -> ServerCredentials:
    body = {
        "need_pts": 1,
        "group_id": 0,
        "lp_version": lp_version,
        "access_token": access_token
    }

    query = {
        "v": v
    }

    req = requests.post("https://api.vk.me/method/messages.getLongPollServer",
                        data=body, params=query)
    
    return ServerCredentials(**req.json()["response"])
│       ├──get_history.py
import requests
from .consts import v, lp_version


def get_history(access_token, ts, pts):
    body = {
        "access_token": access_token,
        "ts": ts,
        "pts": pts,
        "fields": "id,first_name,last_name",
        "lp_version": lp_version,
        "last_n": 1,
        "extended": 1,
        "credentials": 1
    }

    query = {
        "v": v 
    }

    req = requests.post("https://api.vk.me/method/messages.getLongPollHistory",
                        params=query, data=body).json()


    if req.get("error"):
        return {"error": True, "message": "Access token expired"}
    return req["response"]["messages"]["items"], req["response"]["profiles"],\
            req["response"]["conversations"][-1]["chat_settings"]["title"]
│       ├── get_members.py
import requests
from .consts import v


def get_members(access_token, peer):
    data = {
        "access_token": access_token,
        "peer_id": peer,
        "extended": 1,
        "fields": 'id, first_name, last_name'
    }

    query = {
        "v": v
    }

    req = requests.post("https://api.vk.me/method/messages.getConversationMembers",
                        data=data, params=query).json()
    
    if req.get("error"):
        return {"error": True, "text": "No profiles"}
    return req["response"]["profiles"]
│       ├── get_message.py
from .consts import v
import requests
import logging


def get_message(access_token, pts) -> tuple[list, list, str]:
    body = {
        "extended": 1,
        "pts": pts,
        "fields":"id,first_name,last_name",
        "access_token": access_token
    }

    query = {
        "v": v
    }

    req = requests.post("https://api.vk.me/method/messages.getLongPollHistory",
                        data=body, params=query).json()
    logging.debug(f"[VK API] get_message response: {req}")
    if req.get("error"):
        return {"error": True, "text": "access token has expired"}
    
    peer_type = req["response"]["conversations"][-1]["peer"]["type"]

    if peer_type == "user":
        title = "Direct message"
    elif peer_type == "chat":
        title = req["response"]["conversations"][-1]["chat_settings"]["title"]
    else:
        title = "None"

    return {
        "items": req["response"]["messages"]["items"],
        "profiles": req["response"]["profiles"],
        "title": title
        }
│       ├── get_user.py
from vk.types import UserCredentials 
import requests


def get_user_credentials(auth_cookie) -> UserCredentials:
    cookies = {
        "remixdsid": auth_cookie,
    }

    query = {
        "act": "web_token",
        "app_id": 8202606
    }
    
    req = requests.get("https://web.vk.me/", params=query,
                        cookies=cookies, allow_redirects=False)
    
    return UserCredentials(**req.json()[1])
│       └── send_message.py
import requests
from random import randint
from .consts import v


def send_message(access_token, peer_id, text) -> None:
    data = {
        "access_token": access_token,
        "peer_id": peer_id,
        "random_id": -randint(100000000, 999999999),
        "message": text
    }

    query = {
        "v": v 
    }

    requests.post("https://api.vk.me/method/messages.send",
                    data=data, params=query)
└── types/
│       ├── __init__.py
from .user_cred import UserCredentials
from .profile import Profile
from .server_cred import ServerCredentials
from .message import Message
from .event_message import EventMessage
│       ├── event_message.py
class EventMessage:
    def __init__(self, type, msg_id, flags, peer, value, title, text, *args, **kwargs) -> None:
        self.type = type
        self.msg_id = msg_id
        self.flags = flags
        self.chat_id = peer
        self.text = text
        self.title = title
        self.__dict__.update(kwargs)

    def __repr__(self) -> str:
        return f"from {self.title}: {self.text}, chat id: {self.chat_id}"
│       ├── message.py
import requests
from aiogram.types import BufferedInputFile

class Message:
    def __init__(self, date, from_id, text, attachments, conversation_message_id, **kwargs):
        self.__dict__.update(kwargs)
        self.date = date
        self.sender_id = from_id
        self.text = self.__validate_text(text)
        self.attachments = attachments
        self.chat_msg_id = conversation_message_id
        self.fwd = self.__dict__.get("fwd_messages", None)
        self.media = []
        self.full_name = self.sender_id
        
        if self.__dict__.get("profiles", False): self.__sender_full_name()
        if self.attachments: self.__parse_attachments()
        if self.fwd: self.__parse_fwds()
        if self.fwd: self.__get_all_media()

    @staticmethod
    def __get_source_link(attach) -> str:
        req = requests.get(attach["url"])
        if "text" in req.headers.get("Content-Type").split("/"): 
            url = req.text[
                req.text.find("https:", req.text.find("docUrl")):\
                                req.text.find('",', req.text.find("docUrl"))]
            req = requests.get(''.join(url.split("\\")))
            if len(req.content) < 52428800: 
                return BufferedInputFile(req.content, filename=req.url[req.url.rfind("/")+1:req.url.find("?")])
        elif "application" in req.headers.get("Content-Type").split("/"):
            return BufferedInputFile(req.content,
                                    filename=req.url[req.url.rfind("/")+1:\
                                                     req.url.find("?")])
        else: 
            return attach["url"]

    @staticmethod
    def __get_max_size_photo_url(attach: dict) -> str: 
        link = (0, 100)
        lvls = ("w", "z", "y", "r", "q", "p", "o", "x", "m", "s")

        for i in attach["sizes"]:
          if lvls.index(i["type"]) < link[1]:
             link = (i["url"], lvls.index(i["type"]))
        return link[0]

    def __parse_attachments(self) -> list:
        media = []
        parsers = {
            "photo": self.__get_max_size_photo_url,
            "video": lambda x: x["player"],
            "doc": self.__get_source_link,
        }
        for attach in self.attachments:
            media_type = attach["type"]
            if media_type in parsers.keys():
                media.append((media_type, parsers[media_type](attach[media_type])))
        self.media = media
        return media
    
    def __parse_fwds(self) -> list:
        fwds = []
        for msg in self.fwd:
            fwds.append(Message(**msg, profiles=self.profiles))
        self.fwd = fwds
        return fwds
    
    def __sender_full_name(self) -> None:
        for profile in self.profiles:
            if profile["id"] == self.sender_id:
                self.first_name = profile["first_name"]
                self.last_name = profile["last_name"]
                self.full_name = f"{self.first_name} {self.last_name}"
                return 
        
    def __repr__(self) -> str:
        sender = self.__dict__.get("full_name", self.sender_id)
        string = f" from {sender}: {self.text}, chat id: {self.chat_id}" 
        if self.media:
            string += f", media: {self.media}"
        if self.fwd:
            string += f", fwds: {self.fwd}"
        return string

    @staticmethod
    def __validate_text(text) -> str:
        validate_text = []

        text = "\n".join(text.split("<br>"))

        for char in text:
            if char in ('_', '*', '[', ']', '(', ')', '~', '`', '#', '+', '-', '=', '|', '{', '}', '.', '!', ">"):
                validate_text.append("\\")
            validate_text.append(char)
        return "".join(validate_text)    

    def __get_all_media(self) -> list:
        media = []
        if self.fwd:
            for attach in self.fwd: media.extend(Message.__get_all_media(attach))

        for attach in self.media:
            media.append(attach)
        self.media = media
        return media
    
    def get_tg_text(self, fwd_depth = 1) -> str:
        text = f"{self.full_name}:\n{'    '*fwd_depth}{self.text}"
        if self.attachments:
            text += f"\n{'    '*fwd_depth} " + f" "\
                .join([f"*{x[0]}*" if x[0] != "video" else f"[{x[0]}]({x[1]})"
                        for x in self.media ])
        if self.fwd:
            text += f"\n{'    '*fwd_depth}" + f"\n{'    '*fwd_depth}"\
                .join([Message.get_tg_text(msg, fwd_depth=fwd_depth+1) for msg in self.fwd])
        return text
│       ├── profile.py
class Profile:
    id: int
    first_name: str
    last_name: str

    def __init__(self, **kwargs) -> None:
        self.__dict__.update(kwargs)
    
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"
│       ├── server_cred.py
class ServerCredentials:
    server: str
    key: str
    ts: int
    pts: int

    def __init__(self, **kwargs) -> None:
        self.__dict__.update(kwargs)
│       └── user_cred.py
class UserCredentials:
    user_id: int
    profile_type: int # 1-female 2-male
    access_token: str
    expires: int

    def __init__(self, **kwargs) -> None:
        self.__dict__.update(kwargs)
├── example.env
AUTH_COOKIE="remixdsid" cookie file from web.vk.me
BOT_TOKEN=Telegram bot token from Bot Father
TG_USER_ID=Telegram "Host" user id
TG_CHAT_ID=Telegram chat id (if you want dm set you user id)
VK_CHAT_ID=vk peer_id (group_id) exapmle "200000015, 200000016"
├── requirements.txt
aiofiles==23.2.1
aiogram==3.4.1
aiohttp==3.9.3
aiosignal==1.3.1
annotated-types==0.6.0
attrs==23.2.0
certifi==2024.2.2
charset-normalizer==3.3.2
frozenlist==1.4.1
idna==3.6
magic-filter==1.0.12
multidict==6.0.5
pyaes==1.6.1
pydantic==2.5.3
pydantic_core==2.14.6
PySocks==1.7.1
python-dotenv==1.0.1
requests==2.31.0
typing_extensions==4.9.0
urllib3==2.2.0
yarl==1.9.4
├── main.py
import logging
import requests

from asyncio import sleep

from vk.methods import get_credentials, get_user_credentials, get_message
from tg.methods import send_message
from vk.types import Message, EventMessage

async def main(server, key, ts, tg_chat_id, vk_chat_ids, access_token, cookie, pts, bot):
    data = {
        "act": "a_check",
        "key": key,
        "ts": ts,
        "wait": 10  
    }
    while True:
        await sleep(.1)
        try:
            req = requests.post(f"https://{server}", data=data).json()
            
            if req.get("updates"):
                data["ts"] += 1
                event = req["updates"][0]

                if event[0] == 4:
                    raw_msg = EventMessage(*event)
                    logging.info(f"[MAIN] raw_msg: {raw_msg}")
                    if str(raw_msg.chat_id) in vk_chat_ids.split(", "):
                        # message, profile, chat_title = get_message(access_token, pts)
                        logging.debug("[MAIN] allowed chat")
                        
                        message = get_message(access_token, pts)
                        logging.info(message)

                        if message.get("error"):
                            access_token = get_user_credentials(cookie).access_token
                            credentials = get_credentials(access_token)
                            data["ts"] = credentials.ts
                            data["key"] = credentials.key                            
                            
                            message = get_message(access_token, pts)
                            logging.info(message)
                            
                        pts += 1
                        
                        message, profile, chat_title = message["items"], message["profiles"], message["title"]
    
                        msg = Message(**message[-1], profiles=profile, chat_title=chat_title)
                        await send_message(bot, msg, tg_chat_id)
                    else:
                        pts += 1
                    
            if req.get("failed", False) == 1:
                data["ts"] = req["ts"]
            elif req.get("failed", False) == 2:
                access_token = get_user_credentials(cookie).access_token
                credentials = get_credentials(access_token)
                data["ts"] = credentials.ts
                data["key"] = credentials.key

        except Exception as e:
            logging.exception(e)
└── startup.py
import asyncio
import logging

from os import getenv
from dotenv import load_dotenv

from aiogram import Bot

from vk.methods import get_credentials, get_user_credentials

from main import main

load_dotenv()

logging.basicConfig(filename="../sferum_in.log", encoding="utf-8", level=logging.INFO, datefmt='%m/%d/%Y %I:%M:%S %p')

tg_chat_id = getenv("TG_CHAT_ID")
vk_chat_ids = getenv("VK_CHAT_ID")
bot_token = getenv("BOT_TOKEN")
cookie = getenv("AUTH_COOKIE")

user = get_user_credentials(cookie)
access_token = user.access_token
creds = get_credentials(access_token)

loop = asyncio.get_event_loop()

try:
    bot = Bot(bot_token)
    task2 = loop.create_task(main(creds.server, creds.key, creds.ts, tg_chat_id, vk_chat_ids, access_token, cookie, creds.pts, bot))
    logging.info("Loop starting")
    loop.run_forever()
except KeyboardInterrupt:
    pass
except Exception as e:
    logging.exception(e)
finally:
    logging.info("Closing loop...")
    loop.close()
    logging.info("Loop closed")