AbstractHandler wrap method addition, updated start text content and added buttons, changed MessageData to InboundMessage, better logging, code quality improvements, changed env files naming

This commit is contained in:
miloszowi 2021-10-11 17:20:39 +02:00
parent fb223556cb
commit ea2fddff40
19 changed files with 142 additions and 115 deletions

View File

@ -1,22 +1,16 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [UNRELEASED] - 07.10.2021 ## [UNRELEASED] - 11.10.2021
### Added ### Added
- Inline Query for join/leave/everyone - Inline Query for `join`, `leave` & `everyone`
- Group name validator - Banned users
- Banned users env - Buttons for `start` message
- Access validator
- ActionNotAllowedException
### Changed ### Changed
- code quality improvements - Code quality improvements
- `start` text
### Deleted ### Deleted
- `/silent` command - `/silent` command
### Updated
- start command content
## [0.1.0] - 06.10.2021 ## [0.1.0] - 06.10.2021
### Features ### Features
- `/join` command - `/join` command

View File

@ -34,8 +34,8 @@ git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git
- copy environment files and fulfill empty values - copy environment files and fulfill empty values
```bash ```bash
cp .env.local .env cp .env.local .env
cp docker/config/app.dist.env docker/config/app.env cp docker/config/app.env.local docker/config/app.env
cp docker/config/database.dist.env 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) - start the project (`-d` flag will run containers in detached mode)
```bash ```bash

View File

@ -3,7 +3,7 @@ from telegram.ext.dispatcher import Dispatcher
from bot.handler import * from bot.handler import *
from bot.handler.abstractHandler import AbstractHandler 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 from logger import Logger

View File

@ -4,10 +4,31 @@ from telegram.ext import Handler
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.update import Update 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: class AbstractHandler:
bot_handler: Handler bot_handler: Handler
inbound: InboundMessage
@abstractmethod @abstractmethod
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
raise Exception('handle method is not implemented') 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

View File

@ -3,9 +3,9 @@ from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from bot.message.replier import Replier from bot.message.replier import Replier
from config.contents import mention_failed from config.contents import mention_failed
from exception.notFoundException import NotFoundException
from logger import Logger from logger import Logger
from repository.userRepository import UserRepository from repository.userRepository import UserRepository
from utils.messageBuilder import MessageBuilder from utils.messageBuilder import MessageBuilder
@ -17,19 +17,14 @@ class EveryoneHandler(AbstractHandler):
action: str = 'everyone' action: str = 'everyone'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.handle) self.bot_handler = CommandHandler(self.action, self.wrap)
self.user_repository = UserRepository() self.user_repository = UserRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: try:
message_data = MessageData.create_from_arguments(update, context) users = self.user_repository.get_all_for_chat(self.inbound.chat_id)
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)) Replier.markdown(update, MessageBuilder.mention_message(users))
return Logger.action(message_data, self.action) Logger.action(self.inbound, self.action)
except NotFoundException:
Replier.markdown(update, mention_failed) Replier.markdown(update, mention_failed)

View File

@ -3,7 +3,6 @@ from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from bot.message.replier import Replier from bot.message.replier import Replier
from config.contents import no_groups from config.contents import no_groups
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
@ -18,19 +17,17 @@ class GroupsHandler(AbstractHandler):
action: str = 'groups' action: str = 'groups'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.handle) self.bot_handler = CommandHandler(self.action, self.wrap)
self.group_repository = GroupRepository() self.group_repository = GroupRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: try:
message_data = MessageData.create_from_arguments(update, context, False) groups = self.group_repository.get_by_chat_id(self.inbound.chat_id)
except Exception as e:
return Replier.markdown(update, str(e))
try:
groups = self.group_repository.get_by_chat_id(message_data.chat_id)
Replier.html(update, MessageBuilder.group_message(groups)) Replier.html(update, MessageBuilder.group_message(groups))
Logger.action(message_data, self.action) Logger.action(self.inbound, self.action)
except NotFoundException: except NotFoundException:
Replier.markdown(update, no_groups) Replier.markdown(update, no_groups)
def is_group_specific(self) -> bool:
return False

View File

@ -3,7 +3,6 @@ from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from bot.message.replier import Replier from bot.message.replier import Replier
from config.contents import joined, not_joined from config.contents import joined, not_joined
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
@ -17,25 +16,20 @@ class JoinHandler(AbstractHandler):
action: str = 'join' action: str = 'join'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.handle) self.bot_handler = CommandHandler(self.action, self.wrap)
self.user_repository = UserRepository() self.user_repository = UserRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: try:
message_data = MessageData.create_from_arguments(update, context) user = self.user_repository.get_by_id(self.inbound.user_id)
except Exception as e:
return Replier.markdown(update, str(e))
try: if user.is_in_chat(self.inbound.chat_id):
user = self.user_repository.get_by_id(message_data.user_id) return Replier.markdown(update, Replier.interpolate(not_joined, self.inbound))
if user.is_in_chat(message_data.chat_id): user.add_to_chat(self.inbound.chat_id)
return Replier.markdown(update, Replier.interpolate(not_joined, message_data))
user.add_to_chat(message_data.chat_id)
self.user_repository.save(user) self.user_repository.save(user)
except NotFoundException: 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)) Replier.markdown(update, Replier.interpolate(joined, self.inbound))
Logger.action(message_data, self.action) Logger.action(self.inbound, self.action)

View File

@ -3,7 +3,6 @@ from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from bot.message.replier import Replier from bot.message.replier import Replier
from config.contents import left, not_left from config.contents import left, not_left
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
@ -17,21 +16,16 @@ class LeaveHandler(AbstractHandler):
action: str = 'leave' action: str = 'leave'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.handle) self.bot_handler = CommandHandler(self.action, self.wrap)
self.user_repository = UserRepository() self.user_repository = UserRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: try:
message_data = MessageData.create_from_arguments(update, context) user = self.user_repository.get_by_id_and_chat_id(self.inbound.user_id, self.inbound.chat_id)
except Exception as e: user.remove_from_chat(self.inbound.chat_id)
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)
self.user_repository.save(user) self.user_repository.save(user)
Replier.markdown(update, Replier.interpolate(left, message_data)) Replier.markdown(update, Replier.interpolate(left, self.inbound))
Logger.action(message_data, self.action) Logger.action(self.inbound, self.action)
except NotFoundException: except NotFoundException:
return Replier.markdown(update, Replier.interpolate(not_left, message_data)) return Replier.markdown(update, Replier.interpolate(not_left, self.inbound))

View File

@ -1,9 +1,9 @@
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from bot.message.replier import Replier from bot.message.replier import Replier
from config.contents import start_text from config.contents import start_text
from logger import Logger from logger import Logger
@ -14,12 +14,23 @@ class StartHandler(AbstractHandler):
action: str = 'start' action: str = 'start'
def __init__(self) -> None: 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: def handle(self, update: Update, context: CallbackContext) -> None:
try: markup = InlineKeyboardMarkup(
MessageData.create_from_arguments(update, context) [
except Exception as e: [
return Replier.markdown(update, str(e)) InlineKeyboardButton('Inline Mode', switch_inline_query_current_chat='example'),
Replier.markdown(update, start_text) ],
Logger.action(MessageData.create_from_arguments(update, context), self.action) [
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

View File

@ -12,21 +12,21 @@ from validator.groupNameValidator import GroupNameValidator
@dataclass @dataclass
class MessageData: class InboundMessage:
user_id: str user_id: str
chat_id: str chat_id: str
group_name: str group_name: str
username: str username: str
@staticmethod @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) user_id = str(update.effective_user.id)
AccessValidator.validate(user_id) AccessValidator.validate(user_id)
chat_id = str(update.effective_chat.id) chat_id = str(update.effective_chat.id)
group_name = Group.default_name 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() group_name = str(context.args[0]).lower()
GroupNameValidator.validate(group_name) GroupNameValidator.validate(group_name)
@ -39,4 +39,4 @@ class MessageData:
if not username: if not username:
username = names.get_first_name() username = names.get_first_name()
return MessageData(user_id, chat_id, group_name, username) return InboundMessage(user_id, chat_id, group_name, username)

View File

@ -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 telegram.utils.helpers import mention_markdown
from bot.message.messageData import MessageData from bot.message.inboundMessage import InboundMessage
from logger import Logger from logger import Logger
class Replier: class Replier:
@staticmethod @staticmethod
def interpolate(content: str, message_data: MessageData): def interpolate(content: str, inbound_message: InboundMessage):
return content.format( return content.format(
mention_markdown(message_data.user_id, message_data.username), mention_markdown(inbound_message.user_id, inbound_message.username),
message_data.group_name inbound_message.group_name
) )
@staticmethod @staticmethod
def markdown(update: Update, message: str) -> None: def markdown(update: Update, message: str, reply_markup: Optional[InlineKeyboardMarkup] = None) -> None:
try: try:
update.effective_message.reply_markdown_v2(message) update.effective_message.reply_markdown_v2(message, reply_markup=reply_markup)
except Exception as err: except Exception as err:
Logger.error(str(err)) Logger.error(str(err))
@staticmethod @staticmethod
def html(update: Update, html: str) -> None: def html(update: Update, html: str, reply_markup: Optional[InlineKeyboardMarkup] = None) -> None:
try: 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: except Exception as err:
Logger.error(str(err)) Logger.error(str(err))

View File

@ -1,3 +1,4 @@
# markdownv2 python-telegram-bot specific
joined = '{} joined group `{}`' joined = '{} joined group `{}`'
not_joined = '{} is already in group `{}`' not_joined = '{} is already in group `{}`'
left = '{} left group `{}`' left = '{} left group `{}`'
@ -5,27 +6,38 @@ not_left = '{} did not join group `{}` before'
mention_failed = 'There are no users to mention' mention_failed = 'There are no users to mention'
no_groups = 'There are no groups for this chat' no_groups = 'There are no groups for this chat'
# html python-telegram-bot specific
start_text = """ start_text = """
Hello there Hello!
I am @everyone\_mention\_bot @everyone_mention_bot here.
I am here to help you with mass notifies I am here to help you with multiple user mentions.
Please take a look at available commands Using <code>Inline Mode</code> is recommended because <a href="https://core.telegram.org/bots/faq#what-messages-will-my-bot-get">policy of bots with privacy mode enabled</a> 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!
`<group-name>` is not required, if not given, it is set to `default`
To join group: Available commands:
`/join <group-name>` <b>Please note</b>
for example: `/join games` <code>{group-name}</code> is not required, <code>default</code> if not given.
To leave group: <b>Join</b>
`/leave <group-name>` Joins (or creates if group did not exist before) group.
<pre>/join {group-name}</pre>
To gather everyone attention use: <b>Leave</b>
`/everyone <group-name>` Leaves (or deletes if no other users are left) the group
<pre>/leave {group-name}</pre>
To see all available groups use: <b>Everyone</b>
`/groups` Mentions everyone that joined the group.
<pre>/everyone {group-name}</pre>
You can also try to tag me @everyone\_mention\_bot and then enter group name <b>Groups</b>
Possible results will be displayed Show all created groups in this chat.
<pre>/groups</pre>
<b>Start</b>
Show start & help text
<pre>/start</pre>
Reach out to <a href="https://t.me/miloszowi">Creator</a> in case of any issues/questions regarding my usage.
""" """

View File

@ -3,7 +3,7 @@ from urllib.parse import quote_plus
from pymongo import MongoClient from pymongo import MongoClient
from pymongo.database import Database from pymongo.database import Database
from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME, from config.envs import (MONGODB_DATABASE, MONGODB_HOSTNAME,
MONGODB_PASSWORD, MONGODB_PORT, MONGODB_PASSWORD, MONGODB_PORT,
MONGODB_USERNAME) MONGODB_USERNAME)

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
import os import os
from bot.message.messageData import MessageData from bot.message.inboundMessage import InboundMessage
# noinspection SpellCheckingInspection # noinspection SpellCheckingInspection
@ -41,17 +41,21 @@ class Logger:
Logger() Logger()
@staticmethod @staticmethod
def get_logger(logger_name) -> logging.Logger: def get(logger_name: str) -> logging.Logger:
return logging.getLogger(logger_name) return logging.getLogger(logger_name)
@staticmethod @staticmethod
def info(message: str) -> None: def info(message: str) -> None:
Logger.get_logger(Logger.action_logger).info(message) Logger.get(Logger.action_logger).info(message)
@staticmethod @staticmethod
def error(message: str) -> None: def error(message: str) -> None:
Logger.get_logger(Logger.main_logger).error(message) Logger.get(Logger.main_logger).error(message)
@staticmethod @staticmethod
def action(message_data: MessageData, action: str) -> None: def exception(exception: Exception) -> None:
Logger.info(f'User {message_data.username}({message_data.user_id}) called {action.upper()} for {message_data.chat_id}') 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}')

View File

@ -1,6 +1,6 @@
from typing import Iterable from typing import Iterable
from bot.message.messageData import MessageData from bot.message.inboundMessage import InboundMessage
from database.client import Client from database.client import Client
from entity.user import User from entity.user import User
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
@ -44,13 +44,13 @@ class UserRepository:
user.to_mongo_document() 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( self.client.insert_one(
User.collection, User.collection,
{ {
User.id_index: data.user_id, User.id_index: inbound_message.user_id,
User.username_index: data.username, User.username_index: inbound_message.username,
User.chats_index: [data.chat_id] User.chats_index: [inbound_message.chat_id]
} }
) )
@ -68,4 +68,7 @@ class UserRepository:
for record in users: for record in users:
result.append(User.from_mongo_document(record)) result.append(User.from_mongo_document(record))
if not result:
raise NotFoundException
return result return result

View File

@ -1,4 +1,4 @@
from config.credentials import BANNED_USERS from config.envs import BANNED_USERS
from exception.actionNotAllowedException import ActionNotAllowedException from exception.actionNotAllowedException import ActionNotAllowedException
@ -7,4 +7,4 @@ class AccessValidator:
@staticmethod @staticmethod
def validate(user_id: str) -> None: def validate(user_id: str) -> None:
if user_id in BANNED_USERS: if user_id in BANNED_USERS:
raise ActionNotAllowedException('You are banned') raise ActionNotAllowedException('User is banned')