1
0
Fork 0
mirror of https://github.com/EDeev/vkrepost_to_tg.git synced 2026-10-07 20:49:50 +03:00

Зависимости, тесты, CI, Docker, лицензия MIT и README на русском и английском

requirements.txt раньше не устанавливался (sqlite3 и бэкпорт asyncio с PyPI).
This commit is contained in:
Деев Егор Викторович 2026-10-05 19:52:45 +00:00
parent a38819dee7
commit b7a1f6ea22
13 changed files with 378 additions and 146 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
# Токен бота от @BotFather
BOT_TOKEN=123456:your-token
# Сервисный ключ приложения VK (vk.com/apps?act=manage → приложение → Настройки)
VK_SERVICE_TOKEN=your-vk-service-key

23
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements-dev.txt
- run: ruff check --select E9,F code tests
- run: pytest -q
- name: Бот импортируется
working-directory: code
env:
BOT_TOKEN: "123456:TEST"
run: python -c "import bot"

42
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Docker
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/login-action@v3
with:
registry: dcr.deev.su
username: ${{ secrets.ZOT_USERNAME }}
password: ${{ secrets.ZOT_PASSWORD }}
- id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/edeev/vkrepost_to_tg
dcr.deev.su/edeev/vkrepost_to_tg
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.env
__pycache__/
*.pyc
.pytest_cache/
db/*.db
*.log

19
Dockerfile Normal file
View file

@ -0,0 +1,19 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY code/ code/
RUN useradd --create-home --uid 1000 app \
&& mkdir -p db \
&& chown -R app:app /app
USER app
VOLUME ["/app/db"]
# пути к базам в коде — относительно папки code/ (../db)
WORKDIR /app/code
CMD ["python", "bot.py"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Egor Deev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

86
README.en.md Normal file
View file

@ -0,0 +1,86 @@
# Portal in VK
[Русский](README.md) · **English**
[![CI](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/ci.yml)
[![Docker](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/docker.yml/badge.svg)](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/docker.yml)
[![License](https://img.shields.io/github/license/EDeev/vkrepost_to_tg)](LICENSE)
A Telegram bot that forwards new posts from VK (VKontakte) pages and communities to Telegram: subscribe
with `/add <short name>` and every new post arrives as a message, with photos, video covers, documents,
audio, polls and reposts. The bot speaks Russian.
**Status:** personal project, completed · bot [@vkportalbot](https://t.me/vkportalbot)
**Stack:** Python 3.12 · aiogram 3 · vk_api · SQLite · Docker
## Features
- Up to 10 subscriptions per user: personal pages, groups and public pages
- A post is converted to Telegram format:
- photos, video covers and image documents — as an album;
- audio — as a separate message;
- polls, links and documents — in the text;
- `[id1|Name]` mentions — as links;
- reposts show both authors and the comment.
- Telegram limits are handled: a single photo, albums up to 10 items, long text as separate messages
- `/last_post` — the latest post of any page; `/update` — resend a post with fresh data
- Your own VK token (optional) gives posts from closed pages you follow and likes from Telegram (`/like`
as a reply to a post)
> [!IMPORTANT]
> If you send the bot your VK token, it is stored on the bot's server. Delete it with `/logout`; revoke
> access completely in VK settings, "Apps and websites".
## Running
```bash
git clone https://github.com/EDeev/vkrepost_to_tg.git && cd vkrepost_to_tg
cp .env.example .env # BOT_TOKEN and the VK app service key
docker compose up -d
```
Prebuilt image: `docker pull ghcr.io/edeev/vkrepost_to_tg` or `docker pull dcr.deev.su/edeev/vkrepost_to_tg`.
The SQLite databases are created on first start.
Without Docker: Python 3.12, `pip install -r requirements.txt`, then
`cd code && BOT_TOKEN=… VK_SERVICE_TOKEN=… python bot.py`.
## How it works
```
code/bot.py entry point and background polling of subscriptions every minute
code/handlers.py commands: subscriptions, latest post, likes, VK token
code/vk_scripts.py VK API requests and post parsing
code/scripts.py post → HTML text and Telegram media, sending within Telegram limits
code/sql.py users, subscriptions and each page's latest post (SQLite)
```
For a closed page the bot uses the token of one of its subscribers; for open pages, the service key. VK
requests run in a separate thread so the bot never freezes. Text from VK is escaped.
## Development
```bash
pip install -r requirements-dev.txt
ruff check --select E9,F code tests && pytest
```
The tests cover post parsing (escaping, mentions, links, reposts, album limit) and sending (single photo,
long text, audio). The Docker image is built on `v*` tags and published to GitHub Packages and
`dcr.deev.su`.
## License
MIT — see [LICENSE](LICENSE).
## Author
**Egor Deev** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space)
---
<div align="center">
<sub>⭐ If you find this project useful, give it a star on GitHub!</sub>
<p><sub>Made with ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div>

188
README.md
View file

@ -1,155 +1,87 @@
# Repost from Vk to Tg 🔄 # Portal in VK
[![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://python.org) **Русский** · [English](README.en.md)
[![Aiogram](https://img.shields.io/badge/Aiogram-2.x-green.svg)](https://aiogram.dev)
[![VK API](https://img.shields.io/badge/VK%20API-5.131-orange.svg)](https://dev.vk.com)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
**Автоматизированная система репостинга контента из VKontakte в Telegram** [![CI](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/ci.yml)
[![Docker](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/docker.yml/badge.svg)](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/docker.yml)
[![License](https://img.shields.io/github/license/EDeev/vkrepost_to_tg)](LICENSE)
Telegram-бот для автоматического мониторинга и репостинга публикаций из социальной сети ВКонтакте с поддержкой персонализированных подписок и расширенным функционалом взаимодействия. Telegram-бот, который пересылает новые посты со страниц и сообществ ВКонтакте в Telegram: подписываешься
командой `/add <короткое имя>`, и каждый новый пост приходит сообщением — с фото, обложками видео,
документами, аудио, опросами и репостами.
## 🎯 Основная функциональность **Статус:** личный проект, завершён · бот [@vkportalbot](https://t.me/vkportalbot)
### Ключевые возможности **Стек:** Python 3.12 · aiogram 3 · vk_api · SQLite · Docker
- **Автоматический мониторинг** публикаций из VK страниц и сообществ
- **Персонализированные подписки** до 10 источников на пользователя
- **Интеграция с VK API** через пользовательские токены
- **Интерактивное взаимодействие** с лайками постов через Telegram
- **Многоформатная поддержка** медиа-контента (фото, видео, аудио, документы)
### Архитектурные особенности ## Возможности
- Асинхронная обработка запросов с использованием `asyncio`
- Двухуровневая система баз данных (пользователи/группы)
- Токен-ротация для обхода ограничений API
- Централизованное управление подписками
## 🚀 Быстрый старт - До 10 подписок на пользователя: личные страницы, группы и паблики
- Пост переводится в формат Telegram:
- фото, обложки видео и картинки-документы — альбомом;
- аудио — отдельным сообщением;
- опросы, ссылки и документы — в тексте;
- упоминания `[id1|Имя]` — ссылками;
- у репостов — оба автора и комментарий.
- Ограничения Telegram учтены: одиночное фото, альбомы до 10 элементов, длинный текст отдельными
сообщениями
- `/last_post` — последний пост любой страницы, `/update` — прислать пост заново с актуальными данными
- Свой токен VK (необязательно) даёт посты закрытых страниц, на которые вы подписаны, и лайки из Telegram
(`/like` ответом на пост)
> [!IMPORTANT]
> Если вы присылаете боту свой токен VK, он хранится на сервере бота. Удалить его можно командой
> `/logout`, полностью отозвать доступ — в настройках VK, раздел «Приложения и сайты».
## Запуск
### Предварительные требования
```bash ```bash
pip install -r requirements.txt git clone https://github.com/EDeev/vkrepost_to_tg.git && cd vkrepost_to_tg
cp .env.example .env # BOT_TOKEN и сервисный ключ приложения VK
docker compose up -d
``` ```
### Конфигурация Готовый образ: `docker pull ghcr.io/edeev/vkrepost_to_tg` или `docker pull dcr.deev.su/edeev/vkrepost_to_tg`.
1. Создайте Telegram-бота через [@BotFather](https://t.me/botfather) Базы SQLite создаются при первом запуске.
2. Получите служебный токен VK API
3. Настройте файл `config.py`:
```python Без Docker: Python 3.12, `pip install -r requirements.txt`, затем
# TOKENS `cd code && BOT_TOKEN=… VK_SERVICE_TOKEN=… python bot.py`.
botToken = 'YOUR_TELEGRAM_BOT_TOKEN'
serviceToken = "YOUR_VK_SERVICE_TOKEN"
# URL ## Как устроено
loginUrl = "https://oauth.vk.com/authorize?client_id=YOUR_APP_ID&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=wall,likes&response_type=token&v=5.131"
```
code/bot.py запуск и фоновый опрос подписок раз в минуту
code/handlers.py команды: подписки, последний пост, лайки, токен VK
code/vk_scripts.py запросы к VK API и разбор поста
code/scripts.py пост → текст в HTML и медиа Telegram, отправка с учётом ограничений
code/sql.py пользователи, подписки и последний пост каждой страницы (SQLite)
``` ```
### Запуск системы Для закрытой страницы бот берёт токен одного из её подписчиков, для открытых — сервисный ключ. Запросы
к VK идут в отдельном потоке, чтобы бот не замирал. Текст из ВК экранируется.
## Разработка
```bash ```bash
python bot.py pip install -r requirements-dev.txt
ruff check --select E9,F code tests && pytest
``` ```
## 📋 Структура команд Тесты проверяют разбор постов (экранирование, упоминания, ссылки, репосты, лимит альбома) и отправку
(одиночное фото, длинный текст, аудио). Docker-образ собирается по тегу `v*` и публикуется в GitHub
Packages и `dcr.deev.su`.
| Команда | Описание | Синтаксис | ## Лицензия
|---------|----------|-----------|
| `/start` | Инициализация пользователя | `/start` |
| `/add` | Добавление подписки | `/add domain_name` |
| `/del` | Удаление подписки | `/del domain_name` |
| `/list` | Просмотр активных подписок | `/list` |
| `/like` | Лайк поста (ответ на сообщение) | `/like` |
| `/notif` | Переключение уведомлений | `/notif` |
| `/last_post` | Получение последней публикации | `/last_post domain_name` |
## 🏗️ Архитектура системы MIT — см. [LICENSE](LICENSE).
### Компоненты системы ## Автор
```
├── bot.py # Основной модуль бота
├── vk_scripts.py # VK API интеграция
├── sql.py # Управление базами данных
├── scripts.py # Вспомогательные функции
├── config.py # Конфигурация проекта
└── db/
├── users.db # База пользователей
└── base.db # Основная база данных
```
### Технологический стек **Деев Егор Викторович** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space)
- **Backend**: Python 3.8+
- **Telegram Framework**: Aiogram 2.x
- **VK Integration**: vk_api
- **Database**: SQLite
- **Async Processing**: asyncio
- **Media Processing**: Built-in handlers
### Схема базы данных
#### Таблица `user` (users.db)
```sql
user_id INTEGER PRIMARY KEY -- Telegram ID пользователя
id INTEGER AUTOINCREMENT -- Внутренний ID
```
#### Таблица `user` (base.db)
```sql
user_id INTEGER -- Ссылка на users.db
status BOOLEAN -- Статус уведомлений
groups TEXT -- Список подписок (разделитель ;)
token TEXT -- VK access token
count INTEGER -- Количество подписок
```
## ⚡ Алгоритм работы
### Цикл мониторинга
1. **Сканирование источников** (интервал: 60 секунд)
2. **Проверка новых публикаций** через VK API
3. **Форматирование контента** под Telegram
4. **Массовая рассылка** подписчикам
5. **Обновление метаданных** в базе данных
### Обработка медиа-контента
- **Фотографии**: Группировка в медиа-альбомы
- **Видео**: Информационные заглушки с ссылками
- **Аудио**: Отдельные медиа-сообщения
- **Документы**: Прямые ссылки с метаданными
- **Опросы**: Текстовое представление с результатами
## 🔧 Расширенные возможности
### Система токенов
- **Служебный токен**: Базовый доступ к публичному контенту
- **Пользовательские токены**: Доступ к закрытым страницам и функции лайков
- **Автоматическая ротация**: Распределение нагрузки между токенами
### Обработка ошибок
- Graceful handling VK API лимитов
- Автоматический фallback на служебный токен
- Логирование критических ошибок
## 📊 Метрики производительности
- **Пропускная способность**: До 1000 пользователей
- **Частота обновлений**: 60 секунд
- **Лимит подписок**: 10 на пользователя
- **Поддерживаемые форматы**: 6 типов медиа
## 📄 Лицензия
Проект распространяется под лицензией MIT.
## 👨‍💻 Автор
**Деев Егор Викторович**
- GitHub: [@EDeev](https://github.com/EDeev)
- Email: egor@deev.space
- Telegram: [@Egor_Deev](https://t.me/Egor_Deev)
--- ---
<div align="center"> <div align="center">
<sub>Проект носит некоммерческий характер и предназначен для образовательных целей и личного использования.</sub> <sub>⭐ Если проект оказался полезным, поставьте звёздочку на GitHub!</sub>
<p><sub>Создано с ❤️ от вашего дорогого - deev.space ©</sub></p> <p><sub>Сделано с ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div> </div>

11
compose.yaml Normal file
View file

@ -0,0 +1,11 @@
services:
bot:
build: .
image: ghcr.io/edeev/vkrepost_to_tg:latest
env_file: .env
volumes:
- db:/app/db
restart: unless-stopped
volumes:
db:

3
requirements-dev.txt Normal file
View file

@ -0,0 +1,3 @@
-r requirements.txt
pytest==8.4.2
ruff==0.14.0

View file

@ -1,18 +1,3 @@
# Telegram Bot Framework aiogram==3.23.0
aiogram==2.25.1 vk_api==11.10.1
emoji==2.16.0
# VK API
vk-api==11.9.9
# Database
sqlite3
# Async support
asyncio
# Emoji support
emoji==2.8.0
# Additional dependencies
requests>=2.28.0
aiohttp>=3.8.0

4
tests/conftest.py Normal file
View file

@ -0,0 +1,4 @@
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "code"))

96
tests/test_post.py Normal file
View file

@ -0,0 +1,96 @@
import asyncio
from types import SimpleNamespace
import scripts
import vk_scripts
def wall(text, attachments, repost=None):
post = {"id": 7, "owner_id": -100, "text": text, "attachments": attachments}
if repost:
post["copy_history"] = [repost]
return {"items": [post], "groups": [{"id": 100, "name": "Паблик <&>", "screen_name": "pub"}],
"profiles": [{"id": 5, "first_name": "Иван", "last_name": "Петров", "screen_name": "ivan"}]}
def parse(response):
class Api:
class wall:
@staticmethod
def get(**_): return response
parser = vk_scripts.VkParser.__new__(vk_scripts.VkParser)
parser.vk = Api
return parser.last_post(owner_id=-100)
def photo(url):
return {"type": "photo", "photo": {"sizes": [{"url": url + "-small"}, {"url": url}]}}
def test_text_is_escaped_and_mentions_become_links():
out = parse(wall("1 < 2 & [id5|Иван] пишет <b>", []))
text, audio, media = scripts.pars_post(out)
assert "1 &lt; 2 &amp; <a href='https://vk.com/id5'>Иван</a> пишет &lt;b&gt;" in text
assert "Паблик &lt;&amp;&gt;" in text
assert audio == [] and media == []
def test_attachments_links_are_html_and_media_limited():
atts = [{"type": "link", "link": {"title": "Сайт", "url": "https://example.com/?a=1&b=2"}}]
atts += [photo(f"https://img/{i}") for i in range(12)]
text, audio, media = scripts.pars_post(parse(wall("пост", atts)))
assert "<a href='https://example.com/?a=1&amp;b=2'>Сайт</a>" in text
assert "](" not in text # раньше ссылки оформлялись Markdown-синтаксисом
assert len(media) == 10 and media[0].media == "https://img/0"
def test_repost_has_both_authors():
orig = {"id": 3, "owner_id": 5, "text": "оригинал", "attachments": []}
text, _, _ = scripts.pars_post(parse(wall("мой комментарий", [], repost=orig)))
assert "Автор репоста" in text and "Иван Петров" in text and "<blockquote>мой комментарий</blockquote>" in text
def test_split_text_keeps_lines():
text = "\n".join(f"<b>строка {i}</b>" for i in range(1000))
parts = scripts.split_text(text, 4096)
assert all(len(p) <= 4096 for p in parts)
assert "".join(parts).count("<b>") == 1000 and all(p.count("<b>") == p.count("</b>") for p in parts)
class FakeBot:
def __init__(self):
self.calls = []
def __getattr__(self, name):
async def method(**kwargs):
self.calls.append((name, kwargs))
msg = SimpleNamespace(message_id=len(self.calls))
return [msg] if name == "send_media_group" else msg
return method
def send(text, audio, media):
bot = FakeBot()
asyncio.run(scripts.send_post(bot, 1, text, audio, media))
return bot.calls
def test_single_photo_is_sent_as_photo_not_album():
_, _, media = scripts.pars_post(parse(wall("пост", [photo("https://img/1")])))
calls = send("короткий текст", [], media)
assert [c[0] for c in calls] == ["send_photo"] and calls[0][1]["caption"] == "короткий текст"
def test_long_text_with_album_goes_as_separate_messages():
_, _, media = scripts.pars_post(parse(wall("пост", [photo("a"), photo("b")])))
calls = send("x" * 3000, [], media)
assert calls[0][0] == "send_media_group" and calls[0][1]["media"][0].caption is None
assert calls[1][0] == "send_message" and calls[1][1]["reply_to_message_id"] == 1
def test_text_with_single_audio():
atts = [{"type": "audio", "audio": {"title": "Песня", "url": "https://a/1.mp3", "artist": "Автор"}}]
text, audio, media = scripts.pars_post(parse(wall("пост", atts)))
calls = send(text, audio, media)
assert [c[0] for c in calls] == ["send_message", "send_audio"]
assert calls[1][1]["reply_to_message_id"] == 1