1
0
Fork 0
mirror of https://github.com/EDeev/vkrepost_to_tg.git synced 2026-10-08 04:59:45 +03:00

Исправлены ошибки пересылки постов

- фоновая рассылка падала на каждом новом посте: NameError: name 'types' is not defined
  (видно в логе сервера) — бот не пересылал ничего;
- пост с одной картинкой не отправлялся: send_media_group принимает от 2 до 10 элементов;
  больше 10 фото — тоже ошибка; теперь одиночное фото — send_photo, альбом обрезается до 10;
- подпись длиннее 1024 символов ломала отправку — длинный текст уходит отдельными
  сообщениями (с делением по строкам, чтобы не рвать HTML);
- если первым вложением была ссылка или опрос, текст поста терялся (подпись ставилась
  только на вложение с индексом 0);
- текст и названия из ВК подставлялись в HTML без экранирования; ссылки оформлялись
  Markdown-синтаксисом ([..](..)) при разметке HTML;
- post_message[0] у одиночного сообщения, logging.danger, /notif без подписок — падения;
- у видео без photo_800 разбор поста падал; «Лайков» у видео на самом деле были просмотры;
- синхронные вызовы vk_api и time.sleep блокировали бота — теперь в отдельном потоке и
  asyncio.sleep; ошибка у одного подписчика не мешает остальным.

Новое: /logout — удалить сохранённый токен VK; сообщение со ссылкой-токеном удаляется из
переписки. Токены бота и VK — из переменных окружения, таблицы SQLite создаются при первом
запуске (пустые базы убраны из репозитория), ссылка «АВТОР» — на EDeev.
This commit is contained in:
Деев Егор Викторович 2026-10-05 19:52:45 +00:00
parent 490ea8b5d6
commit a38819dee7
10 changed files with 206 additions and 139 deletions

View file

@ -1,61 +1,60 @@
import asyncio, logging, time, random, scripts, vk_scripts import asyncio
import logging
import random
from init import * import scripts
from config import * import vk_scripts
from init import bot, dp, du, db
from config import serviceToken
from handlers import router from handlers import router
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]]
# токен случайного подписчика даёт доступ к закрытым страницам, иначе — сервисный ключ
if tokens:
vk = vk_scripts.VkParser(db.get_token(random.choice(tokens)))
else: vk = vk_scripts.VkParser(serviceToken)
# 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)
if output[3] > last_post:
text, audio, media = scripts.pars_post(output)
db.update_postGroup(group_id, output[3])
for sub in users:
try:
await scripts.send_post(bot, du.get_tg_id(sub), text, audio, media)
except Exception:
# подписчик мог заблокировать бота — остальным пост всё равно уходит
logging.warning("Не удалось отправить пост подписчику %s", sub, exc_info=True)
async def timer(wait_for) -> None: async def timer(wait_for) -> None:
while True: while True:
await asyncio.sleep(wait_for) await asyncio.sleep(wait_for)
notif_group = list(map(lambda x: x[0], db.all_notifGroup())) notif_group = [x[0] for x in db.all_notifGroup()]
activ_user = list(map(lambda x: [x[0], list(map(int, x[1].split(';')))], db.all_activUser())) activ_user = [[x[0], list(map(int, x[1].split(';')))] for x in db.all_activUser() if x[1]]
sub_user = list(map(lambda x: [x[0], list(map(int, x[1].split(';')))], db.all_subUser())) sub_user = [[x[0], list(map(int, x[1].split(';')))] for x in db.all_subUser() if x[1]]
for group_id in notif_group: for group_id in notif_group:
time.sleep(1) await asyncio.sleep(1) # пауза между страницами — лимиты VK API
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 = vk_scripts.VkParser(db.get_token(user_id))
else: vk = vk_scripts.VkParser(serviceToken)
try: try:
output = vk.last_post(owner_id=du.get_vk_id(group_id)) await check_group(group_id, activ_user, sub_user)
last_post = db.get_postGroup(group_id) except Exception:
if output[3] > last_post:
text, audio, media = scripts.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)
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, disable_web_page_preview=True)
except Exception as err:
logging.error("Ошибка обновления поста:", exc_info=True) logging.error("Ошибка обновления поста:", exc_info=True)
async def main() -> None: async def main() -> None:
dp.include_router(router) dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types(), skipUpdates=True) await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
async def process() -> None: async def process() -> None:
@ -63,7 +62,5 @@ async def process() -> None:
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
try: asyncio.run(process()) try: asyncio.run(process())
except KeyboardInterrupt: pass except KeyboardInterrupt: pass

View file

@ -1,9 +1,13 @@
import os
# TOKENS # TOKENS
botToken = '**********' botToken = os.getenv("BOT_TOKEN", "XXXXXXXXXXXXXXXXXXXXXXXX") # @vkportalbot
serviceToken = "**********" serviceToken = os.getenv("VK_SERVICE_TOKEN", "") # сервисный ключ приложения VK
# URL # URL
loginUrl = "************" 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")
checkUrl = "https://oauth.vk.com/blank.html#access_token=" checkUrl = "https://oauth.vk.com/blank.html#access_token="
# VARIABILITY # VARIABILITY
@ -12,3 +16,4 @@ anonymous = {True: 'анонимный опрос', False: 'не анонимн
# DATA # DATA
data = {0: 'Б', 1: 'КБ', 2: 'МБ', 3: 'ГБ', 4: 'ТБ'} data = {0: 'Б', 1: 'КБ', 2: 'МБ', 3: 'ГБ', 4: 'ТБ'}
dura = {0: 'с.', 1: 'мин.'}

View file

@ -1,13 +1,16 @@
import asyncio
import logging
from aiogram import types, F, Router from aiogram import types, F, Router
from aiogram.types import Message, CallbackQuery, ContentType from aiogram.types import Message, CallbackQuery, ContentType
from aiogram.filters import Command from aiogram.filters import Command
from emoji import emojize from emoji import emojize
from init import * from init import bot, du, db
from config import * from config import checkUrl, loginUrl, serviceToken
from vk_scripts import VkParser from vk_scripts import VkParser
from scripts import login, pars_post from scripts import login, pars_post, send_post
router = Router() router = Router()
@ -18,7 +21,7 @@ async def start(msg: Message) -> None:
buttons = [[types.InlineKeyboardButton(text="КОМАНДЫ", callback_data="com"), buttons = [[types.InlineKeyboardButton(text="КОМАНДЫ", callback_data="com"),
types.InlineKeyboardButton(text="АВТОР", callback_data="auth")]] types.InlineKeyboardButton(text="АВТОР", callback_data="auth")]]
keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons, row_width=2) keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons)
await msg.answer(text=f'<b>Portal in VK</b> - это бот для перепоста постов со страниц в социальной сети ВКонтакте. ' await msg.answer(text=f'<b>Portal in VK</b> - это бот для перепоста постов со страниц в социальной сети ВКонтакте. '
f'Для начала работы вам нужно всего лишь вызвать команду <b>/add</b> и добавить к ней ' f'Для начала работы вам нужно всего лишь вызвать команду <b>/add</b> и добавить к ней '
@ -39,7 +42,9 @@ async def function(call: CallbackQuery) -> None:
'<b>/list</b> - список страниц от, которых вы получаете уведомления\n' '<b>/list</b> - список страниц от, которых вы получаете уведомления\n'
'<b>/notif</b> - отписка или подписка от всех уведомлений\n' '<b>/notif</b> - отписка или подписка от всех уведомлений\n'
'<b>/last_post</b> - получение последнего поста по короткому имени страницы\n' '<b>/last_post</b> - получение последнего поста по короткому имени страницы\n'
'<b>/add</b> и <b>/del</b> - добавление и удаление странички из списка подписок\n') '<b>/add</b> и <b>/del</b> - добавление и удаление странички из списка подписок\n'
'<b>/update</b> - ответом на пост: прислать его заново с актуальными данными\n'
'<b>/logout</b> - удалить сохранённый токен VK\n')
@router.callback_query(F.data == "auth") @router.callback_query(F.data == "auth")
@ -51,7 +56,7 @@ async def author(call: CallbackQuery) -> None:
'возможность ставить через Telegram лайки на посты в самой социальной сети!' 'возможность ставить через Telegram лайки на посты в самой социальной сети!'
'\n\nЯ же пишу подобные небольшие проекты, о которых вы можете узнать ' '\n\nЯ же пишу подобные небольшие проекты, о которых вы можете узнать '
'больше на моём <a href="https://github.com/IGlek">GitHub</a>.') 'больше на моём <a href="https://github.com/EDeev">GitHub</a>.')
# КОМАНДЫ # КОМАНДЫ
@ -60,8 +65,10 @@ async def notification(msg: Message) -> None:
user_id = login(msg.chat.id, du, db) user_id = login(msg.chat.id, du, db)
status = db.get_status(user_id) 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) 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("Получение постов из ВК выключено!") if status: await msg.answer("Получение постов из ВК выключено!")
else: await msg.answer("Получение постов из ВК включено!") else: await msg.answer("Получение постов из ВК включено!")
@ -81,7 +88,7 @@ async def add_del(msg: Message) -> None:
if token: vk = VkParser(token) if token: vk = VkParser(token)
else: vk = VkParser(serviceToken) else: vk = VkParser(serviceToken)
group_id, typ, last_post = vk.login(domain) group_id, typ, last_post = await asyncio.to_thread(vk.login, domain)
if not du.group_exists(group_id): if not du.group_exists(group_id):
du.add_group(group_id) du.add_group(group_id)
@ -118,7 +125,7 @@ async def add_del(msg: Message) -> None:
if db.get_status(user_id): db.update_countGroup(group_id, -1) if db.get_status(user_id): db.update_countGroup(group_id, -1)
await msg.answer("Группа успешно удалена из списка уведомлений!") await msg.answer("Группа успешно удалена из списка уведомлений!")
except Exception as err: except Exception:
logging.info("Нет доступа к страницы:", exc_info=True) logging.info("Нет доступа к страницы:", exc_info=True)
await msg.answer("<b>Произошла ошибка!</b> Проверьте правильность написания <b>короткого адреса</b> " await msg.answer("<b>Произошла ошибка!</b> Проверьте правильность написания <b>короткого адреса</b> "
"страницы, <b>имеете ли вы доступ</b> к этой странице и есть ли на ней <b>хотя бы " "страницы, <b>имеете ли вы доступ</b> к этой странице и есть ли на ней <b>хотя бы "
@ -142,7 +149,7 @@ async def lst(msg: Message) -> None:
group_ids = [du.get_vk_id(id) for id in user_groups] group_ids = [du.get_vk_id(id) for id in user_groups]
vk = VkParser(serviceToken) vk = VkParser(serviceToken)
peoples, groups = vk.info(group_ids) peoples, groups = await asyncio.to_thread(vk.info, group_ids)
text = "<b><i>* Список страниц, от которых вы получаете уведомления!</i></b>\n" text = "<b><i>* Список страниц, от которых вы получаете уведомления!</i></b>\n"
@ -186,10 +193,10 @@ async def like(msg: Message) -> None:
[group_id, last_post] = list(map(int, url.split('wall')[-1].split('_'))) [group_id, last_post] = list(map(int, url.split('wall')[-1].split('_')))
vk = VkParser(token) vk = VkParser(token)
vk.like(group_id, last_post) await asyncio.to_thread(vk.like, group_id, last_post)
await msg.answer('Вы поставили лайк 😉') await msg.answer('Вы поставили лайк 😉')
except Exception as err: except Exception:
logging.error("Не удалось поставить лайк:", exc_info=True) logging.error("Не удалось поставить лайк:", exc_info=True)
await msg.answer('По <b>неизвестной причине</b> не удалось поставить лайк!') await msg.answer('По <b>неизвестной причине</b> не удалось поставить лайк!')
else: else:
@ -226,27 +233,16 @@ async def update(msg: Message) -> None:
if token: vk = VkParser(token) if token: vk = VkParser(token)
else: vk = VkParser(serviceToken) else: vk = VkParser(serviceToken)
output = vk.last_post(post=f"{group_id}_{last_post}") output = await asyncio.to_thread(vk.last_post, post=f"{group_id}_{last_post}")
text, audio, media = pars_post(types, output) text, audio, media = pars_post(output)
delete_r = 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.reply_to_message.message_id)
delete = await bot.delete_message(chat_id=user_id, message_id=msg.message_id) await bot.delete_message(chat_id=user_id, message_id=msg.message_id)
if audio and media: await send_post(bot, user_id, text, audio, 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)
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, disable_web_page_preview=True)
except Exception as err: except Exception:
logging.danger("Ошибка обновления поста:", exc_info=True) logging.error("Ошибка обновления поста:", exc_info=True)
await msg.answer(text='По неизвестной причине не удалось обновить пост!') await msg.answer(text='По неизвестной причине не удалось обновить пост!')
else: await msg.answer(text='Вы ответили не на тот пост!') else: await msg.answer(text='Вы ответили не на тот пост!')
else: await msg.answer(text='Вы не отметили пост который хотите обновить!') else: await msg.answer(text='Вы не отметили пост который хотите обновить!')
@ -265,26 +261,29 @@ async def last_post(msg: Message) -> None:
if token: vk = VkParser(token) if token: vk = VkParser(token)
else: vk = VkParser(serviceToken) else: vk = VkParser(serviceToken)
output = vk.last_post(domain=domain); vk.login(domain) output = await asyncio.to_thread(vk.last_post, domain=domain)
text, audio, media = pars_post(types, output) text, audio, media = pars_post(output)
if audio and media: await send_post(bot, user_id, text, audio, 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)
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, disable_web_page_preview=True)
except Exception as err: except Exception:
logging.info("Некорректное/недоступное короткое имя:", exc_info=True) logging.info("Некорректное/недоступное короткое имя:", exc_info=True)
await msg.answer("Короткое <b>имя страницы неверное</b> или у вас <b>нет доступа</b> к этой странице!") await msg.answer("Короткое <b>имя страницы неверное</b> или у вас <b>нет доступа</b> к этой странице!")
else: await msg.answer("Команда введена некорректно! Корректный ввод: /last_post <b>example</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) @router.message(F.content_type == ContentType.TEXT)
async def url(msg: Message) -> None: async def url(msg: Message) -> None:
@ -293,11 +292,14 @@ async def url(msg: Message) -> None:
if checkUrl in msg.text: if checkUrl in msg.text:
try: try:
token = msg.text.split("&")[0].split("=")[-1] token = msg.text.split("&")[0].split("=")[-1]
info = VkParser(token).check() await asyncio.to_thread(VkParser(token).check)
db.update_token(user_id, token) db.update_token(user_id, token)
await msg.answer("Токен успешно сохранён!") # ссылка с токеном не должна оставаться в переписке
except Exception as err: try: await msg.delete()
except Exception: pass
await msg.answer("Токен успешно сохранён! Удалить его можно командой /logout")
except Exception:
logging.info("Некорректная ссылка:", exc_info=True) logging.info("Некорректная ссылка:", exc_info=True)
await msg.answer("Ваша <b>ссылка не корректна</b>, попробуйте повторить копирование!") await msg.answer("Ваша <b>ссылка не корректна</b>, попробуйте повторить копирование!")
else: await msg.answer("Ваше <b>сообщение не корректно</b>, необходима ссылка, что отобразилась в строке " else: await msg.answer("Ваше <b>сообщение не корректно</b>, необходима ссылка, что отобразилась в строке "

View file

@ -1,22 +1,21 @@
import logging
import os
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.enums.parse_mode import ParseMode from aiogram.enums.parse_mode import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.bot import DefaultBotProperties from aiogram.client.bot import DefaultBotProperties
from config import botToken from config import botToken
from sql import Users, Base
bot = Bot(token=botToken, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) bot = Bot(token=botToken, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=MemoryStorage()) dp = Dispatcher(storage=MemoryStorage())
os.makedirs("../db", exist_ok=True)
from sql import Users, Base
du = Users('../db/users.db') du = Users('../db/users.db')
db = Base('../db/base.db') db = Base('../db/base.db')
# лог в файл, если задан LOG_FILE (так бот работал на сервере), иначе — в консоль
import logging, functools logging.basicConfig(level=logging.INFO, filename=os.getenv("LOG_FILE") or None,
logging.basicConfig(level=logging.INFO, filename='../info.log',filemode="w",
format="%(asctime)s %(levelname)s %(message)s") format="%(asctime)s %(levelname)s %(message)s")

View file

@ -1,5 +1,12 @@
from html import escape
from aiogram import types
import config import config
CAPTION_LIMIT = 1024 # подпись к фото в Telegram
TEXT_LIMIT = 4096 # обычное сообщение
def login(user_id, du, db): def login(user_id, du, db):
user_id = int(user_id) user_id = int(user_id)
@ -11,7 +18,9 @@ def login(user_id, du, db):
return user_id return user_id
def pars_post(types, output): def pars_post(output):
"""Пост из vk_scripts.get_output → текст в HTML, аудио и медиа для Telegram.
Всё, что пришло из ВК, экранируется: «<» или «&» в посте иначе ломают отправку"""
media, video, url, audio, poll, link = [], [], [], [], [], [] media, video, url, audio, poll, link = [], [], [], [], [], []
for group in output[0]: for group in output[0]:
@ -20,55 +29,98 @@ def pars_post(types, output):
elif group[0] == "video": video.append(group[1]) elif group[0] == "video": video.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] == "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( elif group[0] == "audio": audio.append(types.InputMediaAudio(
media=group[1][1], caption=f"<b>Название: {group[1][0]} / Автор: {group[1][2]}</b>")) media=group[1][1], caption=f"<b>Название: {escape(group[1][0])} / Автор: {escape(group[1][2])}</b>"))
if output[1] != "": if output[1] != "":
post = [elem_2 for elem_1 in output[1].split("[") for elem_2 in elem_1.split("]")] # упоминания ВК вида [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): for i, elem in enumerate(post):
if "|" in elem and i != 0: if "|" in elem and i != 0:
elem = elem.split("|") elem = elem.split("|", 1)
post[i] = f"<a href='https://vk.com/{elem[0]}'>{elem[1]}</a>" post[i] = f"<a href='https://vk.com/{escape(elem[0])}'>{elem[1]}</a>"
text = f"{output[2]}\n\n{''.join(post)}\n" text = f"{output[2]}\n\n{''.join(post)}\n"
else: text = f"{output[2]}\n" else: text = f"{output[2]}\n"
if poll: if poll:
text += f"\n<b>> Проводиться</b> {config.anonymous[poll[3]]} {config.unvote[poll[2]]}! <b>Вопрос:</b> {poll[0]} <b>[{poll[1]}]</b>\n" text += (f"\n<b>> Проводится</b> {config.anonymous[poll[3]]} {config.unvote[poll[2]]}! "
text += ''.join([f'<b>{i + 1}. Ответ:</b> {ans[0]} <b>[{ans[1]}]</b>\n' for i, ans in enumerate(poll[4])]) + "\n" 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"
if video: if video:
for frame in video: for frame in video:
text += f"\n<b>Видео</b> <i>« {frame[0]} »</i>\n" text += f"\n<b>Видео</b> <i>« {escape(frame[0])} »</i>\n"
k, dura = 0, frame[1] k, dura = 0, frame[1]
while k < 1: if dura > 60: dura, k = dura // 60, 1
if dura > 60: dura = dura // 60; k += 1
else: break
text += f"<b>Длительность:</b> {dura} {config.dura[k]}; <b>Лайков:</b> {frame[2]};\n" text += f"<b>Длительность:</b> {dura} {config.dura[k]}; <b>Просмотров:</b> {frame[2]};\n"
if url: if url:
for data in url: for data in url:
k, dt = 0, data[2] k, dt = 0, data[2]
while True: while dt > 1024 and k < len(config.data) - 1:
if dt > 1024: dt = dt // 1024; k += 1 dt, k = dt // 1024, k + 1
else: break text += (f"\n<b>Документ - [</b> <a href='{escape(data[1])}'>{escape(data[0])}</a> "
text += f"\n<b>Документ - [</b> <a href='{data[1]}'>{data[0]}</a> ({dt} {config.data[k]}) <b>]</b>" f"({dt} {config.data[k]}) <b>]</b>")
text += "\n" text += "\n"
if link: text += ''.join([f"\n<b>Ссылка - [</b> [{s[0]}]({s[1]}) <b>]</b>" for s in link]) + "\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 audio: text += '\n<b>« Также присутствуют аудио файлы »</b>'
for i, group in enumerate(output[0]): for group in output[0]:
if group[0] == "photo": if group[0] == "photo":
if i: media.append(types.InputMediaPhoto(media=group[1])) media.append(types.InputMediaPhoto(media=group[1]))
else: media.append(types.InputMediaPhoto(media=group[1], caption=text)) elif group[0] == "video" and group[1][3]: # у видео может не быть обложки
elif group[0] == "video": media.append(types.InputMediaPhoto(media=group[1][3]))
if i: media.append(types.InputMediaPhoto(media=group[1][3])) elif group[0] == "doc" and group[1][2] == 4:
else: media.append(types.InputMediaPhoto(media=group[1][3], caption=text)) media.append(types.InputMediaPhoto(media=group[1][1]))
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))
return text, audio, media 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)

View file

@ -7,6 +7,11 @@ class Users:
self.connection = sqlite3.connect(database) self.connection = sqlite3.connect(database)
self.cursor = self.connection.cursor() 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 # КОМАНДЫ USER
def user_exists(self, user_id): def user_exists(self, user_id):
"""Проверяем, есть ли уже пользователь в базе""" """Проверяем, есть ли уже пользователь в базе"""
@ -63,6 +68,12 @@ class Base:
self.connection = sqlite3.connect(database) self.connection = sqlite3.connect(database)
self.cursor = self.connection.cursor() 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 # КОМАНДЫ USER
def infoUser_exists(self, user_id): def infoUser_exists(self, user_id):
"""Проверяем, есть ли данные уже в базе""" """Проверяем, есть ли данные уже в базе"""
@ -166,4 +177,3 @@ class Base:
def close(self): def close(self):
"""Закрываем соединение с БД""" """Закрываем соединение с БД"""
self.connection.close() self.connection.close()

View file

@ -1,3 +1,5 @@
from html import escape
import vk_api import vk_api
@ -70,7 +72,7 @@ def get_output(post, name, domain, repost=None):
[post_id, post] = post [post_id, post] = post
owner_id, output = post['owner_id'], [] owner_id, output = post['owner_id'], []
attachments = post['attachments'] attachments = post.get('attachments', [])
types = [[typ['type'], typ] for typ in attachments] types = [[typ['type'], typ] for typ in attachments]
for typ in types: for typ in types:
@ -78,12 +80,12 @@ def get_output(post, name, domain, repost=None):
output.append([typ[0], typ[1]['photo']['sizes'][-1]['url']]) output.append([typ[0], typ[1]['photo']['sizes'][-1]['url']])
elif typ[0] == 'video': elif typ[0] == 'video':
video = typ[1]['video'] video = typ[1]['video']
if 'photo_1280' in video: # обложка: самая крупная из доступных
frame = video['photo_1280'] frame = video.get('photo_1280') or video.get('photo_800') or video.get('photo_320')
else: if frame is None and video.get('image'):
frame = video['photo_800'] frame = video['image'][-1]['url']
output.append([typ[0], [video['title'], video['duration'], video['views'], frame]]) output.append([typ[0], [video['title'], video.get('duration', 0), video.get('views', 0), frame]])
elif typ[0] == "doc": elif typ[0] == "doc":
output.append([typ[0], [typ[1]['doc']['title'], typ[1]['doc']['url'], output.append([typ[0], [typ[1]['doc']['title'], typ[1]['doc']['url'],
typ[1]['doc']['type'], typ[1]['doc']['size']]]) typ[1]['doc']['type'], typ[1]['doc']['size']]])
@ -99,11 +101,11 @@ def get_output(post, name, domain, repost=None):
if repost: if repost:
[author_id, author_owner_id, author_name, author_domain, author_comment] = 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}'>{author_name}</a>" 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>{author_comment}</blockquote>\n" 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}'>{name}</a>" 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] return [output, f"{post['text']}", comment, author_id]
else: else:
return [output, f"{post['text']}", return [output, f"{post['text']}",
f"<b>Автор поста -</b> <a href='https://vk.com/{domain}?w=wall{owner_id}_{post_id}'>{name}</a>", post_id] f"<b>Автор поста -</b> <a href='https://vk.com/{domain}?w=wall{owner_id}_{post_id}'>{escape(name)}</a>", post_id]

0
db/.gitkeep Normal file
View file

Binary file not shown.

Binary file not shown.