From 431b00428469acb322fd4425024210a44a28f16b Mon Sep 17 00:00:00 2001 From: miloszowi Date: Fri, 8 Oct 2021 15:25:47 +0200 Subject: [PATCH] Banned users env, access validator, removed silent command, code quality improvements --- CHANGELOG.md | 11 ++++- README.md | 12 +---- docker/config/app.dist.env | 4 +- docs/silent.png | Bin 28060 -> 0 bytes src/app.py | 26 +++-------- src/bot/handler/__init__.py | 5 ++ src/bot/handler/abstractHandler.py | 34 ++------------ src/bot/handler/everyoneHandler.py | 35 ++++++++++++++ src/bot/handler/groupsHandler.py | 51 +++++++++------------ src/bot/handler/inlineQueryHandler.py | 19 +++++--- src/bot/handler/joinHandler.py | 35 ++++++-------- src/bot/handler/leaveHandler.py | 44 ++++++++---------- src/bot/handler/mentionHandler.py | 47 ------------------- src/bot/handler/silentMentionHandler.py | 21 --------- src/bot/handler/startHandler.py | 22 ++++----- src/bot/message/messageData.py | 12 ++--- src/bot/message/replier.py | 29 ++++++++++++ src/config/contents.py | 3 +- src/config/credentials.py | 2 + src/database/client.py | 9 ++-- src/entity/group.py | 2 +- src/entity/user.py | 6 +-- src/exception/actionNotAllowedException.py | 2 + src/logger.py | 20 ++++++-- src/repository/groupRepository.py | 17 ++++--- src/repository/userRepository.py | 30 +++++++----- src/utils/messageBuilder.py | 21 +++++++++ src/validator/accessValidator.py | 10 ++++ 28 files changed, 268 insertions(+), 261 deletions(-) delete mode 100644 docs/silent.png create mode 100644 src/bot/handler/__init__.py create mode 100755 src/bot/handler/everyoneHandler.py delete mode 100755 src/bot/handler/mentionHandler.py delete mode 100644 src/bot/handler/silentMentionHandler.py create mode 100644 src/bot/message/replier.py create mode 100644 src/exception/actionNotAllowedException.py create mode 100644 src/utils/messageBuilder.py create mode 100644 src/validator/accessValidator.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf55fa..ab1209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file. ## [UNRELEASED] - 07.10.2021 ### Added - Inline Query for join/leave/everyone -- Validator class for group name +- Group name validator +- Banned users env +- Access validator +- ActionNotAllowedException + +### Changed +- code quality improvements + +### Deleted +- `/silent` command ### Updated - start command content diff --git a/README.md b/README.md index d99b678..78eda54 100755 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ * [`/leave`](#leave) * [`/everyone`](#everyone) * [`/groups`](#groups) - * [`/silent`](#silent) * [`/start`](#start) ## Description Everyone Mention Bot is simple, but useful telegram bot to gather group members attention. @@ -62,6 +61,7 @@ docker/logs - `MONGODB_PASSWORD` - MongoDB password - `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name) - `MONGODB_PORT` - MongoDB port (default `27017` - given in docker-compose configuration) + - `BANNED_USERS` - user ids separated by comma that are not allowed to use the bot - `database.env` - `MONGO_INITDB_ROOT_USERNAME` - conf from `app.env` @@ -101,16 +101,6 @@ If user does not have nickname, it will first try to assign his firstname, then Will display available groups for this chat as well with members count. ![groups command example](docs/groups.png) - -### `/silent` -``` -/silent -``` - -Will display all every member of given group (`default` if not given) but without notyfing them. - -![silent command example](docs/silent.png) - ### `/start` Start & Help message diff --git a/docker/config/app.dist.env b/docker/config/app.dist.env index cc14bd0..de9bb54 100755 --- a/docker/config/app.dist.env +++ b/docker/config/app.dist.env @@ -6,4 +6,6 @@ MONGODB_DATABASE= MONGODB_USERNAME= MONGODB_PASSWORD= MONGODB_HOSTNAME=localhost -MONGODB_PORT=27017 \ No newline at end of file +MONGODB_PORT=27017 + +BANNED_USERS= \ No newline at end of file diff --git a/docs/silent.png b/docs/silent.png deleted file mode 100644 index 1db4c9b26aaa1574d5ab961a4fc065599c9370be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28060 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%0ARQ*sqFM#VBYxf!>y}R z^m49Tr@JM5t;sPeOH6!otEwO@keWDv>g=}kK!UbMtuKB_+W&nq{q<{e6?pVDu32AvCXzUty}yOl)zv(89uQ zB~|qp+_Ki#J?++5{>Oo2>wN!%_Lu%}!v6D|BaHBS2K(mz@3p*)igbnK$H4yw<>ha| diff --git a/src/app.py b/src/app.py index 4a2c6b4..67ac0b3 100755 --- a/src/app.py +++ b/src/app.py @@ -1,27 +1,21 @@ -from logging import Logger -import logging from telegram.ext import Updater from telegram.ext.dispatcher import Dispatcher -from logger import Logger -from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL -from bot.handler import (groupsHandler, joinHandler, mentionHandler, leaveHandler, - silentMentionHandler, startHandler, inlineQueryHandler) +from bot.handler import * from bot.handler.abstractHandler import AbstractHandler +from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL +from logger import Logger class App: updater: Updater dispatcher: Dispatcher - log_file: str = '/var/log/bot.log' - log_format: str = '%(levelname)s-%(asctime)s: %(message)s' - def __init__(self): self.updater = Updater(BOT_TOKEN) def run(self) -> None: - self.setup_logging() + Logger.register() self.register_handlers() self.register_webhook() @@ -29,9 +23,7 @@ class App: def register_handlers(self) -> None: for handler in AbstractHandler.__subclasses__(): - self.updater.dispatcher.add_handler( - handler().get_bot_handler() - ) + self.updater.dispatcher.add_handler(handler().bot_handler) def register_webhook(self) -> None: self.updater.start_webhook( @@ -41,15 +33,9 @@ class App: webhook_url="/".join([WEBHOOK_URL, BOT_TOKEN]) ) - Logger.get_logger(Logger.action_logger).info( - f'Webhook configured, listening on {WEBHOOK_URL}/' - ) + Logger.info(f'Webhook configured, listening on {WEBHOOK_URL}/') - def setup_logging(self) -> None: - logger = Logger() - logger.setup() if __name__ == "__main__": app = App() - app.run() diff --git a/src/bot/handler/__init__.py b/src/bot/handler/__init__.py new file mode 100644 index 0000000..3a55bbb --- /dev/null +++ b/src/bot/handler/__init__.py @@ -0,0 +1,5 @@ +__all__ = [ + 'abstractHandler', 'everyoneHandler', 'groupsHandler', + 'inlineQueryHandler', 'joinHandler', 'leaveHandler', + 'startHandler' +] diff --git a/src/bot/handler/abstractHandler.py b/src/bot/handler/abstractHandler.py index 553f4d8..ea57502 100755 --- a/src/bot/handler/abstractHandler.py +++ b/src/bot/handler/abstractHandler.py @@ -1,37 +1,13 @@ from abc import abstractmethod -from bot.message.messageData import MessageData -from logger import Logger +from telegram.ext import Handler from telegram.ext.callbackcontext import CallbackContext -from telegram.ext.handler import Handler from telegram.update import Update -from telegram.utils.helpers import mention_markdown -class AbstractHandler: - @abstractmethod - def get_bot_handler(self) -> Handler: raise Exception('get_bot_handler method is not implemented') +class AbstractHandler: + bot_handler: Handler @abstractmethod - def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented') - - @abstractmethod - def log_action(self, message_data: MessageData) -> None: raise Exception('log_action method is not implemented') - - def interpolate_reply(self, reply: str, message_data: MessageData): - return reply.format( - mention_markdown(message_data.user_id, message_data.username), - message_data.group_name - ) - - def reply_markdown(self, update: Update, message: str) -> None: - try: - update.effective_message.reply_markdown_v2(text=message) - except Exception as err: - Logger.error(str(err)) - - def reply_html(self, update: Update, html: str) -> None: - try: - update.effective_message.reply_html(text=html) - except Exception as err: - Logger.error(str(err)) + def handle(self, update: Update, context: CallbackContext) -> None: + raise Exception('handle method is not implemented') diff --git a/src/bot/handler/everyoneHandler.py b/src/bot/handler/everyoneHandler.py new file mode 100755 index 0000000..9f717e8 --- /dev/null +++ b/src/bot/handler/everyoneHandler.py @@ -0,0 +1,35 @@ +from telegram.ext.callbackcontext import CallbackContext +from telegram.ext.commandhandler import CommandHandler +from telegram.update import Update + +from bot.handler.abstractHandler import AbstractHandler +from bot.message.messageData import MessageData +from bot.message.replier import Replier +from config.contents import mention_failed +from logger import Logger +from repository.userRepository import UserRepository +from utils.messageBuilder import MessageBuilder + + +class EveryoneHandler(AbstractHandler): + bot_handler: CommandHandler + user_repository: UserRepository + action: str = 'everyone' + + def __init__(self) -> None: + self.bot_handler = CommandHandler(self.action, self.handle) + self.user_repository = UserRepository() + + def handle(self, update: Update, context: CallbackContext) -> None: + try: + message_data = MessageData.create_from_arguments(update, context) + except Exception as e: + return Replier.markdown(update, str(e)) + + users = self.user_repository.get_all_for_chat(message_data.chat_id) + + if users: + Replier.markdown(update, MessageBuilder.mention_message(users)) + return Logger.action(message_data, self.action) + + Replier.markdown(update, mention_failed) diff --git a/src/bot/handler/groupsHandler.py b/src/bot/handler/groupsHandler.py index 0270404..9414d5a 100644 --- a/src/bot/handler/groupsHandler.py +++ b/src/bot/handler/groupsHandler.py @@ -1,45 +1,36 @@ -from typing import Iterable - -import prettytable as pt -from bot.handler.abstractHandler import AbstractHandler -from bot.message.messageData import MessageData -from config.contents import no_groups -from entity.group import Group -from logger import Logger -from repository.groupRepository import GroupRepository from telegram.ext.callbackcontext import CallbackContext from telegram.ext.commandhandler import CommandHandler from telegram.update import Update +from bot.handler.abstractHandler import AbstractHandler +from bot.message.messageData import MessageData +from bot.message.replier import Replier +from config.contents import no_groups +from exception.notFoundException import NotFoundException +from logger import Logger +from repository.groupRepository import GroupRepository +from utils.messageBuilder import MessageBuilder + class GroupsHandler(AbstractHandler): bot_handler: CommandHandler group_repository: GroupRepository + action: str = 'groups' def __init__(self) -> None: - self.bot_handler = CommandHandler('groups', self.handle) + self.bot_handler = CommandHandler(self.action, self.handle) self.group_repository = GroupRepository() def handle(self, update: Update, context: CallbackContext) -> None: - message_data = MessageData.create_from_arguments(update, context, False) + try: + message_data = MessageData.create_from_arguments(update, context, False) + except Exception as e: + return Replier.markdown(update, str(e)) - groups = self.group_repository.get_by_chat_id(message_data.chat_id) + try: + groups = self.group_repository.get_by_chat_id(message_data.chat_id) + Replier.html(update, MessageBuilder.group_message(groups)) - if groups: - self.reply_html(update, self.build_groups_message(groups)) - return self.log_action(message_data) - - self.reply_markdown(update, no_groups) - - def get_bot_handler(self) -> CommandHandler: - return self.bot_handler - - def log_action(self, message_data: MessageData) -> None: - Logger.info(f'User {message_data.username} called /groups for {message_data.chat_id}') - - def build_groups_message(self, groups: Iterable[Group]) -> str: - resultTable = pt.PrettyTable(['Name', 'Members']) - - resultTable.add_rows([[record.group_name, record.users_count] for record in groups]) - - return f'
{str(resultTable)}
' + Logger.action(message_data, self.action) + except NotFoundException: + Replier.markdown(update, no_groups) diff --git a/src/bot/handler/inlineQueryHandler.py b/src/bot/handler/inlineQueryHandler.py index 059c209..f2a5d88 100644 --- a/src/bot/handler/inlineQueryHandler.py +++ b/src/bot/handler/inlineQueryHandler.py @@ -1,21 +1,29 @@ -from bot.handler.abstractHandler import AbstractHandler -from entity.group import Group from telegram import InlineQueryResultArticle from telegram.ext.callbackcontext import CallbackContext -from telegram.ext.commandhandler import CommandHandler from telegram.ext.inlinequeryhandler import \ InlineQueryHandler as CoreInlineQueryHandler from telegram.inline.inputtextmessagecontent import InputTextMessageContent from telegram.update import Update +from bot.handler.abstractHandler import AbstractHandler +from entity.group import Group +from exception.actionNotAllowedException import ActionNotAllowedException +from validator.accessValidator import AccessValidator + class InlineQueryHandler(AbstractHandler): - bot_handler: CommandHandler + bot_handler: CoreInlineQueryHandler def __init__(self) -> None: self.bot_handler = CoreInlineQueryHandler(self.handle) def handle(self, update: Update, context: CallbackContext) -> None: + try: + AccessValidator.validate(str(update.effective_user.id)) + except ActionNotAllowedException: + update.inline_query.answer([]) + return + group_display = update.inline_query.query or Group.default_name group = '' if group_display == Group.default_name else group_display @@ -41,6 +49,3 @@ class InlineQueryHandler(AbstractHandler): ] update.inline_query.answer(results, cache_time=4800) - - def get_bot_handler(self) -> CoreInlineQueryHandler: - return self.bot_handler diff --git a/src/bot/handler/joinHandler.py b/src/bot/handler/joinHandler.py index 5262a16..8252033 100755 --- a/src/bot/handler/joinHandler.py +++ b/src/bot/handler/joinHandler.py @@ -1,46 +1,41 @@ -from telegram.utils.helpers import mention_markdown -from bot.handler.abstractHandler import AbstractHandler -from bot.message.messageData import MessageData -from config.contents import joined, not_joined -from exception.invalidArgumentException import InvalidArgumentException -from exception.notFoundException import NotFoundException -from logger import Logger -from repository.userRepository import UserRepository from telegram.ext.callbackcontext import CallbackContext from telegram.ext.commandhandler import CommandHandler from telegram.update import Update +from bot.handler.abstractHandler import AbstractHandler +from bot.message.messageData import MessageData +from bot.message.replier import Replier +from config.contents import joined, not_joined +from exception.notFoundException import NotFoundException +from logger import Logger +from repository.userRepository import UserRepository + class JoinHandler(AbstractHandler): bot_handler: CommandHandler user_repository: UserRepository + action: str = 'join' def __init__(self) -> None: - self.bot_handler = CommandHandler('join', self.handle) + self.bot_handler = CommandHandler(self.action, self.handle) self.user_repository = UserRepository() def handle(self, update: Update, context: CallbackContext) -> None: try: message_data = MessageData.create_from_arguments(update, context) - except InvalidArgumentException as e: - return self.reply_markdown(update, str(e)) + except Exception as e: + return Replier.markdown(update, str(e)) try: user = self.user_repository.get_by_id(message_data.user_id) if user.is_in_chat(message_data.chat_id): - return self.reply_markdown(update, self.interpolate_reply(not_joined, message_data)) + return Replier.markdown(update, Replier.interpolate(not_joined, message_data)) user.add_to_chat(message_data.chat_id) self.user_repository.save(user) except NotFoundException: self.user_repository.save_by_message_data(message_data) - self.reply_markdown(update, self.interpolate_reply(joined, message_data)) - self.log_action(message_data) - - def get_bot_handler(self) -> CommandHandler: - return self.bot_handler - - def log_action(self, message_data: MessageData) -> None: - Logger.info(f'User {message_data.username} joined {message_data.chat_id}') + Replier.markdown(update, Replier.interpolate(joined, message_data)) + Logger.action(message_data, self.action) diff --git a/src/bot/handler/leaveHandler.py b/src/bot/handler/leaveHandler.py index 73b7588..75a4e97 100755 --- a/src/bot/handler/leaveHandler.py +++ b/src/bot/handler/leaveHandler.py @@ -1,45 +1,37 @@ -from bot.handler.abstractHandler import AbstractHandler -from bot.message.messageData import MessageData -from config.contents import left, not_left -from exception.invalidArgumentException import InvalidArgumentException -from exception.notFoundException import NotFoundException -from logger import Logger -from repository.userRepository import UserRepository from telegram.ext.callbackcontext import CallbackContext from telegram.ext.commandhandler import CommandHandler from telegram.update import Update +from bot.handler.abstractHandler import AbstractHandler +from bot.message.messageData import MessageData +from bot.message.replier import Replier +from config.contents import left, not_left +from exception.notFoundException import NotFoundException +from logger import Logger +from repository.userRepository import UserRepository + class LeaveHandler(AbstractHandler): bot_handler: CommandHandler user_repository: UserRepository + action: str = 'leave' def __init__(self) -> None: - self.bot_handler = CommandHandler('leave', self.handle) + self.bot_handler = CommandHandler(self.action, self.handle) self.user_repository = UserRepository() def handle(self, update: Update, context: CallbackContext) -> None: try: message_data = MessageData.create_from_arguments(update, context) - except InvalidArgumentException as e: - return self.reply_markdown(update, str(e)) + except Exception as e: + return Replier.markdown(update, str(e)) try: - user = self.user_repository.get_by_id(message_data.user_id) + user = self.user_repository.get_by_id_and_chat_id(message_data.user_id, message_data.chat_id) + user.remove_from_chat(message_data.chat_id) + self.user_repository.save(user) - if not user.is_in_chat(message_data.chat_id): - raise NotFoundException() + Replier.markdown(update, Replier.interpolate(left, message_data)) + Logger.action(message_data, self.action) except NotFoundException: - return self.reply_markdown(update, self.interpolate_reply(not_left, message_data)) - - user.remove_from_chat(message_data.chat_id) - self.user_repository.save(user) - - self.reply_markdown(update, self.interpolate_reply(left, message_data)) - self.log_action(message_data) - - def get_bot_handler(self) -> CommandHandler: - return self.bot_handler - - def log_action(self, message_data: MessageData) -> None: - Logger.info(f'User {message_data.username} left {message_data.chat_id}') + return Replier.markdown(update, Replier.interpolate(not_left, message_data)) diff --git a/src/bot/handler/mentionHandler.py b/src/bot/handler/mentionHandler.py deleted file mode 100755 index d44e8cb..0000000 --- a/src/bot/handler/mentionHandler.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Iterable - -from telegram.utils.helpers import mention_markdown - -from bot.handler.abstractHandler import AbstractHandler -from bot.message.messageData import MessageData -from config.contents import mention_failed -from entity.user import User -from exception.invalidArgumentException import InvalidArgumentException -from logger import Logger -from repository.userRepository import UserRepository -from telegram.ext.callbackcontext import CallbackContext -from telegram.ext.commandhandler import CommandHandler -from telegram.update import Update - - -class MentionHandler(AbstractHandler): - bot_handler: CommandHandler - user_repository: UserRepository - - def __init__(self) -> None: - self.bot_handler = CommandHandler('everyone', self.handle) - self.user_repository = UserRepository() - - def handle(self, update: Update, context: CallbackContext) -> None: - try: - message_data = MessageData.create_from_arguments(update, context) - except InvalidArgumentException as e: - return self.reply_markdown(update, str(e)) - - users = self.user_repository.get_all_for_chat(message_data.chat_id) - - if users: - self.reply_markdown(update, self.build_mention_message(users)) - return self.log_action(message_data) - - self.reply_markdown(update, mention_failed) - - def get_bot_handler(self) -> CommandHandler: - return self.bot_handler - - def log_action(self, message_data: MessageData) -> None: - Logger.info(f'User {message_data.username} called /everyone for {message_data.chat_id}') - - def build_mention_message(self, users: Iterable[User]) -> str: - return ' '.join([mention_markdown(user.user_id, user.username) for user in users]) - diff --git a/src/bot/handler/silentMentionHandler.py b/src/bot/handler/silentMentionHandler.py deleted file mode 100644 index c1a0248..0000000 --- a/src/bot/handler/silentMentionHandler.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Iterable - -from entity.user import User -from logger import Logger -from telegram.ext.commandhandler import CommandHandler - -from bot.handler.abstractHandler import AbstractHandler -from bot.handler.mentionHandler import MentionHandler -from bot.message.messageData import MessageData - - -class MentionHandler(MentionHandler, AbstractHandler): - def __init__(self) -> None: - super().__init__() - self.bot_handler = CommandHandler('silent', self.handle) - - def build_mention_message(self, users: Iterable[User]) -> str: - return ' '.join([user.username for user in users]) - - def log_action(self, message_data: MessageData) -> None: - Logger.info(f'User {message_data.username} called /silent for {message_data.chat_id}') diff --git a/src/bot/handler/startHandler.py b/src/bot/handler/startHandler.py index 8454d08..0c1f283 100644 --- a/src/bot/handler/startHandler.py +++ b/src/bot/handler/startHandler.py @@ -1,25 +1,25 @@ -from config.contents import start_text -from logger import Logger from telegram.ext.callbackcontext import CallbackContext from telegram.ext.commandhandler import CommandHandler from telegram.update import Update from bot.handler.abstractHandler import AbstractHandler from bot.message.messageData import MessageData +from bot.message.replier import Replier +from config.contents import start_text +from logger import Logger class StartHandler(AbstractHandler): bot_handler: CommandHandler + action: str = 'start' def __init__(self) -> None: - self.bot_handler = CommandHandler('start', self.handle) + self.bot_handler = CommandHandler(self.action, self.handle) def handle(self, update: Update, context: CallbackContext) -> None: - self.reply_markdown(update, start_text) - self.log_action(MessageData.create_from_arguments(update, context)) - - def get_bot_handler(self) -> CommandHandler: - return self.bot_handler - - def log_action(self, message_data: MessageData) -> None: - Logger.info(f'User {message_data.username} called /start for {message_data.chat_id}') + try: + MessageData.create_from_arguments(update, context) + except Exception as e: + return Replier.markdown(update, str(e)) + Replier.markdown(update, start_text) + Logger.action(MessageData.create_from_arguments(update, context), self.action) diff --git a/src/bot/message/messageData.py b/src/bot/message/messageData.py index a63420e..de04f31 100644 --- a/src/bot/message/messageData.py +++ b/src/bot/message/messageData.py @@ -1,19 +1,18 @@ from __future__ import annotations -import re from dataclasses import dataclass import names -from entity.group import Group -from exception.invalidArgumentException import InvalidArgumentException from telegram.ext.callbackcontext import CallbackContext from telegram.update import Update +from entity.group import Group +from validator.accessValidator import AccessValidator from validator.groupNameValidator import GroupNameValidator @dataclass -class MessageData(): +class MessageData: user_id: str chat_id: str group_name: str @@ -21,6 +20,9 @@ class MessageData(): @staticmethod def create_from_arguments(update: Update, context: CallbackContext, include_group: bool = True) -> MessageData: + user_id = str(update.effective_user.id) + AccessValidator.validate(user_id) + chat_id = str(update.effective_chat.id) group_name = Group.default_name @@ -31,9 +33,7 @@ class MessageData(): if group_name is not Group.default_name: chat_id += f'~{group_name}' - - user_id = str(update.effective_user.id) username = update.effective_user.username or update.effective_user.first_name if not username: diff --git a/src/bot/message/replier.py b/src/bot/message/replier.py new file mode 100644 index 0000000..b2f7c02 --- /dev/null +++ b/src/bot/message/replier.py @@ -0,0 +1,29 @@ +from telegram import Update +from telegram.utils.helpers import mention_markdown + +from bot.message.messageData import MessageData +from logger import Logger + + +class Replier: + + @staticmethod + def interpolate(content: str, message_data: MessageData): + return content.format( + mention_markdown(message_data.user_id, message_data.username), + message_data.group_name + ) + + @staticmethod + def markdown(update: Update, message: str) -> None: + try: + update.effective_message.reply_markdown_v2(message) + except Exception as err: + Logger.error(str(err)) + + @staticmethod + def html(update: Update, html: str) -> None: + try: + update.effective_message.reply_html(html) + except Exception as err: + Logger.error(str(err)) diff --git a/src/config/contents.py b/src/config/contents.py index 8a7f81f..3a750dc 100755 --- a/src/config/contents.py +++ b/src/config/contents.py @@ -5,7 +5,6 @@ not_left = '{} did not join group `{}` before' mention_failed = 'There are no users to mention' no_groups = 'There are no groups for this chat' - start_text = """ Hello there I am @everyone\_mention\_bot @@ -32,4 +31,4 @@ To display all members in a group: You can also try to tag me @everyone\_mention\_bot and then enter group name Possible results will be displayed -""" \ No newline at end of file +""" diff --git a/src/config/credentials.py b/src/config/credentials.py index ef89331..fccb6bd 100755 --- a/src/config/credentials.py +++ b/src/config/credentials.py @@ -13,3 +13,5 @@ MONGODB_USERNAME = os.environ['MONGODB_USERNAME'] MONGODB_PASSWORD = os.environ['MONGODB_PASSWORD'] MONGODB_HOSTNAME = os.environ['MONGODB_HOSTNAME'] MONGODB_PORT = os.environ['MONGODB_PORT'] + +BANNED_USERS = os.environ['BANNED_USERS'].split(',') or [] diff --git a/src/database/client.py b/src/database/client.py index e3ef7be..e8df0be 100755 --- a/src/database/client.py +++ b/src/database/client.py @@ -1,13 +1,14 @@ from urllib.parse import quote_plus +from pymongo import MongoClient +from pymongo.database import Database + from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME, MONGODB_PASSWORD, MONGODB_PORT, MONGODB_USERNAME) -from pymongo import MongoClient -from pymongo.database import Database -class Client(): +class Client: mongo_client: MongoClient database: Database @@ -32,7 +33,7 @@ class Client(): def update_one(self, collection: str, filter: dict, data: dict) -> None: self.database.get_collection(collection).update_one( filter, - { "$set" : data } + {"$set": data} ) def aggregate(self, collection, pipeline: list): diff --git a/src/entity/group.py b/src/entity/group.py index ff526f9..113202d 100644 --- a/src/entity/group.py +++ b/src/entity/group.py @@ -4,7 +4,7 @@ from dataclasses import dataclass @dataclass -class Group(): +class Group: chat_id: str group_name: str users_count: int diff --git a/src/entity/user.py b/src/entity/user.py index 50a32a8..75611db 100644 --- a/src/entity/user.py +++ b/src/entity/user.py @@ -1,14 +1,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Iterable +from typing import List @dataclass -class User(): +class User: user_id: str username: str - chats: Iterable[str] + chats: List[str] collection: str = 'users' id_index: str = '_id' diff --git a/src/exception/actionNotAllowedException.py b/src/exception/actionNotAllowedException.py new file mode 100644 index 0000000..e20e369 --- /dev/null +++ b/src/exception/actionNotAllowedException.py @@ -0,0 +1,2 @@ +class ActionNotAllowedException(Exception): + pass diff --git a/src/logger.py b/src/logger.py index 4d0bbba..51bcd27 100644 --- a/src/logger.py +++ b/src/logger.py @@ -3,7 +3,10 @@ from __future__ import annotations import logging import os +from bot.message.messageData import MessageData + +# noinspection SpellCheckingInspection class Logger: action_logger: str = 'action-logger' action_logger_file: str = '/var/log/bot/action.log' @@ -11,9 +14,10 @@ class Logger: main_logger: str = 'main-logger' main_logger_file: str = '/var/log/bot/app.log' - formatter: str = logging.Formatter('%(asctime)s[%(levelname)s]: %(message)s') + # noinspection SpellCheckingInspection + formatter: logging.Formatter = logging.Formatter('%(asctime)s[%(levelname)s]: %(message)s') - def setup(self) -> None: + def __init__(self): self.configure(self.action_logger, self.action_logger_file, logging.INFO) self.configure(self.main_logger, self.main_logger_file, logging.ERROR) @@ -32,12 +36,22 @@ class Logger: logger.addHandler(file_handler) logger.addHandler(stream_handler) + @staticmethod + def register() -> None: + Logger() + @staticmethod def get_logger(logger_name) -> logging.Logger: return logging.getLogger(logger_name) + @staticmethod def info(message: str) -> None: Logger.get_logger(Logger.action_logger).info(message) + @staticmethod def error(message: str) -> None: - Logger.get_logger(Logger.main_logger).error(message) \ No newline at end of file + Logger.get_logger(Logger.main_logger).error(message) + + @staticmethod + def action(message_data: MessageData, action: str) -> None: + Logger.info(f'User {message_data.username}({message_data.user_id}) called {action.upper()} for {message_data.chat_id}') diff --git a/src/repository/groupRepository.py b/src/repository/groupRepository.py index 82363df..43832e0 100644 --- a/src/repository/groupRepository.py +++ b/src/repository/groupRepository.py @@ -1,13 +1,13 @@ -import itertools import re from typing import Iterable from database.client import Client from entity.group import Group from entity.user import User +from exception.notFoundException import NotFoundException -class GroupRepository(): +class GroupRepository: client: Client count: str = 'count' @@ -19,22 +19,22 @@ class GroupRepository(): groups = self.client.aggregate( User.collection, [ - { "$unwind": f'${User.chats_index}' }, + {"$unwind": f'${User.chats_index}'}, { "$match": { - User.chats_index: { "$regex": re.compile(f'^{chat_id}.*$') }, + User.chats_index: {"$regex": re.compile(f'^{chat_id}.*$')}, }, }, { "$group": { "_id": { - "$last": { "$split": [f'${User.chats_index}', "~"] }, + "$last": {"$split": [f'${User.chats_index}', "~"]}, }, - self.count: { "$count": {} }, + self.count: {"$count": {}}, }, }, { - "$sort": { '_id': 1 } + "$sort": {'_id': 1} } ] ) @@ -50,4 +50,7 @@ class GroupRepository(): Group(chat_id, group_name, group[self.count]) ) + if not groups: + raise NotFoundException + return result diff --git a/src/repository/userRepository.py b/src/repository/userRepository.py index d4371f6..94d5d62 100644 --- a/src/repository/userRepository.py +++ b/src/repository/userRepository.py @@ -1,4 +1,4 @@ -from typing import Iterable, Optional +from typing import Iterable from bot.message.messageData import MessageData from database.client import Client @@ -6,57 +6,65 @@ from entity.user import User from exception.notFoundException import NotFoundException -class UserRepository(): +class UserRepository: client: Client def __init__(self) -> None: self.client = Client() - def get_by_id(self, id: str) -> User: + def get_by_id(self, user_id: str) -> User: user = self.client.find_one( User.collection, { - User.id_index: id + User.id_index: user_id } ) if not user: - raise NotFoundException(f'Could not find user with "{id}" id') + raise NotFoundException(f'Could not find user with "{user_id}" id') return User( user[User.id_index], user[User.username_index], user[User.chats_index] ) - + + def get_by_id_and_chat_id(self, user_id: str, chat_id: str) -> User: + user = self.get_by_id(user_id) + + if not user.is_in_chat(chat_id): + raise NotFoundException + + return user + def save(self, user: User) -> None: self.client.update_one( User.collection, - { User.id_index: user.user_id }, + {User.id_index: user.user_id}, user.to_mongo_document() ) def save_by_message_data(self, data: MessageData) -> None: self.client.insert_one( - User.collection, + User.collection, { User.id_index: data.user_id, User.username_index: data.username, User.chats_index: [data.chat_id] } ) - + def get_all_for_chat(self, chat_id: str) -> Iterable[User]: result = [] users = self.client.find_many( User.collection, { User.chats_index: { - "$in" : [chat_id] + "$in": [chat_id] } } ) - + for record in users: result.append(User.from_mongo_document(record)) diff --git a/src/utils/messageBuilder.py b/src/utils/messageBuilder.py new file mode 100644 index 0000000..396664e --- /dev/null +++ b/src/utils/messageBuilder.py @@ -0,0 +1,21 @@ +from typing import Iterable + +from prettytable import prettytable +from telegram.utils.helpers import mention_markdown + +from entity.group import Group +from entity.user import User + + +class MessageBuilder: + @staticmethod + def group_message(groups: Iterable[Group]) -> str: + table = prettytable.PrettyTable(['Name', 'Members']) + + table.add_rows([[record.group_name, record.users_count] for record in groups]) + + return f'
{str(table)}
' + + @staticmethod + def mention_message(users: Iterable[User]) -> str: + return ' '.join([mention_markdown(user.user_id, user.username) for user in users]) diff --git a/src/validator/accessValidator.py b/src/validator/accessValidator.py new file mode 100644 index 0000000..8d1dac8 --- /dev/null +++ b/src/validator/accessValidator.py @@ -0,0 +1,10 @@ +from config.credentials import BANNED_USERS +from exception.actionNotAllowedException import ActionNotAllowedException + + +class AccessValidator: + + @staticmethod + def validate(user_id: str) -> None: + if user_id in BANNED_USERS: + raise ActionNotAllowedException('You are banned')