diff --git a/CHANGELOG.md b/CHANGELOG.md index ab1209b..8d7c61c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,16 @@ # Change Log All notable changes to this project will be documented in this file. -## [UNRELEASED] - 07.10.2021 +## [UNRELEASED] - 11.10.2021 ### Added -- Inline Query for join/leave/everyone -- Group name validator -- Banned users env -- Access validator -- ActionNotAllowedException - +- Inline Query for `join`, `leave` & `everyone` +- Banned users +- Buttons for `start` message ### Changed -- code quality improvements - +- Code quality improvements +- `start` text ### Deleted - `/silent` command - -### Updated -- start command content ## [0.1.0] - 06.10.2021 ### Features - `/join` command diff --git a/README.md b/README.md index 78eda54..d37470f 100755 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git - copy environment files and fulfill empty values ```bash cp .env.local .env -cp docker/config/app.dist.env docker/config/app.env -cp docker/config/database.dist.env docker/config/app.env +cp docker/config/app.env.local docker/config/app.env +cp docker/config/database.env.local docker/config/app.env ``` - start the project (`-d` flag will run containers in detached mode) ```bash diff --git a/docker/config/app.dist.env b/docker/config/app.env.local similarity index 100% rename from docker/config/app.dist.env rename to docker/config/app.env.local diff --git a/docker/config/database.dist.env b/docker/config/database.env.local similarity index 100% rename from docker/config/database.dist.env rename to docker/config/database.env.local diff --git a/src/app.py b/src/app.py index 67ac0b3..a5ccf9b 100755 --- a/src/app.py +++ b/src/app.py @@ -3,7 +3,7 @@ from telegram.ext.dispatcher import Dispatcher from bot.handler import * from bot.handler.abstractHandler import AbstractHandler -from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL +from config.envs import BOT_TOKEN, PORT, WEBHOOK_URL from logger import Logger diff --git a/src/bot/handler/abstractHandler.py b/src/bot/handler/abstractHandler.py index ea57502..c52118d 100755 --- a/src/bot/handler/abstractHandler.py +++ b/src/bot/handler/abstractHandler.py @@ -4,10 +4,31 @@ from telegram.ext import Handler from telegram.ext.callbackcontext import CallbackContext from telegram.update import Update +from bot.message.inboundMessage import InboundMessage +from bot.message.replier import Replier +from exception.actionNotAllowedException import ActionNotAllowedException +from exception.invalidArgumentException import InvalidArgumentException +from logger import Logger + class AbstractHandler: bot_handler: Handler + inbound: InboundMessage @abstractmethod def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented') + + def wrap(self, update: Update, context: CallbackContext) -> None: + try: + group_specific = self.is_group_specific() + + self.inbound = InboundMessage.create(update, context, group_specific) + self.handle(update, context) + except (ActionNotAllowedException, InvalidArgumentException) as e: + Replier.markdown(update, str(e)) + except Exception as e: + Logger.exception(e) + + def is_group_specific(self) -> bool: + return True diff --git a/src/bot/handler/everyoneHandler.py b/src/bot/handler/everyoneHandler.py index 9f717e8..36ac603 100755 --- a/src/bot/handler/everyoneHandler.py +++ b/src/bot/handler/everyoneHandler.py @@ -3,9 +3,9 @@ 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 exception.notFoundException import NotFoundException from logger import Logger from repository.userRepository import UserRepository from utils.messageBuilder import MessageBuilder @@ -17,19 +17,14 @@ class EveryoneHandler(AbstractHandler): action: str = 'everyone' def __init__(self) -> None: - self.bot_handler = CommandHandler(self.action, self.handle) + self.bot_handler = CommandHandler(self.action, self.wrap) 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) + users = self.user_repository.get_all_for_chat(self.inbound.chat_id) - Replier.markdown(update, mention_failed) + Replier.markdown(update, MessageBuilder.mention_message(users)) + Logger.action(self.inbound, self.action) + except NotFoundException: + Replier.markdown(update, mention_failed) diff --git a/src/bot/handler/groupsHandler.py b/src/bot/handler/groupsHandler.py index 9414d5a..deff1c4 100644 --- a/src/bot/handler/groupsHandler.py +++ b/src/bot/handler/groupsHandler.py @@ -3,7 +3,6 @@ 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 @@ -18,19 +17,17 @@ class GroupsHandler(AbstractHandler): action: str = 'groups' def __init__(self) -> None: - self.bot_handler = CommandHandler(self.action, self.handle) + self.bot_handler = CommandHandler(self.action, self.wrap) self.group_repository = GroupRepository() def handle(self, update: Update, context: CallbackContext) -> None: try: - message_data = MessageData.create_from_arguments(update, context, False) - except Exception as e: - return Replier.markdown(update, str(e)) - - try: - groups = self.group_repository.get_by_chat_id(message_data.chat_id) + groups = self.group_repository.get_by_chat_id(self.inbound.chat_id) Replier.html(update, MessageBuilder.group_message(groups)) - Logger.action(message_data, self.action) + Logger.action(self.inbound, self.action) except NotFoundException: Replier.markdown(update, no_groups) + + def is_group_specific(self) -> bool: + return False diff --git a/src/bot/handler/joinHandler.py b/src/bot/handler/joinHandler.py index 8252033..d6f2067 100755 --- a/src/bot/handler/joinHandler.py +++ b/src/bot/handler/joinHandler.py @@ -3,7 +3,6 @@ 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 @@ -17,25 +16,20 @@ class JoinHandler(AbstractHandler): action: str = 'join' def __init__(self) -> None: - self.bot_handler = CommandHandler(self.action, self.handle) + self.bot_handler = CommandHandler(self.action, self.wrap) 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)) + user = self.user_repository.get_by_id(self.inbound.user_id) - try: - user = self.user_repository.get_by_id(message_data.user_id) + if user.is_in_chat(self.inbound.chat_id): + return Replier.markdown(update, Replier.interpolate(not_joined, self.inbound)) - if user.is_in_chat(message_data.chat_id): - return Replier.markdown(update, Replier.interpolate(not_joined, message_data)) - - user.add_to_chat(message_data.chat_id) + user.add_to_chat(self.inbound.chat_id) self.user_repository.save(user) except NotFoundException: - self.user_repository.save_by_message_data(message_data) + self.user_repository.save_by_inbound_message(self.inbound) - Replier.markdown(update, Replier.interpolate(joined, message_data)) - Logger.action(message_data, self.action) + Replier.markdown(update, Replier.interpolate(joined, self.inbound)) + Logger.action(self.inbound, self.action) diff --git a/src/bot/handler/leaveHandler.py b/src/bot/handler/leaveHandler.py index 75a4e97..e031596 100755 --- a/src/bot/handler/leaveHandler.py +++ b/src/bot/handler/leaveHandler.py @@ -3,7 +3,6 @@ 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 @@ -17,21 +16,16 @@ class LeaveHandler(AbstractHandler): action: str = 'leave' def __init__(self) -> None: - self.bot_handler = CommandHandler(self.action, self.handle) + self.bot_handler = CommandHandler(self.action, self.wrap) 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)) - - try: - 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) + user = self.user_repository.get_by_id_and_chat_id(self.inbound.user_id, self.inbound.chat_id) + user.remove_from_chat(self.inbound.chat_id) self.user_repository.save(user) - Replier.markdown(update, Replier.interpolate(left, message_data)) - Logger.action(message_data, self.action) + Replier.markdown(update, Replier.interpolate(left, self.inbound)) + Logger.action(self.inbound, self.action) except NotFoundException: - return Replier.markdown(update, Replier.interpolate(not_left, message_data)) + return Replier.markdown(update, Replier.interpolate(not_left, self.inbound)) diff --git a/src/bot/handler/startHandler.py b/src/bot/handler/startHandler.py index 0c1f283..46e0427 100644 --- a/src/bot/handler/startHandler.py +++ b/src/bot/handler/startHandler.py @@ -1,9 +1,9 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup 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 @@ -14,12 +14,23 @@ class StartHandler(AbstractHandler): action: str = 'start' def __init__(self) -> None: - self.bot_handler = CommandHandler(self.action, self.handle) + self.bot_handler = CommandHandler(self.action, self.wrap) def handle(self, update: Update, context: CallbackContext) -> None: - 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) + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton('Inline Mode', switch_inline_query_current_chat='example'), + ], + [ + InlineKeyboardButton('GitHub', url='https://github.com/miloszowi/everyone-mention-telegram-bot'), + InlineKeyboardButton('Creator', url='https://t.me/miloszowi') + ] + ] + ) + + Replier.html(update, start_text, markup) + Logger.action(self.inbound, self.action) + + def is_group_specific(self) -> bool: + return False diff --git a/src/bot/message/messageData.py b/src/bot/message/inboundMessage.py similarity index 78% rename from src/bot/message/messageData.py rename to src/bot/message/inboundMessage.py index de04f31..697f7c6 100644 --- a/src/bot/message/messageData.py +++ b/src/bot/message/inboundMessage.py @@ -12,21 +12,21 @@ from validator.groupNameValidator import GroupNameValidator @dataclass -class MessageData: +class InboundMessage: user_id: str chat_id: str group_name: str username: str @staticmethod - def create_from_arguments(update: Update, context: CallbackContext, include_group: bool = True) -> MessageData: + def create(update: Update, context: CallbackContext, group_specific: bool) -> InboundMessage: user_id = str(update.effective_user.id) AccessValidator.validate(user_id) chat_id = str(update.effective_chat.id) group_name = Group.default_name - if context.args and context.args[0] and include_group: + if context.args and context.args[0] and group_specific: group_name = str(context.args[0]).lower() GroupNameValidator.validate(group_name) @@ -39,4 +39,4 @@ class MessageData: if not username: username = names.get_first_name() - return MessageData(user_id, chat_id, group_name, username) + return InboundMessage(user_id, chat_id, group_name, username) diff --git a/src/bot/message/replier.py b/src/bot/message/replier.py index b2f7c02..6d207dd 100644 --- a/src/bot/message/replier.py +++ b/src/bot/message/replier.py @@ -1,29 +1,31 @@ -from telegram import Update +from typing import Optional + +from telegram import InlineKeyboardMarkup, Update from telegram.utils.helpers import mention_markdown -from bot.message.messageData import MessageData +from bot.message.inboundMessage import InboundMessage from logger import Logger class Replier: @staticmethod - def interpolate(content: str, message_data: MessageData): + def interpolate(content: str, inbound_message: InboundMessage): return content.format( - mention_markdown(message_data.user_id, message_data.username), - message_data.group_name + mention_markdown(inbound_message.user_id, inbound_message.username), + inbound_message.group_name ) @staticmethod - def markdown(update: Update, message: str) -> None: + def markdown(update: Update, message: str, reply_markup: Optional[InlineKeyboardMarkup] = None) -> None: try: - update.effective_message.reply_markdown_v2(message) + update.effective_message.reply_markdown_v2(message, reply_markup=reply_markup) except Exception as err: Logger.error(str(err)) @staticmethod - def html(update: Update, html: str) -> None: + def html(update: Update, html: str, reply_markup: Optional[InlineKeyboardMarkup] = None) -> None: try: - update.effective_message.reply_html(html) + update.effective_message.reply_html(html, reply_markup=reply_markup, disable_web_page_preview=True) except Exception as err: Logger.error(str(err)) diff --git a/src/config/contents.py b/src/config/contents.py index 9b5e7b7..3aa3cdb 100755 --- a/src/config/contents.py +++ b/src/config/contents.py @@ -1,3 +1,4 @@ +# markdownv2 python-telegram-bot specific joined = '{} joined group `{}`' not_joined = '{} is already in group `{}`' left = '{} left group `{}`' @@ -5,27 +6,38 @@ not_left = '{} did not join group `{}` before' mention_failed = 'There are no users to mention' no_groups = 'There are no groups for this chat' + +# html python-telegram-bot specific start_text = """ -Hello there -I am @everyone\_mention\_bot -I am here to help you with mass notifies +Hello! +@everyone_mention_bot here. +I am here to help you with multiple user mentions. -Please take a look at available commands -`` is not required, if not given, it is set to `default` +Using Inline Mode is recommended because policy of bots with privacy mode enabled says that command trigger is sent (without mentioning the bot) only to the last mentioned bot. So if you do have multiple bots in current chat, I might not receive your command! -To join group: -`/join ` -for example: `/join games` +Available commands: +Please note +{group-name} is not required, default if not given. -To leave group: -`/leave ` +Join +Joins (or creates if group did not exist before) group. +
/join {group-name}
-To gather everyone attention use: -`/everyone ` +Leave +Leaves (or deletes if no other users are left) the group +
/leave {group-name}
-To see all available groups use: -`/groups` +Everyone +Mentions everyone that joined the group. +
/everyone {group-name}
-You can also try to tag me @everyone\_mention\_bot and then enter group name -Possible results will be displayed +Groups +Show all created groups in this chat. +
/groups
+ +Start +Show start & help text +
/start
+ +Reach out to Creator in case of any issues/questions regarding my usage. """ diff --git a/src/config/credentials.py b/src/config/envs.py similarity index 100% rename from src/config/credentials.py rename to src/config/envs.py diff --git a/src/database/client.py b/src/database/client.py index e8df0be..5ea9f50 100755 --- a/src/database/client.py +++ b/src/database/client.py @@ -3,9 +3,9 @@ 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 config.envs import (MONGODB_DATABASE, MONGODB_HOSTNAME, + MONGODB_PASSWORD, MONGODB_PORT, + MONGODB_USERNAME) class Client: diff --git a/src/logger.py b/src/logger.py index 51bcd27..6dc33f5 100644 --- a/src/logger.py +++ b/src/logger.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import os -from bot.message.messageData import MessageData +from bot.message.inboundMessage import InboundMessage # noinspection SpellCheckingInspection @@ -41,17 +41,21 @@ class Logger: Logger() @staticmethod - def get_logger(logger_name) -> logging.Logger: + def get(logger_name: str) -> logging.Logger: return logging.getLogger(logger_name) @staticmethod def info(message: str) -> None: - Logger.get_logger(Logger.action_logger).info(message) + Logger.get(Logger.action_logger).info(message) @staticmethod def error(message: str) -> None: - Logger.get_logger(Logger.main_logger).error(message) + Logger.get(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}') + def exception(exception: Exception) -> None: + Logger.get(Logger.main_logger).exception(exception) + + @staticmethod + def action(inbound: InboundMessage, action: str) -> None: + Logger.info(f'User {inbound.username}({inbound.user_id}) called {action.upper()} for {inbound.chat_id}') diff --git a/src/repository/userRepository.py b/src/repository/userRepository.py index 94d5d62..0ea3716 100644 --- a/src/repository/userRepository.py +++ b/src/repository/userRepository.py @@ -1,6 +1,6 @@ from typing import Iterable -from bot.message.messageData import MessageData +from bot.message.inboundMessage import InboundMessage from database.client import Client from entity.user import User from exception.notFoundException import NotFoundException @@ -44,13 +44,13 @@ class UserRepository: user.to_mongo_document() ) - def save_by_message_data(self, data: MessageData) -> None: + def save_by_inbound_message(self, inbound_message: InboundMessage) -> None: self.client.insert_one( User.collection, { - User.id_index: data.user_id, - User.username_index: data.username, - User.chats_index: [data.chat_id] + User.id_index: inbound_message.user_id, + User.username_index: inbound_message.username, + User.chats_index: [inbound_message.chat_id] } ) @@ -68,4 +68,7 @@ class UserRepository: for record in users: result.append(User.from_mongo_document(record)) + if not result: + raise NotFoundException + return result diff --git a/src/validator/accessValidator.py b/src/validator/accessValidator.py index 8d1dac8..135bce2 100644 --- a/src/validator/accessValidator.py +++ b/src/validator/accessValidator.py @@ -1,4 +1,4 @@ -from config.credentials import BANNED_USERS +from config.envs import BANNED_USERS from exception.actionNotAllowedException import ActionNotAllowedException @@ -7,4 +7,4 @@ class AccessValidator: @staticmethod def validate(user_id: str) -> None: if user_id in BANNED_USERS: - raise ActionNotAllowedException('You are banned') + raise ActionNotAllowedException('User is banned')