mirror of
https://github.com/EDeev/vkrepost_to_tg.git
synced 2026-10-08 04:59:45 +03:00
Compare commits
No commits in common. "b7a1f6ea220c3abe9ee289c9bf714a17d50a64e8" and "c93ffe7c57abd3b23394369b11898e49a1d68d65" have entirely different histories.
b7a1f6ea22
...
c93ffe7c57
24 changed files with 599 additions and 1040 deletions
|
|
@ -1,4 +0,0 @@
|
|||
# Токен бота от @BotFather
|
||||
BOT_TOKEN=123456:your-token
|
||||
# Сервисный ключ приложения VK (vk.com/apps?act=manage → приложение → Настройки)
|
||||
VK_SERVICE_TOKEN=your-vk-service-key
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
|||
* text=auto eol=lf
|
||||
*.db binary
|
||||
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
|
|
@ -1,23 +0,0 @@
|
|||
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
42
.github/workflows/docker.yml
vendored
|
|
@ -1,42 +0,0 @@
|
|||
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
6
.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
|||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
db/*.db
|
||||
*.log
|
||||
19
Dockerfile
19
Dockerfile
|
|
@ -1,19 +0,0 @@
|
|||
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
21
LICENSE
|
|
@ -1,21 +0,0 @@
|
|||
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
86
README.en.md
|
|
@ -1,86 +0,0 @@
|
|||
# Portal in VK
|
||||
|
||||
[Русский](README.md) · **English**
|
||||
|
||||
[](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/ci.yml)
|
||||
[](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/docker.yml)
|
||||
[](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
188
README.md
|
|
@ -1,87 +1,155 @@
|
|||
# Portal in VK
|
||||
# Repost from Vk to Tg 🔄
|
||||
|
||||
**Русский** · [English](README.en.md)
|
||||
[](https://python.org)
|
||||
[](https://aiogram.dev)
|
||||
[](https://dev.vk.com)
|
||||
[](LICENSE)
|
||||
|
||||
[](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/ci.yml)
|
||||
[](https://github.com/EDeev/vkrepost_to_tg/actions/workflows/docker.yml)
|
||||
[](LICENSE)
|
||||
**Автоматизированная система репостинга контента из VKontakte в Telegram**
|
||||
|
||||
Telegram-бот, который пересылает новые посты со страниц и сообществ ВКонтакте в Telegram: подписываешься
|
||||
командой `/add <короткое имя>`, и каждый новый пост приходит сообщением — с фото, обложками видео,
|
||||
документами, аудио, опросами и репостами.
|
||||
Telegram-бот для автоматического мониторинга и репостинга публикаций из социальной сети ВКонтакте с поддержкой персонализированных подписок и расширенным функционалом взаимодействия.
|
||||
|
||||
**Статус:** личный проект, завершён · бот [@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
|
||||
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
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Готовый образ: `docker pull ghcr.io/edeev/vkrepost_to_tg` или `docker pull dcr.deev.su/edeev/vkrepost_to_tg`.
|
||||
Базы SQLite создаются при первом запуске.
|
||||
### Конфигурация
|
||||
1. Создайте Telegram-бота через [@BotFather](https://t.me/botfather)
|
||||
2. Получите служебный токен VK API
|
||||
3. Настройте файл `config.py`:
|
||||
|
||||
Без Docker: Python 3.12, `pip install -r requirements.txt`, затем
|
||||
`cd code && BOT_TOKEN=… VK_SERVICE_TOKEN=… python bot.py`.
|
||||
```python
|
||||
# TOKENS
|
||||
botToken = 'YOUR_TELEGRAM_BOT_TOKEN'
|
||||
serviceToken = "YOUR_VK_SERVICE_TOKEN"
|
||||
|
||||
## Как устроено
|
||||
|
||||
```
|
||||
code/bot.py запуск и фоновый опрос подписок раз в минуту
|
||||
code/handlers.py команды: подписки, последний пост, лайки, токен VK
|
||||
code/vk_scripts.py запросы к VK API и разбор поста
|
||||
code/scripts.py пост → текст в HTML и медиа Telegram, отправка с учётом ограничений
|
||||
code/sql.py пользователи, подписки и последний пост каждой страницы (SQLite)
|
||||
# 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"
|
||||
```
|
||||
|
||||
Для закрытой страницы бот берёт токен одного из её подписчиков, для открытых — сервисный ключ. Запросы
|
||||
к VK идут в отдельном потоке, чтобы бот не замирал. Текст из ВК экранируется.
|
||||
|
||||
## Разработка
|
||||
|
||||
### Запуск системы
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
ruff check --select E9,F code tests && pytest
|
||||
python bot.py
|
||||
```
|
||||
|
||||
Тесты проверяют разбор постов (экранирование, упоминания, ссылки, репосты, лимит альбома) и отправку
|
||||
(одиночное фото, длинный текст, аудио). 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">
|
||||
<sub>⭐ Если проект оказался полезным, поставьте звёздочку на GitHub!</sub>
|
||||
<p><sub>Сделано с ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
|
||||
<sub>Проект носит некоммерческий характер и предназначен для образовательных целей и личного использования.</sub>
|
||||
<p><sub>Создано с ❤️ от вашего дорогого - deev.space ©</sub></p>
|
||||
</div>
|
||||
|
|
|
|||
334
code/bot.py
334
code/bot.py
|
|
@ -1,66 +1,306 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import logging, config, asyncio, time, sql, random
|
||||
|
||||
import scripts
|
||||
import vk_scripts
|
||||
from init import bot, dp, du, db
|
||||
from config import serviceToken
|
||||
from handlers import router
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
from emoji import emojize
|
||||
|
||||
from vk_scripts import VkParser
|
||||
from scripts import login, pars_post
|
||||
|
||||
# log level
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# bot init
|
||||
bot = Bot(token=config.botToken)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
# connection db
|
||||
du = sql.Users('../db/users.db')
|
||||
db = sql.Base('../db/base.db')
|
||||
|
||||
|
||||
async def check_group(group_id, activ_user, sub_user):
|
||||
tokens = [x[0] for x in activ_user if group_id in x[1]]
|
||||
users = [x[0] for x in sub_user if group_id in x[1]]
|
||||
# events
|
||||
@dp.message_handler(commands=["start", "help"])
|
||||
async def start(message: types.Message):
|
||||
login(message.chat.id, du, db)
|
||||
|
||||
# токен случайного подписчика даёт доступ к закрытым страницам, иначе — сервисный ключ
|
||||
if tokens:
|
||||
vk = vk_scripts.VkParser(db.get_token(random.choice(tokens)))
|
||||
else: vk = vk_scripts.VkParser(serviceToken)
|
||||
buttons = [types.InlineKeyboardButton(text="КОМАНДЫ", callback_data="com"),
|
||||
types.InlineKeyboardButton(text="АВТОР", callback_data="auth")]
|
||||
|
||||
# vk_api синхронный — в отдельном потоке, чтобы не замирал весь бот
|
||||
output = await asyncio.to_thread(vk.last_post, owner_id=du.get_vk_id(group_id))
|
||||
last_post = db.get_postGroup(group_id)
|
||||
keyboard = types.InlineKeyboardMarkup(row_width=2)
|
||||
keyboard.add(*buttons)
|
||||
|
||||
if output[3] > last_post:
|
||||
text, audio, media = scripts.pars_post(output)
|
||||
db.update_postGroup(group_id, output[3])
|
||||
await message.answer(text=f'*Portal in VK* - это бот для перепоста постов со страниц в социальной сети ВКонтакте. '
|
||||
f'Для начала работы вам нужно всего лишь вызвать команду */add* и добавить к ней короткое '
|
||||
f'имя личной или публичной страницы. После этого, бот начнёт присылать каждый новый пост '
|
||||
f'с этой страницы форматируя данные с неё под сообщение в Telegram. С полным функционалом '
|
||||
f'бота, вы можете ознакомиться по кнопке *КОМАНДЫ*\n\n'
|
||||
|
||||
for sub in users:
|
||||
f'Если вы хотите ставить лайки или получать уведомления от закрытых страниц на которые '
|
||||
f'подписаны в социальной сети, вам необходимо скинуть ссылку из поисковой строки в чат, '
|
||||
f'после подтверждения на [сайте]({config.loginUrl})',
|
||||
parse_mode=types.ParseMode.MARKDOWN, reply_markup=keyboard)
|
||||
|
||||
|
||||
# ИЛАЙН КЛАВИАТУРА HELP
|
||||
@dp.callback_query_handler(text="com")
|
||||
async def function(call: types.CallbackQuery):
|
||||
await call.message.answer(text='*| КОМАНДЫ |*\n\n'
|
||||
f'*/like* - лайк на пост, который вы отметили\n'
|
||||
f'*/list* - список страниц от, которых вы получаете уведомления\n'
|
||||
f'*/notif* - отписка или подписка от всех уведомлений\n'
|
||||
f'*/last_post* - получение последнего поста по короткому имени страницы\n'
|
||||
f'*/add* и */del* - добавление и удаление странички из списка подписок\n',
|
||||
parse_mode=types.ParseMode.MARKDOWN)
|
||||
|
||||
|
||||
@dp.callback_query_handler(text="auth")
|
||||
async def author(call: types.CallbackQuery):
|
||||
await call.message.answer(text='*| АВТОР |*\n\n*>>* Этот бот не коммерческий проект, для получения постов из '
|
||||
'социальной сети ВКонтакте. Бот работает на сервисном токене VK API, пока вы не '
|
||||
'предоставите собственный. Ваш токен нужен для получения постов со страниц, '
|
||||
'доступ к которым есть исключительно у вас. Также это даст вам возможность ставить '
|
||||
'через Telegram лайки на посты в самой социальной сети!'
|
||||
|
||||
'\n\nЯ же пишу подобные небольшие проекты, о которых вы можете узнать '
|
||||
'больше на моём [GitHub](https://github.com/IGlek).',
|
||||
parse_mode=types.ParseMode.MARKDOWN)
|
||||
|
||||
|
||||
# КОМАНДЫ
|
||||
@dp.message_handler(commands=["notif"])
|
||||
async def notification(message: types.Message):
|
||||
user_id = login(message.chat.id, du, db)
|
||||
status = db.get_status(user_id)
|
||||
|
||||
groups = list(map(int, db.get_user_groups(user_id).split(";")))
|
||||
for group in groups: db.update_countGroup(group, -1 if status else 1)
|
||||
|
||||
if status: await message.answer("Получение постов из ВК выключено!")
|
||||
else: await message.answer("Получение постов из ВК включено!")
|
||||
db.update_status(user_id)
|
||||
|
||||
|
||||
@dp.message_handler(commands=["add", "del"])
|
||||
async def add_del(message: types.Message):
|
||||
user_id = login(message.chat.id, du, db)
|
||||
domain = message.get_args()
|
||||
|
||||
if domain:
|
||||
try:
|
||||
token = db.get_token(user_id)
|
||||
if token: vk = VkParser(token)
|
||||
else: vk = VkParser(config.serviceToken)
|
||||
|
||||
group_id, typ, last_post = vk.login(domain)
|
||||
|
||||
if not du.group_exists(group_id):
|
||||
du.add_group(group_id)
|
||||
group_id = du.get_group_id(group_id)
|
||||
|
||||
if not db.infoGroup_exists(group_id):
|
||||
db.add_infoGroup(group_id, typ, last_post)
|
||||
|
||||
groups = db.get_user_groups(user_id)
|
||||
user_groups = groups.split(";") if groups else []
|
||||
|
||||
if str(group_id) not in user_groups:
|
||||
if message.get_command() == "/del":
|
||||
await message.answer("Вы и так не подписаны на эту группу!")
|
||||
else:
|
||||
if db.get_countUser(user_id) < 10:
|
||||
user_groups.append(str(group_id))
|
||||
db.update_user_groups(user_id, ";".join(user_groups))
|
||||
|
||||
db.update_countUser(user_id, 1)
|
||||
if db.get_status(user_id): db.update_countGroup(group_id, 1)
|
||||
|
||||
await message.answer("Группа успешно добавлена в список уведомлений!")
|
||||
else:
|
||||
await message.answer("У вас добавлено максимально количество групп!")
|
||||
else:
|
||||
if message.get_command() == "/add":
|
||||
await message.answer("Вы уже подписаны на эту страницу!")
|
||||
else:
|
||||
user_groups.remove(str(group_id))
|
||||
db.update_user_groups(user_id, ";".join(user_groups))
|
||||
|
||||
db.update_countUser(user_id, -1)
|
||||
if db.get_status(user_id): db.update_countGroup(group_id, -1)
|
||||
|
||||
await message.answer("Группа успешно удалена из списка уведомлений!")
|
||||
except Exception as e:
|
||||
await message.answer("Произошла ошибка! Проверьте правильность написания короткого адреса страницы "
|
||||
"и имеете ли вы доступ к этой странице!")
|
||||
else:
|
||||
if message.get_command() == "/add":
|
||||
await message.answer("Команда введена некорректно, такой страницы не существует или она закрыта для "
|
||||
"просмотра!\n\nКорректный ввод команд: /add <i>example</i>", types.ParseMode.HTML)
|
||||
elif message.get_command() == "/del":
|
||||
await message.answer("Команда введена некорректно, такой страницы не существует или она закрыта для "
|
||||
"просмотра!\n\nКорректный ввод команд: /del <i>example</i>", types.ParseMode.HTML)
|
||||
|
||||
|
||||
@dp.message_handler(commands=["list"])
|
||||
async def lst(message: types.Message):
|
||||
user_id = login(message.chat.id, du, db)
|
||||
groups = db.get_user_groups(user_id)
|
||||
user_groups = list(map(int, groups.split(";") if groups else []))
|
||||
|
||||
if user_groups:
|
||||
group_ids = [du.get_vk_id(id) for id in user_groups]
|
||||
|
||||
vk = VkParser(config.serviceToken)
|
||||
peoples, groups = vk.info(group_ids)
|
||||
|
||||
text = "<b><i>* Список страниц, от которых вы получаете уведомления!</i></b>\n"
|
||||
|
||||
if peoples:
|
||||
text += "\n<b>Личные страницы пользователей</b>\n"
|
||||
for i, people in enumerate(peoples):
|
||||
text += f'{emojize(":green_circle:") if people[2] else emojize(":red_circle:")} ' \
|
||||
f'{i + 1}. {people[1]} <a href="https://vk.com/{people[0]}">@{people[0]}</a>\n'
|
||||
|
||||
if groups:
|
||||
text += "\n<b>Группы / сообщества / паблики</b>\n"
|
||||
for i, group in enumerate(groups):
|
||||
text += f'{i + 1}. {group[1]} <a href="https://vk.com/{group[0]}">@{group[0]}</a>\n'
|
||||
|
||||
await message.answer(text, types.ParseMode.HTML)
|
||||
else:
|
||||
await message.answer("Вы не добавили ни одной страницы для отслеживания публикаций!")
|
||||
|
||||
|
||||
@dp.message_handler(commands=["like"])
|
||||
async def like(message: types.Message):
|
||||
user_id = login(message.chat.id, du, db)
|
||||
token = db.get_token(user_id)
|
||||
|
||||
if token and 'reply_to_message' in message:
|
||||
url = ''
|
||||
|
||||
if 'entities' in message.reply_to_message:
|
||||
for i in message.reply_to_message.entities:
|
||||
if i['type'] == 'text_link': url = i['url']; break
|
||||
elif 'caption_entities' in message.reply_to_message:
|
||||
for i in message.reply_to_message.caption_entities:
|
||||
if i['type'] == 'text_link': url = i['url']; break
|
||||
|
||||
if url:
|
||||
try:
|
||||
await scripts.send_post(bot, du.get_tg_id(sub), text, audio, media)
|
||||
except Exception:
|
||||
# подписчик мог заблокировать бота — остальным пост всё равно уходит
|
||||
logging.warning("Не удалось отправить пост подписчику %s", sub, exc_info=True)
|
||||
[group_id, last_post] = list(map(int, url.split('wall')[-1].split('_')))
|
||||
|
||||
vk = VkParser(token)
|
||||
vk.like(group_id, last_post)
|
||||
|
||||
await message.answer(text='Вы поставили лайк 😉')
|
||||
except Exception as e:
|
||||
await message.answer(text='По неизвестной причине не удалось поставить лайк!')
|
||||
else:
|
||||
await message.answer(text='Вы ответили не на тот пост, для отправки лайка!')
|
||||
else:
|
||||
await message.answer(text='Вы не отметили пост который хотите лайкнуть или не прислали свой токен для '
|
||||
'возможности лайкать посты!')
|
||||
|
||||
|
||||
async def timer(wait_for) -> None:
|
||||
# ВЗАИМОДЕЙСТВИЕ С ДАННЫМИ ВК ПОСТОВ
|
||||
@dp.message_handler(commands=["last_post"])
|
||||
async def last_post(message: types.Message):
|
||||
user_id = message.chat.id
|
||||
short_id = login(user_id, du, db)
|
||||
domain = message.get_args()
|
||||
|
||||
if domain:
|
||||
try:
|
||||
token = db.get_token(short_id)
|
||||
if token: vk = VkParser(token)
|
||||
else: vk = VkParser(config.serviceToken)
|
||||
|
||||
output = vk.last_post(domain=domain); vk.login(domain)
|
||||
text, audio, media = pars_post(types, output)
|
||||
|
||||
if audio and media:
|
||||
post_message = await bot.send_media_group(chat_id=user_id, media=media)
|
||||
await bot.send_media_group(chat_id=user_id, media=audio, reply_to_message_id=post_message[0].message_id)
|
||||
elif audio and media == []:
|
||||
post_message = await bot.send_message(chat_id=user_id, text=text, parse_mode=types.ParseMode.MARKDOWN)
|
||||
await bot.send_media_group(chat_id=user_id, media=audio, reply_to_message_id=(post_message[0].message_id))
|
||||
elif audio == [] and media:
|
||||
await bot.send_media_group(chat_id=user_id, media=media)
|
||||
else:
|
||||
await bot.send_message(chat_id=user_id, text=text, parse_mode=types.ParseMode.MARKDOWN)
|
||||
|
||||
except Exception as e:
|
||||
await message.answer("Короткое имя страницы неверное или у вас нет доступа к этой странице!")
|
||||
else:
|
||||
await message.answer("Команда введена некорректно! Корректный ввод: /last_post example")
|
||||
|
||||
|
||||
# ПОЛУЧЕНИЕ ТОКЕНА ПОЛЬЗОВАТЕЛЯ
|
||||
@dp.message_handler(content_types="text")
|
||||
async def url(message: types.Message):
|
||||
user_id = login(message.chat.id, du, db)
|
||||
|
||||
if config.checkUrl in message.text:
|
||||
try:
|
||||
token = message.text.split("&")[0].split("=")[-1]
|
||||
info = VkParser(token).check()
|
||||
|
||||
db.update_token(user_id, token)
|
||||
await message.answer("Токен успешно сохранён!")
|
||||
except Exception as e:
|
||||
await message.answer("Ваша ссылка не корректна, попробуйте повторить копирование!")
|
||||
else:
|
||||
await message.answer("Ваше сообщение не корректно, необходима ссылка, что отобразилась в строке поиске после"
|
||||
" подтверждения доступа!")
|
||||
|
||||
|
||||
# ПРОВЕРКА СТРАНИЦ НА НОВЫЕ ПОСТЫ
|
||||
async def timer(wait_for):
|
||||
while True:
|
||||
await asyncio.sleep(wait_for)
|
||||
|
||||
notif_group = [x[0] for x in db.all_notifGroup()]
|
||||
activ_user = [[x[0], list(map(int, x[1].split(';')))] for x in db.all_activUser() if x[1]]
|
||||
sub_user = [[x[0], list(map(int, x[1].split(';')))] for x in db.all_subUser() if x[1]]
|
||||
notif_group = list(map(lambda x: x[0], db.all_notifGroup()))
|
||||
activ_user = list(map(lambda x: [x[0], list(map(int, x[1].split(';')))], db.all_activUser()))
|
||||
sub_user = list(map(lambda x: [x[0], list(map(int, x[1].split(';')))], db.all_subUser()))
|
||||
|
||||
for group_id in notif_group:
|
||||
await asyncio.sleep(1) # пауза между страницами — лимиты VK API
|
||||
time.sleep(1)
|
||||
|
||||
tokens = [x[0] for x in activ_user if group_id in x[1]]
|
||||
users = [x[0] for x in sub_user if group_id in x[1]]
|
||||
|
||||
if tokens:
|
||||
user_id = tokens[random.randrange(len(tokens))]
|
||||
vk = VkParser(db.get_token(user_id))
|
||||
else: vk = VkParser(config.serviceToken)
|
||||
|
||||
try:
|
||||
await check_group(group_id, activ_user, sub_user)
|
||||
except Exception:
|
||||
logging.error("Ошибка обновления поста:", exc_info=True)
|
||||
output = vk.last_post(owner_id=du.get_vk_id(group_id))
|
||||
last_post = db.get_postGroup(group_id)
|
||||
|
||||
if output[3] > last_post:
|
||||
text, audio, media = pars_post(types, output)
|
||||
db.update_postGroup(group_id, output[3])
|
||||
|
||||
for sub in users:
|
||||
tg_id = du.get_tg_id(sub)
|
||||
|
||||
if audio and media:
|
||||
post_message = await bot.send_media_group(chat_id=tg_id, media=media)
|
||||
await bot.send_media_group(chat_id=tg_id, media=audio, reply_to_message_id=post_message[0].message_id)
|
||||
elif audio and media == []:
|
||||
post_message = await bot.send_message(chat_id=tg_id, text=text, parse_mode=types.ParseMode.MARKDOWN)
|
||||
await bot.send_media_group(chat_id=tg_id, media=audio, reply_to_message_id=post_message[0].message_id)
|
||||
elif audio == [] and media:
|
||||
await bot.send_media_group(chat_id=tg_id, media=media)
|
||||
else:
|
||||
await bot.send_message(chat_id=tg_id, text=text, parse_mode=types.ParseMode.MARKDOWN)
|
||||
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
dp.include_router(router)
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
|
||||
|
||||
|
||||
async def process() -> None:
|
||||
await asyncio.gather(main(), timer(60))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try: asyncio.run(process())
|
||||
except KeyboardInterrupt: pass
|
||||
if __name__ == '__main__':
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(timer(60)) # ПРОВЕРКА КАЖДУЮ МИНУТУ
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import os
|
||||
|
||||
# TOKENS
|
||||
botToken = os.getenv("BOT_TOKEN", "XXXXXXXXXXXXXXXXXXXXXXXX") # @vkportalbot
|
||||
serviceToken = os.getenv("VK_SERVICE_TOKEN", "") # сервисный ключ приложения VK
|
||||
botToken = '**********'
|
||||
serviceToken = "**********"
|
||||
|
||||
# URL
|
||||
loginUrl = os.getenv("VK_LOGIN_URL", "https://oauth.vk.com/authorize?client_id=6121396&scope=215985366"
|
||||
"&redirect_uri=https://oauth.vk.com/blank.html&display=page"
|
||||
"&response_type=token&revoke=1")
|
||||
loginUrl = "************"
|
||||
checkUrl = "https://oauth.vk.com/blank.html#access_token="
|
||||
|
||||
# VARIABILITY
|
||||
|
|
@ -16,4 +12,3 @@ anonymous = {True: 'анонимный опрос', False: 'не анонимн
|
|||
|
||||
# DATA
|
||||
data = {0: 'Б', 1: 'КБ', 2: 'МБ', 3: 'ГБ', 4: 'ТБ'}
|
||||
dura = {0: 'с.', 1: 'мин.'}
|
||||
|
|
|
|||
306
code/handlers.py
306
code/handlers.py
|
|
@ -1,306 +0,0 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiogram import types, F, Router
|
||||
from aiogram.types import Message, CallbackQuery, ContentType
|
||||
from aiogram.filters import Command
|
||||
|
||||
from emoji import emojize
|
||||
|
||||
from init import bot, du, db
|
||||
from config import checkUrl, loginUrl, serviceToken
|
||||
from vk_scripts import VkParser
|
||||
from scripts import login, pars_post, send_post
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(Command("start", "help"))
|
||||
async def start(msg: Message) -> None:
|
||||
login(msg.chat.id, du, db)
|
||||
|
||||
buttons = [[types.InlineKeyboardButton(text="КОМАНДЫ", callback_data="com"),
|
||||
types.InlineKeyboardButton(text="АВТОР", callback_data="auth")]]
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
await msg.answer(text=f'<b>Portal in VK</b> - это бот для перепоста постов со страниц в социальной сети ВКонтакте. '
|
||||
f'Для начала работы вам нужно всего лишь вызвать команду <b>/add</b> и добавить к ней '
|
||||
f'короткое имя личной или публичной страницы. После этого, бот начнёт присылать каждый новый '
|
||||
f'пост с этой страницы форматируя данные с неё под сообщение в Telegram. С полным '
|
||||
f'функционалом бота, вы можете ознакомиться по кнопке <b>КОМАНДЫ</b>\n\n'
|
||||
|
||||
f'Если вы хотите ставить лайки или получать уведомления от закрытых страниц на которые '
|
||||
f'подписаны в социальной сети, вам необходимо скинуть ссылку из поисковой строки в чат, '
|
||||
f'после подтверждения на <a href="{loginUrl}">сайте</a>', reply_markup=keyboard)
|
||||
|
||||
|
||||
# ИЛАЙН КЛАВИАТУРА HELP
|
||||
@router.callback_query(F.data == "com")
|
||||
async def function(call: CallbackQuery) -> None:
|
||||
await call.message.answer(text='<b>| КОМАНДЫ |</b>\n\n'
|
||||
'<b>/like</b> - лайк на пост, который вы отметили\n'
|
||||
'<b>/list</b> - список страниц от, которых вы получаете уведомления\n'
|
||||
'<b>/notif</b> - отписка или подписка от всех уведомлений\n'
|
||||
'<b>/last_post</b> - получение последнего поста по короткому имени страницы\n'
|
||||
'<b>/add</b> и <b>/del</b> - добавление и удаление странички из списка подписок\n'
|
||||
'<b>/update</b> - ответом на пост: прислать его заново с актуальными данными\n'
|
||||
'<b>/logout</b> - удалить сохранённый токен VK\n')
|
||||
|
||||
|
||||
@router.callback_query(F.data == "auth")
|
||||
async def author(call: CallbackQuery) -> None:
|
||||
await call.message.answer(text='<b>| АВТОР |</b>\n\n<b>>></b> Этот бот не коммерческий проект, для получения '
|
||||
'постов из социальной сети ВКонтакте. Бот работает на сервисном токене VK API, '
|
||||
'пока вы не предоставите собственный. Ваш токен нужен для получения постов со '
|
||||
'страниц, доступ к которым есть исключительно у вас. Также это даст вам '
|
||||
'возможность ставить через Telegram лайки на посты в самой социальной сети!'
|
||||
|
||||
'\n\nЯ же пишу подобные небольшие проекты, о которых вы можете узнать '
|
||||
'больше на моём <a href="https://github.com/EDeev">GitHub</a>.')
|
||||
|
||||
|
||||
# КОМАНДЫ
|
||||
@router.message(Command("notif"))
|
||||
async def notification(msg: Message) -> None:
|
||||
user_id = login(msg.chat.id, du, db)
|
||||
status = db.get_status(user_id)
|
||||
|
||||
# у пользователя может ещё не быть ни одной подписки
|
||||
groups = db.get_user_groups(user_id)
|
||||
for group in map(int, groups.split(";") if groups else []):
|
||||
db.update_countGroup(group, -1 if status else 1)
|
||||
|
||||
if status: await msg.answer("Получение постов из ВК выключено!")
|
||||
else: await msg.answer("Получение постов из ВК включено!")
|
||||
db.update_status(user_id)
|
||||
|
||||
|
||||
@router.message(Command("add", "del"))
|
||||
async def add_del(msg: Message) -> None:
|
||||
user_id = login(msg.chat.id, du, db)
|
||||
|
||||
domain = msg.text.split()[-1]
|
||||
command = msg.text.split()[0]
|
||||
|
||||
if "/" not in domain:
|
||||
try:
|
||||
token = db.get_token(user_id)
|
||||
if token: vk = VkParser(token)
|
||||
else: vk = VkParser(serviceToken)
|
||||
|
||||
group_id, typ, last_post = await asyncio.to_thread(vk.login, domain)
|
||||
|
||||
if not du.group_exists(group_id):
|
||||
du.add_group(group_id)
|
||||
group_id = du.get_group_id(group_id)
|
||||
|
||||
if not db.infoGroup_exists(group_id):
|
||||
db.add_infoGroup(group_id, typ, last_post)
|
||||
|
||||
groups = db.get_user_groups(user_id)
|
||||
user_groups = groups.split(";") if groups else []
|
||||
|
||||
if str(group_id) not in user_groups:
|
||||
if command == "/del":
|
||||
await msg.answer("Вы и так не подписаны на эту группу!")
|
||||
else:
|
||||
if db.get_countUser(user_id) < 10:
|
||||
user_groups.append(str(group_id))
|
||||
db.update_user_groups(user_id, ";".join(user_groups))
|
||||
|
||||
db.update_countUser(user_id, 1)
|
||||
if db.get_status(user_id): db.update_countGroup(group_id, 1)
|
||||
|
||||
await msg.answer("Группа успешно добавлена в список уведомлений!")
|
||||
else:
|
||||
await msg.answer("У вас добавлено максимально количество групп!")
|
||||
else:
|
||||
if command == "/add":
|
||||
await msg.answer("Вы уже подписаны на эту страницу!")
|
||||
else:
|
||||
user_groups.remove(str(group_id))
|
||||
db.update_user_groups(user_id, ";".join(user_groups))
|
||||
|
||||
db.update_countUser(user_id, -1)
|
||||
if db.get_status(user_id): db.update_countGroup(group_id, -1)
|
||||
|
||||
await msg.answer("Группа успешно удалена из списка уведомлений!")
|
||||
except Exception:
|
||||
logging.info("Нет доступа к страницы:", exc_info=True)
|
||||
await msg.answer("<b>Произошла ошибка!</b> Проверьте правильность написания <b>короткого адреса</b> "
|
||||
"страницы, <b>имеете ли вы доступ</b> к этой странице и есть ли на ней <b>хотя бы "
|
||||
"один пост</b>!")
|
||||
else:
|
||||
if command == "/add":
|
||||
await msg.answer("Команда введена некорректно, такой страницы не существует или она закрыта для "
|
||||
"просмотра!\n\nКорректный ввод команд: /add <i>example</i>")
|
||||
elif command == "/del":
|
||||
await msg.answer("Команда введена некорректно, такой страницы не существует или она закрыта для "
|
||||
"просмотра!\n\nКорректный ввод команд: /del <i>example</i>")
|
||||
|
||||
|
||||
@router.message(Command("list"))
|
||||
async def lst(msg: Message) -> None:
|
||||
user_id = login(msg.chat.id, du, db)
|
||||
groups = db.get_user_groups(user_id)
|
||||
user_groups = list(map(int, groups.split(";") if groups else []))
|
||||
|
||||
if user_groups:
|
||||
group_ids = [du.get_vk_id(id) for id in user_groups]
|
||||
|
||||
vk = VkParser(serviceToken)
|
||||
peoples, groups = await asyncio.to_thread(vk.info, group_ids)
|
||||
|
||||
text = "<b><i>* Список страниц, от которых вы получаете уведомления!</i></b>\n"
|
||||
|
||||
if peoples:
|
||||
text += "\n<b>Личные страницы пользователей</b>\n"
|
||||
for i, people in enumerate(peoples):
|
||||
text += f'{emojize(":green_circle:") if people[2] else emojize(":red_circle:")} ' \
|
||||
f'{i + 1}. {people[1]} <a href="https://vk.com/{people[0]}">@{people[0]}</a>\n'
|
||||
|
||||
if groups:
|
||||
text += "\n<b>Группы / сообщества / паблики</b>\n"
|
||||
for i, group in enumerate(groups):
|
||||
text += f'{i + 1}. {group[1]} <a href="https://vk.com/{group[0]}">@{group[0]}</a>\n'
|
||||
|
||||
await msg.answer(text, disable_web_page_preview=True)
|
||||
else:
|
||||
await msg.answer("Вы <b>не добавили ни одной страницы</b> для отслеживания публикаций!")
|
||||
|
||||
|
||||
@router.message(Command("like"))
|
||||
async def like(msg: Message) -> None:
|
||||
user_id = login(msg.chat.id, du, db)
|
||||
token = db.get_token(user_id)
|
||||
|
||||
if token and (msg.reply_to_message is not None):
|
||||
url = ''
|
||||
|
||||
if msg.reply_to_message.entities is not None:
|
||||
for i in list(msg.reply_to_message.entities):
|
||||
i = dict(i)
|
||||
if i['type'] == 'text_link':
|
||||
url = i['url']; break
|
||||
elif msg.reply_to_message.caption_entities is not None:
|
||||
for i in list(msg.reply_to_message.caption_entities):
|
||||
i = dict(i)
|
||||
if i['type'] == 'text_link':
|
||||
url = i['url']; break
|
||||
|
||||
if url:
|
||||
try:
|
||||
[group_id, last_post] = list(map(int, url.split('wall')[-1].split('_')))
|
||||
|
||||
vk = VkParser(token)
|
||||
await asyncio.to_thread(vk.like, group_id, last_post)
|
||||
|
||||
await msg.answer('Вы поставили лайк 😉')
|
||||
except Exception:
|
||||
logging.error("Не удалось поставить лайк:", exc_info=True)
|
||||
await msg.answer('По <b>неизвестной причине</b> не удалось поставить лайк!')
|
||||
else:
|
||||
await msg.answer('Вы <b>ответили не на тот пост</b>, для отправки лайка!')
|
||||
else:
|
||||
await msg.answer('Вы <b>не отметили пост</b> который хотите лайкнуть или <b>не прислали свой токен</b> для '
|
||||
'возможности лайкать посты!')
|
||||
|
||||
|
||||
@router.message(Command("update"))
|
||||
async def update(msg: Message) -> None:
|
||||
user_id = msg.chat.id
|
||||
short_id = login(msg.chat.id, du, db)
|
||||
|
||||
if msg.reply_to_message is not None:
|
||||
url = ''
|
||||
|
||||
if msg.reply_to_message.entities is not None:
|
||||
for i in list(msg.reply_to_message.entities):
|
||||
i = dict(i)
|
||||
if i['type'] == 'text_link':
|
||||
url = i['url']; break
|
||||
elif msg.reply_to_message.caption_entities is not None:
|
||||
for i in list(msg.reply_to_message.caption_entities):
|
||||
i = dict(i)
|
||||
if i['type'] == 'text_link':
|
||||
url = i['url']; break
|
||||
|
||||
if url:
|
||||
try:
|
||||
[group_id, last_post] = list(map(int, url.split('wall')[-1].split('_')))
|
||||
|
||||
token = db.get_token(short_id)
|
||||
if token: vk = VkParser(token)
|
||||
else: vk = VkParser(serviceToken)
|
||||
|
||||
output = await asyncio.to_thread(vk.last_post, post=f"{group_id}_{last_post}")
|
||||
text, audio, media = pars_post(output)
|
||||
|
||||
await bot.delete_message(chat_id=user_id, message_id=msg.reply_to_message.message_id)
|
||||
await bot.delete_message(chat_id=user_id, message_id=msg.message_id)
|
||||
|
||||
await send_post(bot, user_id, text, audio, media)
|
||||
|
||||
except Exception:
|
||||
logging.error("Ошибка обновления поста:", exc_info=True)
|
||||
await msg.answer(text='По неизвестной причине не удалось обновить пост!')
|
||||
else: await msg.answer(text='Вы ответили не на тот пост!')
|
||||
else: await msg.answer(text='Вы не отметили пост который хотите обновить!')
|
||||
|
||||
|
||||
# ВЗАИМОДЕЙСТВИЕ С ДАННЫМИ ВК ПОСТОВ
|
||||
@router.message(Command("last_post"))
|
||||
async def last_post(msg: Message) -> None:
|
||||
user_id = msg.chat.id
|
||||
short_id = login(user_id, du, db)
|
||||
|
||||
domain = msg.text.split()[-1]
|
||||
if "/" not in domain:
|
||||
try:
|
||||
token = db.get_token(short_id)
|
||||
if token: vk = VkParser(token)
|
||||
else: vk = VkParser(serviceToken)
|
||||
|
||||
output = await asyncio.to_thread(vk.last_post, domain=domain)
|
||||
text, audio, media = pars_post(output)
|
||||
|
||||
await send_post(bot, user_id, text, audio, media)
|
||||
|
||||
except Exception:
|
||||
logging.info("Некорректное/недоступное короткое имя:", exc_info=True)
|
||||
await msg.answer("Короткое <b>имя страницы неверное</b> или у вас <b>нет доступа</b> к этой странице!")
|
||||
else: await msg.answer("Команда введена некорректно! Корректный ввод: /last_post <b>example</b>")
|
||||
|
||||
|
||||
@router.message(Command("logout"))
|
||||
async def logout(msg: Message) -> None:
|
||||
user_id = login(msg.chat.id, du, db)
|
||||
|
||||
if db.get_token(user_id):
|
||||
db.update_token(user_id, None)
|
||||
await msg.answer("Токен VK удалён, бот снова работает на сервисном ключе. Полностью отозвать доступ "
|
||||
"можно в настройках VK: <b>Приложения и сайты</b>.")
|
||||
else:
|
||||
await msg.answer("Сохранённого токена нет.")
|
||||
|
||||
|
||||
# ПОЛУЧЕНИЕ ТОКЕНА ПОЛЬЗОВАТЕЛЯ
|
||||
@router.message(F.content_type == ContentType.TEXT)
|
||||
async def url(msg: Message) -> None:
|
||||
user_id = login(msg.chat.id, du, db)
|
||||
|
||||
if checkUrl in msg.text:
|
||||
try:
|
||||
token = msg.text.split("&")[0].split("=")[-1]
|
||||
await asyncio.to_thread(VkParser(token).check)
|
||||
|
||||
db.update_token(user_id, token)
|
||||
# ссылка с токеном не должна оставаться в переписке
|
||||
try: await msg.delete()
|
||||
except Exception: pass
|
||||
await msg.answer("Токен успешно сохранён! Удалить его можно командой /logout")
|
||||
except Exception:
|
||||
logging.info("Некорректная ссылка:", exc_info=True)
|
||||
await msg.answer("Ваша <b>ссылка не корректна</b>, попробуйте повторить копирование!")
|
||||
else: await msg.answer("Ваше <b>сообщение не корректно</b>, необходима ссылка, что отобразилась в строке "
|
||||
"поиске после подтверждения доступа!")
|
||||
21
code/init.py
21
code/init.py
|
|
@ -1,21 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.enums.parse_mode import ParseMode
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.client.bot import DefaultBotProperties
|
||||
|
||||
from config import botToken
|
||||
from sql import Users, Base
|
||||
|
||||
bot = Bot(token=botToken, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
dp = Dispatcher(storage=MemoryStorage())
|
||||
|
||||
os.makedirs("../db", exist_ok=True)
|
||||
du = Users('../db/users.db')
|
||||
db = Base('../db/base.db')
|
||||
|
||||
# лог в файл, если задан LOG_FILE (так бот работал на сервере), иначе — в консоль
|
||||
logging.basicConfig(level=logging.INFO, filename=os.getenv("LOG_FILE") or None,
|
||||
format="%(asctime)s %(levelname)s %(message)s")
|
||||
119
code/scripts.py
119
code/scripts.py
|
|
@ -1,12 +1,5 @@
|
|||
from html import escape
|
||||
|
||||
from aiogram import types
|
||||
|
||||
import config
|
||||
|
||||
CAPTION_LIMIT = 1024 # подпись к фото в Telegram
|
||||
TEXT_LIMIT = 4096 # обычное сообщение
|
||||
|
||||
|
||||
def login(user_id, du, db):
|
||||
user_id = int(user_id)
|
||||
|
|
@ -18,109 +11,47 @@ def login(user_id, du, db):
|
|||
return user_id
|
||||
|
||||
|
||||
def pars_post(output):
|
||||
"""Пост из vk_scripts.get_output → текст в HTML, аудио и медиа для Telegram.
|
||||
Всё, что пришло из ВК, экранируется: «<» или «&» в посте иначе ломают отправку"""
|
||||
media, video, url, audio, poll, link = [], [], [], [], [], []
|
||||
def pars_post(types, output):
|
||||
media, ttl, url, audio, poll, link = [], [], [], [], [], []
|
||||
|
||||
for group in output[0]:
|
||||
if group[0] == "poll": poll = group[1]
|
||||
elif group[0] == "link": link.append(group[1])
|
||||
elif group[0] == "video": video.append(group[1])
|
||||
elif group[0] == "video": ttl.append(group[1])
|
||||
elif group[0] == "doc" and group[1][2] != 4: url.append([group[1][0], group[1][1], group[1][3]])
|
||||
elif group[0] == "audio": audio.append(types.InputMediaAudio(
|
||||
media=group[1][1], caption=f"<b>Название: {escape(group[1][0])} / Автор: {escape(group[1][2])}</b>"))
|
||||
elif group[0] == "audio": audio.append(types.InputMediaAudio(media=group[1][1], parse_mode=types.ParseMode.MARKDOWN,
|
||||
caption=f"*Название: {group[1][0]} / Автор: {group[1][2]}*"))
|
||||
|
||||
if output[1] != "":
|
||||
# упоминания ВК вида [id1|Имя] превращаются в ссылки
|
||||
post = [elem_2 for elem_1 in escape(output[1], quote=False).split("[") for elem_2 in elem_1.split("]")]
|
||||
for i, elem in enumerate(post):
|
||||
if "|" in elem and i != 0:
|
||||
elem = elem.split("|", 1)
|
||||
post[i] = f"<a href='https://vk.com/{escape(elem[0])}'>{elem[1]}</a>"
|
||||
|
||||
text = f"{output[2]}\n\n{''.join(post)}\n"
|
||||
if output[1] != "": text = f"{output[2]}\n\n*{output[1]}*\n"
|
||||
else: text = f"{output[2]}\n"
|
||||
|
||||
if poll:
|
||||
text += (f"\n<b>> Проводится</b> {config.anonymous[poll[3]]} {config.unvote[poll[2]]}! "
|
||||
f"<b>Вопрос:</b> {escape(poll[0])} <b>[{poll[1]}]</b>\n")
|
||||
text += ''.join([f'<b>{i + 1}. Ответ:</b> {escape(ans[0])} <b>[{ans[1]}]</b>\n'
|
||||
for i, ans in enumerate(poll[4])]) + "\n"
|
||||
text += f"\n*> Проводиться {config.anonymous[poll[3]]} {config.unvote[poll[2]]}!"
|
||||
text += f"*\n*---------------------*\n*- Вопрос: {poll[0]} [{poll[1]}]*\n"
|
||||
text += ''.join([f'*{i}. Ответ: {ans[0]} [{ans[1]}]*\n' for i, ans in enumerate(poll[4])])
|
||||
text += "*---------------------*\n"
|
||||
|
||||
if video:
|
||||
for frame in video:
|
||||
text += f"\n<b>Видео</b> <i>« {escape(frame[0])} »</i>\n"
|
||||
|
||||
k, dura = 0, frame[1]
|
||||
if dura > 60: dura, k = dura // 60, 1
|
||||
|
||||
text += f"<b>Длительность:</b> {dura} {config.dura[k]}; <b>Просмотров:</b> {frame[2]};\n"
|
||||
if ttl: text += f"\n*Видео -* находятся в оригинальном посте в количестве {len(ttl)} записей\n"
|
||||
|
||||
if url:
|
||||
for data in url:
|
||||
k, dt = 0, data[2]
|
||||
while dt > 1024 and k < len(config.data) - 1:
|
||||
dt, k = dt // 1024, k + 1
|
||||
text += (f"\n<b>Документ - [</b> <a href='{escape(data[1])}'>{escape(data[0])}</a> "
|
||||
f"({dt} {config.data[k]}) <b>]</b>")
|
||||
while True:
|
||||
if dt > 1024: dt = dt // 1024; k += 1
|
||||
else: break
|
||||
text += f"\n*Документ - [* [{data[0]}]({data[1]}) ({dt} {config.data[k]}) *]*"
|
||||
text += "\n"
|
||||
|
||||
if link:
|
||||
text += ''.join([f"\n<b>Ссылка - [</b> <a href='{escape(s[1])}'>{escape(s[0])}</a> <b>]</b>"
|
||||
for s in link]) + "\n"
|
||||
if audio: text += '\n<b>« Также присутствуют аудио файлы »</b>'
|
||||
if link: text += ''.join([f"\n*Ссылка - [* [{s[0]}]({s[1]}) *]*" for s in link]) + "\n"
|
||||
if audio: text += '\n*« Также присутствуют аудио файлы »*'
|
||||
|
||||
for group in output[0]:
|
||||
for i, group in enumerate(output[0]):
|
||||
if group[0] == "photo":
|
||||
media.append(types.InputMediaPhoto(media=group[1]))
|
||||
elif group[0] == "video" and group[1][3]: # у видео может не быть обложки
|
||||
media.append(types.InputMediaPhoto(media=group[1][3]))
|
||||
elif group[0] == "doc" and group[1][2] == 4:
|
||||
media.append(types.InputMediaPhoto(media=group[1][1]))
|
||||
if i: media.append(types.InputMediaPhoto(media=group[1]))
|
||||
else: media.append(types.InputMediaPhoto(media=group[1], caption=text, parse_mode=types.ParseMode.MARKDOWN))
|
||||
elif group[0] == "doc":
|
||||
if group[1][2] == 4:
|
||||
if i: media.append(types.InputMediaPhoto(media=group[1][1]))
|
||||
else: media.append(types.InputMediaPhoto(media=group[1][1], caption=text, parse_mode=types.ParseMode.MARKDOWN))
|
||||
|
||||
return text, audio[:10], media[:10] # в одном альбоме Telegram не больше 10 элементов
|
||||
|
||||
|
||||
def split_text(text, limit=TEXT_LIMIT):
|
||||
"""Делит длинный текст по строкам, чтобы не разрывать HTML-теги посередине"""
|
||||
parts, current = [], ""
|
||||
for line in text.split("\n"):
|
||||
while len(line) > limit: # одна строка длиннее лимита — режем как есть
|
||||
if current: parts.append(current); current = ""
|
||||
parts.append(line[:limit]); line = line[limit:]
|
||||
if len(current) + len(line) + 1 > limit:
|
||||
parts.append(current); current = ""
|
||||
current += line + "\n"
|
||||
if current.strip(): parts.append(current)
|
||||
return parts
|
||||
|
||||
|
||||
async def send_post(bot, chat_id, text, audio, media):
|
||||
"""Отправляет пост с учётом ограничений Telegram: альбом — от 2 до 10 элементов, подпись —
|
||||
до 1024 символов, сообщение — до 4096"""
|
||||
first = None
|
||||
|
||||
if media:
|
||||
caption = text if len(text) <= CAPTION_LIMIT else None
|
||||
if len(media) == 1:
|
||||
first = await bot.send_photo(chat_id=chat_id, photo=media[0].media, caption=caption)
|
||||
else:
|
||||
media[0].caption = caption
|
||||
first = (await bot.send_media_group(chat_id=chat_id, media=media))[0]
|
||||
if caption is None:
|
||||
for part in split_text(text):
|
||||
await bot.send_message(chat_id=chat_id, text=part, disable_web_page_preview=True,
|
||||
reply_to_message_id=first.message_id)
|
||||
else:
|
||||
for part in split_text(text):
|
||||
sent = await bot.send_message(chat_id=chat_id, text=part, disable_web_page_preview=True)
|
||||
first = first or sent
|
||||
|
||||
if audio:
|
||||
reply_to = first.message_id if first else None
|
||||
if len(audio) == 1:
|
||||
await bot.send_audio(chat_id=chat_id, audio=audio[0].media, caption=audio[0].caption,
|
||||
reply_to_message_id=reply_to)
|
||||
else:
|
||||
await bot.send_media_group(chat_id=chat_id, media=audio, reply_to_message_id=reply_to)
|
||||
return text, audio, media
|
||||
|
|
|
|||
11
code/sql.py
11
code/sql.py
|
|
@ -7,11 +7,6 @@ class Users:
|
|||
self.connection = sqlite3.connect(database)
|
||||
self.cursor = self.connection.cursor()
|
||||
|
||||
# на новой установке (например, в Docker с пустым томом) таблиц ещё нет
|
||||
with self.connection:
|
||||
self.cursor.execute("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL)")
|
||||
self.cursor.execute("CREATE TABLE IF NOT EXISTS \"group\" (id INTEGER PRIMARY KEY, group_id INTEGER NOT NULL)")
|
||||
|
||||
# КОМАНДЫ USER
|
||||
def user_exists(self, user_id):
|
||||
"""Проверяем, есть ли уже пользователь в базе"""
|
||||
|
|
@ -68,12 +63,6 @@ class Base:
|
|||
self.connection = sqlite3.connect(database)
|
||||
self.cursor = self.connection.cursor()
|
||||
|
||||
with self.connection:
|
||||
self.cursor.execute("CREATE TABLE IF NOT EXISTS user (user_id INTEGER NOT NULL, status BOOLEAN NOT NULL "
|
||||
"DEFAULT (True), count INTEGER NOT NULL DEFAULT (0), groups TEXT, token TEXT)")
|
||||
self.cursor.execute("CREATE TABLE IF NOT EXISTS \"group\" (group_id INTEGER NOT NULL, type BOOLEAN NOT NULL, "
|
||||
"count INTEGER NOT NULL DEFAULT (0), last_post INTEGER NOT NULL)")
|
||||
|
||||
# КОМАНДЫ USER
|
||||
def infoUser_exists(self, user_id):
|
||||
"""Проверяем, есть ли данные уже в базе"""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from html import escape
|
||||
|
||||
import vk_api
|
||||
|
||||
|
||||
|
|
@ -40,72 +38,39 @@ class VkParser:
|
|||
|
||||
return peoples, groups
|
||||
|
||||
def last_post(self, owner_id=None, domain=None, post=None):
|
||||
if post: wall = self.vk.wall.getById(posts=post, extended=1)
|
||||
else: wall = self.vk.wall.get(owner_id=owner_id, domain=domain, count=2, extended=1)
|
||||
|
||||
def last_post(self, owner_id=None, domain=None):
|
||||
wall = self.vk.wall.get(owner_id=owner_id, domain=domain, count=2, extended=1)
|
||||
post = max([[int(pt["id"]), pt] for pt in wall["items"]])
|
||||
owner_id = post[1]['owner_id']
|
||||
|
||||
if owner_id > 0:
|
||||
[name, domain] = [[per['first_name'] + ' ' + per['last_name'], per['screen_name']] for per in wall["profiles"] if per["id"] == owner_id][0]
|
||||
else:
|
||||
[name, domain] = [[gro['name'], gro['screen_name']] for gro in wall["groups"] if gro["id"] == int(str(owner_id)[1:])][0]
|
||||
[post_id, post] = post
|
||||
owner_id, output = post['owner_id'], []
|
||||
|
||||
if 'copy_history' in post[1]:
|
||||
comment, orig_id = post[1]['text'], post[0]
|
||||
if owner_id > 0: [name, domain] = [[per['first_name'] + ' ' + per['last_name'], per['screen_name']]
|
||||
for per in wall["profiles"] if per["id"] == owner_id][0]
|
||||
else: [name, domain] = [[gro['name'], gro['screen_name']]
|
||||
for gro in wall["groups"] if gro["id"] == int(str(owner_id)[1:])][0]
|
||||
|
||||
post = [post[1]['copy_history'][0]['id'], post[1]['copy_history'][0]]
|
||||
owner_id_r = post[1]['owner_id']
|
||||
attachments = post['attachments']
|
||||
types = [[typ['type'], typ] for typ in attachments]
|
||||
|
||||
if owner_id_r > 0:
|
||||
[name_r, domain_r] = [[per['first_name'] + ' ' + per['last_name'], per['screen_name']] for per in wall["profiles"] if per["id"] == owner_id_r][0]
|
||||
else:
|
||||
[name_r, domain_r] = [[gro['name'], gro['screen_name']] for gro in wall["groups"] if gro["id"] == int(str(owner_id_r)[1:])][0]
|
||||
for typ in types:
|
||||
if typ[0] == 'photo':
|
||||
output.append([typ[0], typ[1]['photo']['sizes'][-1]['url']])
|
||||
elif typ[0] == 'video':
|
||||
output.append([typ[0], typ[1]['video']['title']])
|
||||
elif typ[0] == "doc":
|
||||
output.append([typ[0], [typ[1]['doc']['title'], typ[1]['doc']['url'],
|
||||
typ[1]['doc']['type'], typ[1]['doc']['size']]])
|
||||
elif typ[0] == "audio":
|
||||
output.append([typ[0], [typ[1]['audio']['title'], typ[1]['audio']['url'], typ[1]['audio']['artist']]])
|
||||
elif typ[0] == "poll":
|
||||
answer = [[que['text'], que['votes']] for que in typ[1]['poll']['answers']]
|
||||
output.append([typ[0], [typ[1]['poll']['question'], typ[1]['poll']['votes'],
|
||||
typ[1]['poll']['disable_unvote'], typ[1]['poll']['anonymous'], answer]])
|
||||
elif typ[0] == "link":
|
||||
output.append([typ[0], [typ[1]['link']['title'], typ[1]['link']['url']]])
|
||||
|
||||
return get_output(post, name_r, domain_r, [orig_id, owner_id, name, domain, comment])
|
||||
else:
|
||||
return get_output(post, name, domain)
|
||||
return [output, post['text'],
|
||||
f"*Автор поста -* [{name}](https://vk.com/{domain}?w=wall{owner_id}_{post_id})", post_id]
|
||||
|
||||
|
||||
def get_output(post, name, domain, repost=None):
|
||||
[post_id, post] = post
|
||||
owner_id, output = post['owner_id'], []
|
||||
|
||||
attachments = post.get('attachments', [])
|
||||
types = [[typ['type'], typ] for typ in attachments]
|
||||
|
||||
for typ in types:
|
||||
if typ[0] == 'photo':
|
||||
output.append([typ[0], typ[1]['photo']['sizes'][-1]['url']])
|
||||
elif typ[0] == 'video':
|
||||
video = typ[1]['video']
|
||||
# обложка: самая крупная из доступных
|
||||
frame = video.get('photo_1280') or video.get('photo_800') or video.get('photo_320')
|
||||
if frame is None and video.get('image'):
|
||||
frame = video['image'][-1]['url']
|
||||
|
||||
output.append([typ[0], [video['title'], video.get('duration', 0), video.get('views', 0), frame]])
|
||||
elif typ[0] == "doc":
|
||||
output.append([typ[0], [typ[1]['doc']['title'], typ[1]['doc']['url'],
|
||||
typ[1]['doc']['type'], typ[1]['doc']['size']]])
|
||||
elif typ[0] == "audio":
|
||||
output.append([typ[0], [typ[1]['audio']['title'], typ[1]['audio']['url'], typ[1]['audio']['artist']]])
|
||||
elif typ[0] == "poll":
|
||||
answer = [[que['text'], que['votes']] for que in typ[1]['poll']['answers']]
|
||||
output.append([typ[0], [typ[1]['poll']['question'], typ[1]['poll']['votes'],
|
||||
typ[1]['poll']['disable_unvote'], typ[1]['poll']['anonymous'], answer]])
|
||||
elif typ[0] == "link":
|
||||
output.append([typ[0], [typ[1]['link']['title'], typ[1]['link']['url']]])
|
||||
|
||||
if repost:
|
||||
[author_id, author_owner_id, author_name, author_domain, author_comment] = repost
|
||||
|
||||
comment = f"<b>Автор репоста -</b> <a href='https://vk.com/{author_domain}?w=wall{author_owner_id}_{author_id}'>{escape(author_name)}</a>"
|
||||
if author_comment != "": comment += f"\n<blockquote>{escape(author_comment)}</blockquote>\n"
|
||||
comment += f"\n<b>Автор поста -</b> <a href='https://vk.com/{domain}?w=wall{owner_id}_{post_id}'>{escape(name)}</a>"
|
||||
|
||||
return [output, f"{post['text']}", comment, author_id]
|
||||
else:
|
||||
return [output, f"{post['text']}",
|
||||
f"<b>Автор поста -</b> <a href='https://vk.com/{domain}?w=wall{owner_id}_{post_id}'>{escape(name)}</a>", post_id]
|
||||
11
compose.yaml
11
compose.yaml
|
|
@ -1,11 +0,0 @@
|
|||
services:
|
||||
bot:
|
||||
build: .
|
||||
image: ghcr.io/edeev/vkrepost_to_tg:latest
|
||||
env_file: .env
|
||||
volumes:
|
||||
- db:/app/db
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
db:
|
||||
BIN
db/base.db
Normal file
BIN
db/base.db
Normal file
Binary file not shown.
BIN
db/users.db
Normal file
BIN
db/users.db
Normal file
Binary file not shown.
|
|
@ -1,3 +0,0 @@
|
|||
-r requirements.txt
|
||||
pytest==8.4.2
|
||||
ruff==0.14.0
|
||||
|
|
@ -1,3 +1,18 @@
|
|||
aiogram==3.23.0
|
||||
vk_api==11.10.1
|
||||
emoji==2.16.0
|
||||
# Telegram Bot Framework
|
||||
aiogram==2.25.1
|
||||
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "code"))
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
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 < 2 & <a href='https://vk.com/id5'>Иван</a> пишет <b>" in text
|
||||
assert "Паблик <&>" 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&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
|
||||
Loading…
Add table
Reference in a new issue