diff --git a/CHANGELOG.md b/CHANGELOG.md index 8046c8a..24d47a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log All notable changes to this project will be documented in this file. +## [0.3.0] - 12.11.2021 +### Added +- Dynamic mentioning by `@` character +### Changed +- `start` text +- Group name validation - those are forbidden now - `all`, `channel`, `chat`, `everyone`, `group`, `here` +### Deleted +- mentioning user that performed `/everyone` or dynamic mention in bot response ## [0.2.0] - 26.10.2021 ### Added - Inline Mode for `join`, `leave` & `everyone` diff --git a/README.md b/README.md index 2204f60..8fcbbf1 100755 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ # Contents * [Description](#description) +* [Usage](#usage) +* [Dynamic Mentioning](#dynamic-mentioning) * [Commands](#commands) * [`/join`](#join) * [`/leave`](#leave) @@ -12,8 +14,7 @@ * [`/groups`](#groups) * [`/start`](#start) * [Example command flow](#example-command-flow) -* [Inline Mode](#inline-mode) - * [Usage](#usage) +* [Inline Mode Usage](#inline-mode-usage) * [Getting started.](#getting-started) * [Requirements](#requirements) * [Installation](#installation) @@ -24,6 +25,32 @@ Everyone Mention Bot is simple, but useful telegram bot to gather group members You can create groups per chat to mention every user that joined the group by calling one command instead of mentioning them one by one. +## Usage +First, users need to join the group to let mentioning them, to do that, they simply need to join specific group. +It can be done in 2 ways: +- Command [`/join`](#join) +- [Inline Mode](#inline-mode-usage) + +Users that have joined the group can be mentioned in 3 ways: +- [Dynaminc Mentioning](#dynamic-mentioning) by `@`, for example `@everyone` +- Command [`/everyone`](#everyone) +- [Inline Mode](#inline-mode-usage) + +To leave the group use one of the two ways: +- Command [`/leave`](#leave) +- [Inline Mode](#inline-mode-usage) + +To display available groups: +- Command [`/groups`](#groups) + +## Dynamic mentioning +You can use `@` character (as you would mention a user) to mention specific group. + +All the below will mention users from `default` group. + +`@all`, `@channel`, `@chat`, `@everyone`, `@group`, `@here`. + +If you did create a group named `gaming`, you can simply use `@gaming` in your text to mention them all. ## Commands *Important*: `{group-name}` is not required, if not given, it will be set to `default`. ### `/join` @@ -85,10 +112,7 @@ Start & Help message ### Example command flow ![example command flow](docs/flow_command.png) -## Inline Mode -Using Inline Mode is recommended because policy of bots with privacy mode enabled (https://core.telegram.org/bots/faq#what-messages-will-my-bot-get) 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! - -### Usage +### Inline Mode Usage To use inline mode, type `@everyone_mention_bot` in telegram message input or click on the `Inline Mode` button from `/start` command. ![inline popup](docs/inline_mode_1.png) diff --git a/src/bot/handler/__init__.py b/src/bot/handler/__init__.py index 3a55bbb..b5eceda 100644 --- a/src/bot/handler/__init__.py +++ b/src/bot/handler/__init__.py @@ -1,5 +1,5 @@ __all__ = [ 'abstractHandler', 'everyoneHandler', 'groupsHandler', 'inlineQueryHandler', 'joinHandler', 'leaveHandler', - 'startHandler' + 'startHandler', 'dynamicMentionHandler' ] diff --git a/src/bot/handler/abstractHandler.py b/src/bot/handler/abstractHandler.py index d686924..308ad58 100755 --- a/src/bot/handler/abstractHandler.py +++ b/src/bot/handler/abstractHandler.py @@ -9,6 +9,7 @@ from bot.message.replier import Replier from exception.actionNotAllowedException import ActionNotAllowedException from exception.invalidActionException import InvalidActionException from exception.invalidArgumentException import InvalidArgumentException +from exception.notFoundException import NotFoundException from logger import Logger @@ -30,6 +31,8 @@ class AbstractHandler: Logger.action(self.inbound, self.action) except (InvalidActionException, InvalidArgumentException, ActionNotAllowedException) as e: Replier.markdown(update, str(e)) + except NotFoundException: + pass # probably just mentioning user except Exception as e: Logger.exception(e) diff --git a/src/bot/handler/dynamicMentionHandler.py b/src/bot/handler/dynamicMentionHandler.py new file mode 100644 index 0000000..d5dfe6e --- /dev/null +++ b/src/bot/handler/dynamicMentionHandler.py @@ -0,0 +1,28 @@ +import re + +from telegram.ext import Filters, MessageHandler +from telegram.ext.callbackcontext import CallbackContext +from telegram.update import Update + +from bot.handler.abstractHandler import AbstractHandler +from bot.message.replier import Replier +from repository.chatRepository import ChatRepository +from utils.messageBuilder import MessageBuilder + + +class DynamicMentionHandler(AbstractHandler): + bot_handler: MessageHandler + chat_repository: ChatRepository + action: str = 'dynamic-mention' + + def __init__(self) -> None: + self.bot_handler = MessageHandler( + Filters.regex(re.compile(r'@[^ ]')), + self.wrap + ) + self.chat_repository = ChatRepository() + + def handle(self, update: Update, context: CallbackContext) -> None: + users = self.chat_repository.get_users_for_group(self.inbound) + + Replier.markdown(update, MessageBuilder.mention_message(users)) diff --git a/src/bot/handler/everyoneHandler.py b/src/bot/handler/everyoneHandler.py index a31feac..047ae65 100755 --- a/src/bot/handler/everyoneHandler.py +++ b/src/bot/handler/everyoneHandler.py @@ -8,24 +8,21 @@ from config.contents import mention_failed from exception.invalidActionException import InvalidActionException from exception.notFoundException import NotFoundException from repository.chatRepository import ChatRepository -from repository.userRepository import UserRepository from utils.messageBuilder import MessageBuilder class EveryoneHandler(AbstractHandler): bot_handler: CommandHandler chat_repository: ChatRepository - user_repository: UserRepository action: str = 'everyone' def __init__(self) -> None: self.bot_handler = CommandHandler(self.action, self.wrap) self.chat_repository = ChatRepository() - self.user_repository = UserRepository() def handle(self, update: Update, context: CallbackContext) -> None: try: - users = self.chat_repository.get_users_for_group(self.inbound.chat_id, self.inbound.group_name) + users = self.chat_repository.get_users_for_group(self.inbound) Replier.markdown(update, MessageBuilder.mention_message(users)) except NotFoundException as e: diff --git a/src/bot/message/inboundMessage.py b/src/bot/message/inboundMessage.py index c0f5505..685dfe8 100644 --- a/src/bot/message/inboundMessage.py +++ b/src/bot/message/inboundMessage.py @@ -3,9 +3,11 @@ from __future__ import annotations from dataclasses import dataclass import names +import re from telegram.ext.callbackcontext import CallbackContext from telegram.update import Update +from exception.invalidActionException import InvalidActionException from validator.accessValidator import AccessValidator from validator.groupNameValidator import GroupNameValidator @@ -24,14 +26,25 @@ class InboundMessage: user_id = str(update.effective_user.id) AccessValidator.validate(user_id) + message_content = update.edited_message.text if update.edited_message else update.message.text + message_content = message_content.replace("\n", " ") chat_id = str(update.effective_chat.id) group_name = InboundMessage.default_group + # done upon resolving a command action if context.args and context.args[0] and group_specific: group_name = str(context.args[0]).lower() GroupNameValidator.validate(group_name) + # done upon resolving a message handler action + if '@' in message_content: + searched_message_part = [part for part in message_content.split(' ') if '@' in part][0] + group_name = re.sub(r'\W+', '', searched_message_part).lower() + + if group_name in GroupNameValidator.FORBIDDEN_GROUP_NAMES: + group_name = InboundMessage.default_group + username = update.effective_user.username or update.effective_user.first_name if not username: diff --git a/src/config/contents.py b/src/config/contents.py index 952a2bc..ac1eac7 100755 --- a/src/config/contents.py +++ b/src/config/contents.py @@ -10,13 +10,16 @@ no_groups = 'There are no groups for this chat' start_text = """ Hello! @everyone_mention_bot here. - -Description: -I do not have access to your messages! I am here to help you with multiple user mentions. Usage: -Users that joined the group by /join command, can be mentioned after calling /everyone command. +Users that joined the group by /join command, +can be mentioned after typing one of those in your message: +@all, @channel, @chat, @everyone, @group or @here. + +If you did create a group named gaming, simply use @gaming to call users from that group. + +You can also use /everyone command. Commands:
/join {group-name}
@@ -36,6 +39,4 @@ Show start & help text Please note {group-name} is not required, default if not given. - -If your chat does have multiple bots I might not receive your command according to policy of bots with privacy mode enabled - use Inline Mode to avoid this. """ diff --git a/src/repository/chatRepository.py b/src/repository/chatRepository.py index c8786f3..83a7a70 100644 --- a/src/repository/chatRepository.py +++ b/src/repository/chatRepository.py @@ -39,12 +39,17 @@ class ChatRepository(AbstractRepository): return Chat.from_mongo_document(chat) - def get_users_for_group(self, chat_id: str, group: str) -> Iterable[User]: - chat = self.get(chat_id) - if not chat.groups.get(group): + def get_users_for_group(self, inbound: InboundMessage) -> Iterable[User]: + chat = self.get(inbound.chat_id) + if not chat.groups.get(inbound.group_name): raise NotFoundException - return [self.user_repository.get(user_id) for user_id in chat.groups.get(group)] + users = [self.user_repository.get(user_id) for user_id in chat.groups.get(inbound.group_name) if user_id != inbound.user_id] + + if not users: + raise NotFoundException + + return users def save(self, chat: Chat) -> None: self.database_client.save( diff --git a/src/validator/groupNameValidator.py b/src/validator/groupNameValidator.py index c910ae6..82ec899 100644 --- a/src/validator/groupNameValidator.py +++ b/src/validator/groupNameValidator.py @@ -5,6 +5,7 @@ from exception.invalidArgumentException import InvalidArgumentException class GroupNameValidator: MAX_GROUP_NAME_LENGTH: int = 40 + FORBIDDEN_GROUP_NAMES = ['all', 'channel', 'chat', 'everyone', 'group', 'here'] @staticmethod def validate(group: str) -> None: @@ -15,3 +16,6 @@ class GroupNameValidator: if len(group) > GroupNameValidator.MAX_GROUP_NAME_LENGTH: raise InvalidArgumentException(re.escape(f'Group name length can not be greater than {GroupNameValidator.MAX_GROUP_NAME_LENGTH}.')) + + if group in GroupNameValidator.FORBIDDEN_GROUP_NAMES: + raise InvalidArgumentException(re.escape(f'This group name is forbidden, please try with other name.'))