Banned users env, access validator, removed silent command, code quality improvements

This commit is contained in:
miloszowi 2021-10-08 15:25:47 +02:00
parent d05d0c0904
commit 431b004284
28 changed files with 268 additions and 261 deletions

View File

@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file.
## [UNRELEASED] - 07.10.2021 ## [UNRELEASED] - 07.10.2021
### Added ### Added
- Inline Query for join/leave/everyone - 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 ### Updated
- start command content - start command content

View File

@ -15,7 +15,6 @@
* [`/leave`](#leave) * [`/leave`](#leave)
* [`/everyone`](#everyone) * [`/everyone`](#everyone)
* [`/groups`](#groups) * [`/groups`](#groups)
* [`/silent`](#silent)
* [`/start`](#start) * [`/start`](#start)
## Description ## Description
Everyone Mention Bot is simple, but useful telegram bot to gather group members attention. Everyone Mention Bot is simple, but useful telegram bot to gather group members attention.
@ -62,6 +61,7 @@ docker/logs <container>
- `MONGODB_PASSWORD` - MongoDB password - `MONGODB_PASSWORD` - MongoDB password
- `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name) - `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name)
- `MONGODB_PORT` - MongoDB port (default `27017` - given in docker-compose configuration) - `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` - `database.env`
- `MONGO_INITDB_ROOT_USERNAME` - conf from `app.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. Will display available groups for this chat as well with members count.
![groups command example](docs/groups.png) ![groups command example](docs/groups.png)
### `/silent`
```
/silent <group_name>
```
Will display all every member of given group (`default` if not given) but without notyfing them.
![silent command example](docs/silent.png)
### `/start` ### `/start`
Start & Help message Start & Help message

View File

@ -6,4 +6,6 @@ MONGODB_DATABASE=
MONGODB_USERNAME= MONGODB_USERNAME=
MONGODB_PASSWORD= MONGODB_PASSWORD=
MONGODB_HOSTNAME=localhost MONGODB_HOSTNAME=localhost
MONGODB_PORT=27017 MONGODB_PORT=27017
BANNED_USERS=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,27 +1,21 @@
from logging import Logger
import logging
from telegram.ext import Updater from telegram.ext import Updater
from telegram.ext.dispatcher import Dispatcher from telegram.ext.dispatcher import Dispatcher
from logger import Logger from bot.handler import *
from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
from bot.handler import (groupsHandler, joinHandler, mentionHandler, leaveHandler,
silentMentionHandler, startHandler, inlineQueryHandler)
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
from logger import Logger
class App: class App:
updater: Updater updater: Updater
dispatcher: Dispatcher dispatcher: Dispatcher
log_file: str = '/var/log/bot.log'
log_format: str = '%(levelname)s-%(asctime)s: %(message)s'
def __init__(self): def __init__(self):
self.updater = Updater(BOT_TOKEN) self.updater = Updater(BOT_TOKEN)
def run(self) -> None: def run(self) -> None:
self.setup_logging() Logger.register()
self.register_handlers() self.register_handlers()
self.register_webhook() self.register_webhook()
@ -29,9 +23,7 @@ class App:
def register_handlers(self) -> None: def register_handlers(self) -> None:
for handler in AbstractHandler.__subclasses__(): for handler in AbstractHandler.__subclasses__():
self.updater.dispatcher.add_handler( self.updater.dispatcher.add_handler(handler().bot_handler)
handler().get_bot_handler()
)
def register_webhook(self) -> None: def register_webhook(self) -> None:
self.updater.start_webhook( self.updater.start_webhook(
@ -41,15 +33,9 @@ class App:
webhook_url="/".join([WEBHOOK_URL, BOT_TOKEN]) webhook_url="/".join([WEBHOOK_URL, BOT_TOKEN])
) )
Logger.get_logger(Logger.action_logger).info( Logger.info(f'Webhook configured, listening on {WEBHOOK_URL}/<bot-token>')
f'Webhook configured, listening on {WEBHOOK_URL}/<bot-token>'
)
def setup_logging(self) -> None:
logger = Logger()
logger.setup()
if __name__ == "__main__": if __name__ == "__main__":
app = App() app = App()
app.run() app.run()

View File

@ -0,0 +1,5 @@
__all__ = [
'abstractHandler', 'everyoneHandler', 'groupsHandler',
'inlineQueryHandler', 'joinHandler', 'leaveHandler',
'startHandler'
]

View File

@ -1,37 +1,13 @@
from abc import abstractmethod from abc import abstractmethod
from bot.message.messageData import MessageData from telegram.ext import Handler
from logger import Logger
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.handler import Handler
from telegram.update import Update from telegram.update import Update
from telegram.utils.helpers import mention_markdown
class AbstractHandler: class AbstractHandler:
@abstractmethod bot_handler: Handler
def get_bot_handler(self) -> Handler: raise Exception('get_bot_handler method is not implemented')
@abstractmethod @abstractmethod
def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented') 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))

View File

@ -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)

View File

@ -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.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.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): class GroupsHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
group_repository: GroupRepository group_repository: GroupRepository
action: str = 'groups'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler('groups', self.handle) self.bot_handler = CommandHandler(self.action, self.handle)
self.group_repository = GroupRepository() self.group_repository = GroupRepository()
def handle(self, update: Update, context: CallbackContext) -> None: 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: Logger.action(message_data, self.action)
self.reply_html(update, self.build_groups_message(groups)) except NotFoundException:
return self.log_action(message_data) Replier.markdown(update, no_groups)
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'<pre>{str(resultTable)}</pre>'

View File

@ -1,21 +1,29 @@
from bot.handler.abstractHandler import AbstractHandler
from entity.group import Group
from telegram import InlineQueryResultArticle from telegram import InlineQueryResultArticle
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler
from telegram.ext.inlinequeryhandler import \ from telegram.ext.inlinequeryhandler import \
InlineQueryHandler as CoreInlineQueryHandler InlineQueryHandler as CoreInlineQueryHandler
from telegram.inline.inputtextmessagecontent import InputTextMessageContent from telegram.inline.inputtextmessagecontent import InputTextMessageContent
from telegram.update import Update 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): class InlineQueryHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CoreInlineQueryHandler
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CoreInlineQueryHandler(self.handle) self.bot_handler = CoreInlineQueryHandler(self.handle)
def handle(self, update: Update, context: CallbackContext) -> None: 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_display = update.inline_query.query or Group.default_name
group = '' if group_display == Group.default_name else group_display 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) update.inline_query.answer(results, cache_time=4800)
def get_bot_handler(self) -> CoreInlineQueryHandler:
return self.bot_handler

View File

@ -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.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.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): class JoinHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
user_repository: UserRepository user_repository: UserRepository
action: str = 'join'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler('join', self.handle) self.bot_handler = CommandHandler(self.action, self.handle)
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) message_data = MessageData.create_from_arguments(update, context)
except InvalidArgumentException as e: except Exception as e:
return self.reply_markdown(update, str(e)) return Replier.markdown(update, str(e))
try: try:
user = self.user_repository.get_by_id(message_data.user_id) user = self.user_repository.get_by_id(message_data.user_id)
if user.is_in_chat(message_data.chat_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) 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_message_data(message_data)
self.reply_markdown(update, self.interpolate_reply(joined, message_data)) Replier.markdown(update, Replier.interpolate(joined, message_data))
self.log_action(message_data) Logger.action(message_data, self.action)
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}')

View File

@ -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.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.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): class LeaveHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
user_repository: UserRepository user_repository: UserRepository
action: str = 'leave'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler('leave', self.handle) self.bot_handler = CommandHandler(self.action, self.handle)
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) message_data = MessageData.create_from_arguments(update, context)
except InvalidArgumentException as e: except Exception as e:
return self.reply_markdown(update, str(e)) return Replier.markdown(update, str(e))
try: 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): Replier.markdown(update, Replier.interpolate(left, message_data))
raise NotFoundException() Logger.action(message_data, self.action)
except NotFoundException: except NotFoundException:
return self.reply_markdown(update, self.interpolate_reply(not_left, message_data)) return Replier.markdown(update, Replier.interpolate(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}')

View File

@ -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])

View File

@ -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}')

View File

@ -1,25 +1,25 @@
from config.contents import start_text
from logger import Logger
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.messageData import MessageData
from bot.message.replier import Replier
from config.contents import start_text
from logger import Logger
class StartHandler(AbstractHandler): class StartHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
action: str = 'start'
def __init__(self) -> None: 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: def handle(self, update: Update, context: CallbackContext) -> None:
self.reply_markdown(update, start_text) try:
self.log_action(MessageData.create_from_arguments(update, context)) MessageData.create_from_arguments(update, context)
except Exception as e:
def get_bot_handler(self) -> CommandHandler: return Replier.markdown(update, str(e))
return self.bot_handler Replier.markdown(update, start_text)
Logger.action(MessageData.create_from_arguments(update, context), self.action)
def log_action(self, message_data: MessageData) -> None:
Logger.info(f'User {message_data.username} called /start for {message_data.chat_id}')

View File

@ -1,19 +1,18 @@
from __future__ import annotations from __future__ import annotations
import re
from dataclasses import dataclass from dataclasses import dataclass
import names import names
from entity.group import Group
from exception.invalidArgumentException import InvalidArgumentException
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.update import Update from telegram.update import Update
from entity.group import Group
from validator.accessValidator import AccessValidator
from validator.groupNameValidator import GroupNameValidator from validator.groupNameValidator import GroupNameValidator
@dataclass @dataclass
class MessageData(): class MessageData:
user_id: str user_id: str
chat_id: str chat_id: str
group_name: str group_name: str
@ -21,6 +20,9 @@ class MessageData():
@staticmethod @staticmethod
def create_from_arguments(update: Update, context: CallbackContext, include_group: bool = True) -> MessageData: 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) chat_id = str(update.effective_chat.id)
group_name = Group.default_name group_name = Group.default_name
@ -31,9 +33,7 @@ class MessageData():
if group_name is not Group.default_name: if group_name is not Group.default_name:
chat_id += f'~{group_name}' chat_id += f'~{group_name}'
user_id = str(update.effective_user.id)
username = update.effective_user.username or update.effective_user.first_name username = update.effective_user.username or update.effective_user.first_name
if not username: if not username:

View File

@ -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))

View File

@ -5,7 +5,6 @@ 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'
start_text = """ start_text = """
Hello there Hello there
I am @everyone\_mention\_bot 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 You can also try to tag me @everyone\_mention\_bot and then enter group name
Possible results will be displayed Possible results will be displayed
""" """

View File

@ -13,3 +13,5 @@ MONGODB_USERNAME = os.environ['MONGODB_USERNAME']
MONGODB_PASSWORD = os.environ['MONGODB_PASSWORD'] MONGODB_PASSWORD = os.environ['MONGODB_PASSWORD']
MONGODB_HOSTNAME = os.environ['MONGODB_HOSTNAME'] MONGODB_HOSTNAME = os.environ['MONGODB_HOSTNAME']
MONGODB_PORT = os.environ['MONGODB_PORT'] MONGODB_PORT = os.environ['MONGODB_PORT']
BANNED_USERS = os.environ['BANNED_USERS'].split(',') or []

View File

@ -1,13 +1,14 @@
from urllib.parse import quote_plus from urllib.parse import quote_plus
from pymongo import MongoClient
from pymongo.database import Database
from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME, from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME,
MONGODB_PASSWORD, MONGODB_PORT, MONGODB_PASSWORD, MONGODB_PORT,
MONGODB_USERNAME) MONGODB_USERNAME)
from pymongo import MongoClient
from pymongo.database import Database
class Client(): class Client:
mongo_client: MongoClient mongo_client: MongoClient
database: Database database: Database
@ -32,7 +33,7 @@ class Client():
def update_one(self, collection: str, filter: dict, data: dict) -> None: def update_one(self, collection: str, filter: dict, data: dict) -> None:
self.database.get_collection(collection).update_one( self.database.get_collection(collection).update_one(
filter, filter,
{ "$set" : data } {"$set": data}
) )
def aggregate(self, collection, pipeline: list): def aggregate(self, collection, pipeline: list):

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
@dataclass @dataclass
class Group(): class Group:
chat_id: str chat_id: str
group_name: str group_name: str
users_count: int users_count: int

View File

@ -1,14 +1,14 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterable from typing import List
@dataclass @dataclass
class User(): class User:
user_id: str user_id: str
username: str username: str
chats: Iterable[str] chats: List[str]
collection: str = 'users' collection: str = 'users'
id_index: str = '_id' id_index: str = '_id'

View File

@ -0,0 +1,2 @@
class ActionNotAllowedException(Exception):
pass

View File

@ -3,7 +3,10 @@ from __future__ import annotations
import logging import logging
import os import os
from bot.message.messageData import MessageData
# noinspection SpellCheckingInspection
class Logger: class Logger:
action_logger: str = 'action-logger' action_logger: str = 'action-logger'
action_logger_file: str = '/var/log/bot/action.log' action_logger_file: str = '/var/log/bot/action.log'
@ -11,9 +14,10 @@ class Logger:
main_logger: str = 'main-logger' main_logger: str = 'main-logger'
main_logger_file: str = '/var/log/bot/app.log' 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.action_logger, self.action_logger_file, logging.INFO)
self.configure(self.main_logger, self.main_logger_file, logging.ERROR) self.configure(self.main_logger, self.main_logger_file, logging.ERROR)
@ -32,12 +36,22 @@ class Logger:
logger.addHandler(file_handler) logger.addHandler(file_handler)
logger.addHandler(stream_handler) logger.addHandler(stream_handler)
@staticmethod
def register() -> None:
Logger()
@staticmethod @staticmethod
def get_logger(logger_name) -> logging.Logger: def get_logger(logger_name) -> logging.Logger:
return logging.getLogger(logger_name) return logging.getLogger(logger_name)
@staticmethod
def info(message: str) -> None: def info(message: str) -> None:
Logger.get_logger(Logger.action_logger).info(message) Logger.get_logger(Logger.action_logger).info(message)
@staticmethod
def error(message: str) -> None: def error(message: str) -> None:
Logger.get_logger(Logger.main_logger).error(message) 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}')

View File

@ -1,13 +1,13 @@
import itertools
import re import re
from typing import Iterable from typing import Iterable
from database.client import Client from database.client import Client
from entity.group import Group from entity.group import Group
from entity.user import User from entity.user import User
from exception.notFoundException import NotFoundException
class GroupRepository(): class GroupRepository:
client: Client client: Client
count: str = 'count' count: str = 'count'
@ -19,22 +19,22 @@ class GroupRepository():
groups = self.client.aggregate( groups = self.client.aggregate(
User.collection, User.collection,
[ [
{ "$unwind": f'${User.chats_index}' }, {"$unwind": f'${User.chats_index}'},
{ {
"$match": { "$match": {
User.chats_index: { "$regex": re.compile(f'^{chat_id}.*$') }, User.chats_index: {"$regex": re.compile(f'^{chat_id}.*$')},
}, },
}, },
{ {
"$group": { "$group": {
"_id": { "_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]) Group(chat_id, group_name, group[self.count])
) )
if not groups:
raise NotFoundException
return result return result

View File

@ -1,4 +1,4 @@
from typing import Iterable, Optional from typing import Iterable
from bot.message.messageData import MessageData from bot.message.messageData import MessageData
from database.client import Client from database.client import Client
@ -6,57 +6,65 @@ from entity.user import User
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
class UserRepository(): class UserRepository:
client: Client client: Client
def __init__(self) -> None: def __init__(self) -> None:
self.client = Client() 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 = self.client.find_one(
User.collection, User.collection,
{ {
User.id_index: id User.id_index: user_id
} }
) )
if not user: 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( return User(
user[User.id_index], user[User.id_index],
user[User.username_index], user[User.username_index],
user[User.chats_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: def save(self, user: User) -> None:
self.client.update_one( self.client.update_one(
User.collection, User.collection,
{ User.id_index: user.user_id }, {User.id_index: user.user_id},
user.to_mongo_document() user.to_mongo_document()
) )
def save_by_message_data(self, data: MessageData) -> None: def save_by_message_data(self, data: MessageData) -> None:
self.client.insert_one( self.client.insert_one(
User.collection, User.collection,
{ {
User.id_index: data.user_id, User.id_index: data.user_id,
User.username_index: data.username, User.username_index: data.username,
User.chats_index: [data.chat_id] User.chats_index: [data.chat_id]
} }
) )
def get_all_for_chat(self, chat_id: str) -> Iterable[User]: def get_all_for_chat(self, chat_id: str) -> Iterable[User]:
result = [] result = []
users = self.client.find_many( users = self.client.find_many(
User.collection, User.collection,
{ {
User.chats_index: { User.chats_index: {
"$in" : [chat_id] "$in": [chat_id]
} }
} }
) )
for record in users: for record in users:
result.append(User.from_mongo_document(record)) result.append(User.from_mongo_document(record))

View File

@ -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'<pre>{str(table)}</pre>'
@staticmethod
def mention_message(users: Iterable[User]) -> str:
return ' '.join([mention_markdown(user.user_id, user.username) for user in users])

View File

@ -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')