diff --git a/README.md b/README.md index dc94a6f..d530205 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ ___ ```JSON "token": "" ``` -Сюда необходимо занести токен бота Telegram (можно получить у [BotFather](https://t.me/BotFather)). Токен должен быть уникальным для каждого источника. +Сюда необходимо занести токен бота Telegram (можно получить у [BotFather](https://t.me/BotFather)). Можно использовать одни и те же токены для разных источников. ___ ```JSON "target": "" @@ -132,7 +132,7 @@ ___ ```JSON "token": "" ``` -Сюда необходимо занести токен бота Telegram (можно получить у [BotFather](https://t.me/BotFather)). Токен должен быть уникальным для каждого источника. +Сюда необходимо занести токен бота Telegram (можно получить у [BotFather](https://t.me/BotFather)). Можно использовать одни и те же токены для разных источников. ___ ```JSON "target": "" diff --git a/Source/BotsManager.py b/Source/BotsManager.py new file mode 100644 index 0000000..676a39c --- /dev/null +++ b/Source/BotsManager.py @@ -0,0 +1,43 @@ +import logging +import telebot + +# Менеджер подключений к ботам. +class BotsManager: + + # Конструктор. + def __init__(self): + + #---> Генерация динамических свойств. + #==========================================================================================# + # Хранилище активных ботов. + self.__Bots = dict() + # Данные о подключениях к ботам. + self.__ConnectionsData = dict() + + # Создаёт новое подключение к боту. + def createBotConnection(self, Token: str, Source: str, Target: str): + + # Если бот с таким токеном ещё не инициализирован. + if Token not in self.__Bots.keys(): + + try: + # Инициализация бота. + self.__Bots[Token] = telebot.TeleBot(Token) + + except Exception as ExceptionData: + # Запись в лог ошибки: не удалось инициализировать бота. + logging.error("Incorrect bot token. Exception: " + str(ExceptionData)) + + # Заполнение данных о подключении. + self.__ConnectionsData[Token] = { + "sources": list(), + "targets": list() + } + + # Дополнение информации о подключении. + self.__ConnectionsData[Token]["sources"].append(Source) + self.__ConnectionsData[Token]["targets"].append(Target) + + # Возвращает экземпляр бота по токену. + def getBot(self, Token: str) -> telebot.TeleBot: + return self.__Bots[Token] \ No newline at end of file diff --git a/Source/Callback.py b/Source/Callback.py index 7575a92..223caa4 100644 --- a/Source/Callback.py +++ b/Source/Callback.py @@ -1,5 +1,6 @@ from telebot.types import InputMediaDocument, InputMediaPhoto, InputMediaVideo from Source.Configurator import Configurator +from Source.BotsManager import BotsManager from MessageEditor import MessageEditor from Source.Functions import * from threading import Thread @@ -15,12 +16,14 @@ class Callback: def __SenderThread(self): # Запись в лог отладочной информации: поток очереди отправки запущен. logging.debug("Callback API sender thread started.") - + # Пока сообщение не отправлено. while True: # Если в очереди на отправку есть сообщения. if len(self.__MessagesBufer) > 0: + # Конфигурация источника. + Config = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"]) # Список медиа-вложений. MediaGroup = list() @@ -37,7 +40,7 @@ def __SenderThread(self): InputMediaDocument( open("Temp/" + self.__MessagesBufer[0]["attachments"][Index]["filename"], "rb"), caption = self.__MessagesBufer[0]["text"] if Index == 0 else "", - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"] if Index == 0 else None + parse_mode = Config["parse-mode"] if Index == 0 else None ) ) @@ -48,7 +51,7 @@ def __SenderThread(self): InputMediaPhoto( open("Temp/" + self.__MessagesBufer[0]["attachments"][Index]["filename"], "rb"), caption = self.__MessagesBufer[0]["text"] if Index == 0 else "", - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"] if Index == 0 else None + parse_mode = Config["parse-mode"] if Index == 0 else None ) ) @@ -59,7 +62,7 @@ def __SenderThread(self): InputMediaVideo( open("Temp/" + self.__MessagesBufer[0]["attachments"][Index]["filename"], "rb"), caption = self.__MessagesBufer[0]["text"] if Index == 0 else "", - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"] if Index == 0 else None + parse_mode = Config["parse-mode"] if Index == 0 else None ) ) @@ -68,18 +71,18 @@ def __SenderThread(self): # Если есть вложения. if len(MediaGroup) > 0: # Отправка медиа группы. - self.__TelegramBots[self.__MessagesBufer[0]["source"]].send_media_group( + self.__Bots.getBot(self.__MessagesBufer[0]["token"]).send_media_group( self.__MessagesBufer[0]["target"], media = MediaGroup ) else: # Отправка текстового сообщения. - self.__TelegramBots[self.__MessagesBufer[0]["source"]].send_message( + self.__Bots.getBot(self.__MessagesBufer[0]["token"]).send_message( self.__MessagesBufer[0]["target"], self.__MessagesBufer[0]["text"], - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"], - disable_web_page_preview = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["disable-web-page-preview"] + parse_mode = Config["parse-mode"], + disable_web_page_preview = Config["disable-web-page-preview"] ) except telebot.apihelper.ApiTelegramException as ExceptionData: @@ -108,7 +111,7 @@ def __SenderThread(self): else: # Запись в лог отладочной информации: поток очереди отправки оставновлен. - logging.debug("Sender thread stopped.") + logging.debug("Callback API sender thread stopped.") # Остановка потока. break @@ -116,23 +119,26 @@ def __SenderThread(self): def __SendMessage(self, PostObject: dict, Source: str): # Состояние: есть ли запрещённые слова в посте. HasBlacklistWords = False + # Конфигурация источника. + Config = self.__Configurations.getConfig(Source) # Объект сообщения. MessageStruct = { "source": Source, - "target": self.__Configurations.getConfig(Source)["target"], + "token": Config["token"], + "target": Config["target"], "text": None, "attachments": list() } # Экранировать символы при указанной разметке MarkdownV2. - if self.__Configurations.getConfig(Source)["parse-mode"] == "MarkdownV2": + if Config["parse-mode"] == "MarkdownV2": PostObject["text"] = EscapeCharacters(PostObject["text"]) # Обработка текста поста пользовательским скриптом. PostObject["text"] = MessageEditor(PostObject["text"] if PostObject["text"] != None else "", Source) # Для каждого запрещённого слова проверить соответствие словам поста. - for ForbiddenWord in self.__Configurations.getConfig(Source)["blacklist"]: + for ForbiddenWord in Config["blacklist"]: for Word in PostObject["text"].split(): # Если пост содержит запрещённое слово, то игнорировать его. @@ -143,7 +149,7 @@ def __SendMessage(self, PostObject: dict, Source: str): if PostObject["text"] != None and PostObject["text"] != "" and HasBlacklistWords == False: # Если включена очистка тегов, то удалить упоминания из них. - if self.__Configurations.getConfig(Source)["clean-tags"] == True: + if Config["clean-tags"] == True: PostObject["text"] = CleanTags(PostObject["text"]) # Обрезка текста поста до максимально дозволенной длинны. @@ -176,8 +182,8 @@ def __SendMessage(self, PostObject: dict, Source: str): self.__Sender = Thread(target = self.__SenderThread, name = "VK-Telegram Poster (Callback API sender)") self.__Sender.start() - # Конструктор: задаёт глобальные настройки и обработчик конфигураций. - def __init__(self, Settings: dict, ConfiguratorObject: Configurator): + # Конструктор: задаёт глобальные настройки, обработчик конфигураций и менеджер подключений к ботам. + def __init__(self, Settings: dict, ConfiguratorObject: Configurator, BotsManagerObject: BotsManager): #---> Генерация динамических свойств. #==========================================================================================# @@ -189,17 +195,20 @@ def __init__(self, Settings: dict, ConfiguratorObject: Configurator): self.__PostsEditorsThreads = list() # Глобальные настройки. self.__Settings = Settings.copy() + # Менеджер подключений к ботам. + self.__Bots = BotsManagerObject # Очередь отложенных сообщений. self.__MessagesBufer = list() - # Список экземпляров бота. - self.__TelegramBots = dict() # Запуск потока обработки буфера сообщений. self.__Sender.start() - # Инициализация экзепляров бота. - for Target in self.__Configurations.getConfigsNames("Callback"): - self.__TelegramBots[Target] = telebot.TeleBot(self.__Configurations.getToken(Target)) + # Инициализация экзепляров ботов. + for ConfigName in self.__Configurations.getConfigsNames("Callback"): + # Конфигурация источника. + Config = self.__Configurations.getConfig(ConfigName) + # Инициализация подключения к боту. + self.__Bots.createBotConnection(Config["token"], ConfigName, Config["target"]) # Добавляет сообщение в очередь отправки. def AddMessageToBufer(self, CallbackRequest: dict, Source: str): diff --git a/Source/Open.py b/Source/Open.py index a05d43b..fbf2356 100644 --- a/Source/Open.py +++ b/Source/Open.py @@ -1,6 +1,8 @@ from telebot.types import InputMediaDocument, InputMediaPhoto, InputMediaVideo +from vk_api.exceptions import AuthError, ApiError from dublib.Methods import ReadJSON, WriteJSON from Source.Configurator import Configurator +from Source.BotsManager import BotsManager from MessageEditor import MessageEditor from vk_captcha import VkCaptchaSolver from threading import Thread, Timer @@ -37,7 +39,7 @@ def __Authorizate(self): if self.__Settings["vk-access-token"] == None: self.__Session.auth(token_only = True) - except VkApi.exceptions.AuthError as ExceptionData: + except AuthError as ExceptionData: # Запись в лог ошибки: исключение авторизации. logging.error("[Open API] Authorization exception: " + str(ExceptionData).split(" Please")[0]) # Выжидание интервала. @@ -61,7 +63,7 @@ def __GetPosts(self, WallID: str, PostsCount: int = 20, Offset: int = 0) -> list # Попытка получить список постов. WallPosts = self.__API.wall.get(owner_id = WallID, count = PostsCount, offset = Offset)["items"] - except VkApi.exceptions.ApiError as ExceptionData: + except ApiError as ExceptionData: # Запись в лог ошибки: исключение API. logging.error("[Open API] Exception: " + str(ExceptionData)) @@ -81,7 +83,7 @@ def __GetUpdates(self, ConfigName: str) -> list[dict]: # Пока не будет найдено обновление. while IsUpdated == False: # Получение последних 20 постов. - Bufer = self.__GetPosts(Config["wall-id"], Offset = 20 * RequestIndex) + Bufer = self.__GetPosts(Config["wall-id"], Offset = (20 * RequestIndex) + 1) # Если ID последнего отправленного поста записан. if Config["last-post-id"] != None: @@ -117,12 +119,14 @@ def __GetUpdates(self, ConfigName: str) -> list[dict]: def __SenderThread(self): # Запись в лог отладочной информации: поток очереди отправки запущен. logging.debug("Open API sender thread started.") - + # Пока сообщение не отправлено. while True: # Если в очереди на отправку есть сообщения. if len(self.__MessagesBufer) > 0: + # Конфигурация источника. + Config = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"]) # Список медиа-вложений. MediaGroup = list() @@ -139,7 +143,7 @@ def __SenderThread(self): InputMediaDocument( open("Temp/" + self.__MessagesBufer[0]["attachments"][Index]["filename"], "rb"), caption = self.__MessagesBufer[0]["text"] if Index == 0 else "", - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"] if Index == 0 else None + parse_mode = Config["parse-mode"] if Index == 0 else None ) ) @@ -150,7 +154,7 @@ def __SenderThread(self): InputMediaPhoto( open("Temp/" + self.__MessagesBufer[0]["attachments"][Index]["filename"], "rb"), caption = self.__MessagesBufer[0]["text"] if Index == 0 else "", - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"] if Index == 0 else None + parse_mode = Config["parse-mode"] if Index == 0 else None ) ) @@ -161,7 +165,7 @@ def __SenderThread(self): InputMediaVideo( open("Temp/" + self.__MessagesBufer[0]["attachments"][Index]["filename"], "rb"), caption = self.__MessagesBufer[0]["text"] if Index == 0 else "", - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"] if Index == 0 else None + parse_mode = Config["parse-mode"] if Index == 0 else None ) ) @@ -170,18 +174,18 @@ def __SenderThread(self): # Если есть вложения. if len(MediaGroup) > 0: # Отправка медиа группы. - self.__TelegramBots[self.__MessagesBufer[0]["source"]].send_media_group( + self.__Bots.getBot(self.__MessagesBufer[0]["token"]).send_media_group( self.__MessagesBufer[0]["target"], media = MediaGroup ) else: # Отправка текстового сообщения. - self.__TelegramBots[self.__MessagesBufer[0]["source"]].send_message( + self.__Bots.getBot(self.__MessagesBufer[0]["token"]).send_message( self.__MessagesBufer[0]["target"], self.__MessagesBufer[0]["text"], - parse_mode = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["parse-mode"], - disable_web_page_preview = self.__Configurations.getConfig(self.__MessagesBufer[0]["source"])["disable-web-page-preview"] + parse_mode = Config["parse-mode"], + disable_web_page_preview = Config["disable-web-page-preview"] ) except telebot.apihelper.ApiTelegramException as ExceptionData: @@ -194,7 +198,7 @@ def __SenderThread(self): else: # Запись в лог ошибки: исключение Telegram. - logging.error("Telegram exception: \"" + Description + "\".") + logging.error("Telegram exception: \"" + Description + "\"." + self.__MessagesBufer[0]["text"]) # Удаление первого сообщения в очереди отправки. self.__MessagesBufer.pop(0) @@ -210,7 +214,7 @@ def __SenderThread(self): else: # Запись в лог отладочной информации: поток очереди отправки оставновлен. - logging.debug("Sender thread stopped.") + logging.debug("Open API sender thread stopped.") # Остановка потока. break @@ -218,23 +222,26 @@ def __SenderThread(self): def __SendMessage(self, PostObject: dict, Source: str): # Состояние: есть ли запрещённые слова в посте. HasBlacklistWords = False + # Конфигурация источника. + Config = self.__Configurations.getConfig(Source) # Объект сообщения. MessageStruct = { "source": Source, - "target": self.__Configurations.getConfig(Source)["target"], + "token": Config["token"], + "target": Config["target"], "text": None, "attachments": list() } # Экранировать символы при указанной разметке MarkdownV2. - if self.__Configurations.getConfig(Source)["parse-mode"] == "MarkdownV2": + if Config["parse-mode"] == "MarkdownV2": PostObject["text"] = EscapeCharacters(PostObject["text"]) # Обработка текста поста пользовательским скриптом. PostObject["text"] = MessageEditor(PostObject["text"] if PostObject["text"] != None else "", Source) # Для каждого запрещённого слова проверить соответствие словам поста. - for ForbiddenWord in self.__Configurations.getConfig(Source)["blacklist"]: + for ForbiddenWord in Config["blacklist"]: for Word in PostObject["text"].split(): # Если пост содержит запрещённое слово, то игнорировать его. @@ -245,7 +252,7 @@ def __SendMessage(self, PostObject: dict, Source: str): if PostObject["text"] != None and PostObject["text"] != "" and HasBlacklistWords == False: # Если включена очистка тегов, то удалить упоминания из них. - if self.__Configurations.getConfig(Source)["clean-tags"] == True: + if Config["clean-tags"] == True: PostObject["text"] = CleanTags(PostObject["text"]) # Обрезка текста поста до максимально дозволенной длинны. @@ -287,8 +294,8 @@ def __WriteLastPostID(self, Source: str, ID: int): # Запись обновлённой конфигурации. WriteJSON(f"Config/{Source}.json", Config) - # Конструктор: задаёт глобальные настройки и обработчик конфигураций. - def __init__(self, Settings: dict, ConfiguratorObject: Configurator): + # Конструктор: задаёт глобальные настройки, обработчик конфигураций и менеджер подключений к ботам. + def __init__(self, Settings: dict, ConfiguratorObject: Configurator, BotsManagerObject: BotsManager): #---> Генерация динамических свойств. #==========================================================================================# @@ -300,25 +307,28 @@ def __init__(self, Settings: dict, ConfiguratorObject: Configurator): self.__PostsEditorsThreads = list() # Глобальные настройки. self.__Settings = Settings.copy() + # Менеджер подключений к ботам. + self.__Bots = BotsManagerObject # Очередь отложенных сообщений. self.__MessagesBufer = list() - # Список экземпляров бота. - self.__TelegramBots = dict() # Сессия ВКонтакте. self.__Session = None # Обработчик повторов. self.__Repiter = None # Экземпляр API. self.__API = None - + # Авторизация и получение API. self.__Authorizate() # Запуск потока обработки буфера сообщений. self.__Sender.start() - # Инициализация экзепляров бота. - for Target in self.__Configurations.getConfigsNames("Open"): - self.__TelegramBots[Target] = telebot.TeleBot(self.__Configurations.getToken(Target)) + # Инициализация экзепляров ботов. + for ConfigName in self.__Configurations.getConfigsNames("Open"): + # Конфигурация источника. + Config = self.__Configurations.getConfig(ConfigName) + # Инициализация подключения к боту. + self.__Bots.createBotConnection(Config["token"], ConfigName, Config["target"]) # Немедленная проверка новых постов и активация таймера. self.CheckUpdates() @@ -326,7 +336,7 @@ def __init__(self, Settings: dict, ConfiguratorObject: Configurator): # Интервально проверяет обновления и добавляет сообщения в очередь отправки. def CheckUpdates(self): # Обновление конфигураций с Open API. - self.__Configurations.updateOpenConfigs() + self.__Configurations.updateConfigs("Open") # Получение списка конфигураций, использующих Open API. Configs = self.__Configurations.getConfigsNames("Open") # Количество новых постов. diff --git a/vtp.py b/vtp.py index 2be022d..e82d5ee 100644 --- a/vtp.py +++ b/vtp.py @@ -3,6 +3,7 @@ from dublib.Methods import CheckPythonMinimalVersion, MakeRootDirectories, ReadJSON from starlette.responses import HTMLResponse, Response from Source.Configurator import Configurator +from Source.BotsManager import BotsManager from starlette.requests import Request from Source.Callback import Callback from fastapi import FastAPI @@ -99,6 +100,8 @@ # Инициализация менеджера конфигураций. ConfiguratorObject = Configurator() +# Менеджер подключений к ботам. +BotsManagerObject = BotsManager() # Количество модулей с требуемыми типами API. RequiredAPI = ConfiguratorObject.getRequiredAPI() # Запись в лог сообщения: заголовок раздела обработки запросов. @@ -107,7 +110,7 @@ # Если требуется обработка Callback API. if RequiredAPI["Callback"] > 0: # Обработчик Callback-запросов. - CallbackSender = Callback(Settings, ConfiguratorObject) + CallbackSender = Callback(Settings, ConfiguratorObject, BotsManagerObject) # Обрабатывает запросы от браузера. @App.get("/vtp/{Source}") @@ -188,4 +191,4 @@ async def SendMessageToGroup(CallbackRequest: Request, Source: str) -> Response: # Если требуется обработка Open API. if RequiredAPI["Open"] > 0: # Обработчик Open-запросов. - OpenSender = Open(Settings, ConfiguratorObject) \ No newline at end of file + OpenSender = Open(Settings, ConfiguratorObject, BotsManagerObject) \ No newline at end of file