mongodb new structure, added new collection chats

This commit is contained in:
miloszowi 2021-10-14 20:24:58 +02:00
parent 9c8f5795f8
commit e0916441b9
17 changed files with 194 additions and 193 deletions

View File

@ -7,6 +7,7 @@ from telegram.update import Update
from bot.message.inboundMessage import InboundMessage from bot.message.inboundMessage import InboundMessage
from bot.message.replier import Replier from bot.message.replier import Replier
from exception.actionNotAllowedException import ActionNotAllowedException from exception.actionNotAllowedException import ActionNotAllowedException
from exception.invalidActionException import InvalidActionException
from exception.invalidArgumentException import InvalidArgumentException from exception.invalidArgumentException import InvalidArgumentException
from logger import Logger from logger import Logger
@ -14,6 +15,7 @@ from logger import Logger
class AbstractHandler: class AbstractHandler:
bot_handler: Handler bot_handler: Handler
inbound: InboundMessage inbound: InboundMessage
action: str
@abstractmethod @abstractmethod
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
@ -25,7 +27,8 @@ class AbstractHandler:
self.inbound = InboundMessage.create(update, context, group_specific) self.inbound = InboundMessage.create(update, context, group_specific)
self.handle(update, context) self.handle(update, context)
except (ActionNotAllowedException, InvalidArgumentException) as e: Logger.action(self.inbound, self.action)
except (InvalidActionException, InvalidArgumentException, ActionNotAllowedException) as e:
Replier.markdown(update, str(e)) Replier.markdown(update, str(e))
except Exception as e: except Exception as e:
Logger.exception(e) Logger.exception(e)

View File

@ -5,26 +5,28 @@ from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
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.invalidActionException import InvalidActionException
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
from logger import Logger from repository.chatRepository import ChatRepository
from repository.userRepository import UserRepository from repository.userRepository import UserRepository
from utils.messageBuilder import MessageBuilder from utils.messageBuilder import MessageBuilder
class EveryoneHandler(AbstractHandler): class EveryoneHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
chat_repository: ChatRepository
user_repository: UserRepository user_repository: UserRepository
action: str = 'everyone' action: str = 'everyone'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler(self.action, self.wrap)
self.chat_repository = ChatRepository()
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:
users = self.user_repository.get_all_for_chat(self.inbound.chat_id) users = self.chat_repository.get_users_for_group(self.inbound.chat_id, self.inbound.group_name)
Replier.markdown(update, MessageBuilder.mention_message(users)) Replier.markdown(update, MessageBuilder.mention_message(users))
Logger.action(self.inbound, self.action) except NotFoundException as e:
except NotFoundException: raise InvalidActionException(mention_failed) from e
Replier.markdown(update, mention_failed)

View File

@ -5,29 +5,30 @@ from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
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.invalidActionException import InvalidActionException
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
from logger import Logger from repository.chatRepository import ChatRepository
from repository.groupRepository import GroupRepository
from utils.messageBuilder import MessageBuilder from utils.messageBuilder import MessageBuilder
class GroupsHandler(AbstractHandler): class GroupsHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
group_repository: GroupRepository chat_repository: ChatRepository
action: str = 'groups' action: str = 'groups'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler(self.action, self.wrap)
self.group_repository = GroupRepository() self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: try:
groups = self.group_repository.get_by_chat_id(self.inbound.chat_id) chat = self.chat_repository.get(self.inbound.chat_id)
Replier.html(update, MessageBuilder.group_message(groups)) if not chat.groups:
raise NotFoundException
Logger.action(self.inbound, self.action) Replier.html(update, MessageBuilder.group_message(chat.groups))
except NotFoundException: except NotFoundException:
Replier.markdown(update, no_groups) raise InvalidActionException(no_groups)
def is_group_specific(self) -> bool: def is_group_specific(self) -> bool:
return False return False

View File

@ -6,7 +6,7 @@ from telegram.inline.inputtextmessagecontent import InputTextMessageContent
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from entity.group import Group from bot.message.inboundMessage import InboundMessage
from exception.actionNotAllowedException import ActionNotAllowedException from exception.actionNotAllowedException import ActionNotAllowedException
from validator.accessValidator import AccessValidator from validator.accessValidator import AccessValidator
@ -24,8 +24,8 @@ class InlineQueryHandler(AbstractHandler):
update.inline_query.answer([]) update.inline_query.answer([])
return return
group_display = update.inline_query.query or Group.default_name group_display = update.inline_query.query or InboundMessage.default_group
group = '' if group_display == Group.default_name else group_display group = '' if group_display == InboundMessage.default_group else group_display
results = [ results = [
InlineQueryResultArticle( InlineQueryResultArticle(

View File

@ -5,8 +5,8 @@ from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
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.invalidActionException import InvalidActionException
from logger import Logger from repository.chatRepository import ChatRepository
from repository.userRepository import UserRepository from repository.userRepository import UserRepository
@ -18,18 +18,17 @@ class JoinHandler(AbstractHandler):
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler(self.action, self.wrap)
self.user_repository = UserRepository() self.user_repository = UserRepository()
self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: user = self.user_repository.provide(self.inbound)
user = self.user_repository.get_by_id(self.inbound.user_id) chat = self.chat_repository.provide(self.inbound)
users = chat.groups.get(self.inbound.group_name)
if user.is_in_chat(self.inbound.chat_id): if user.user_id in users:
return Replier.markdown(update, Replier.interpolate(not_joined, self.inbound)) raise InvalidActionException(Replier.interpolate(not_joined, self.inbound))
user.add_to_chat(self.inbound.chat_id) users.append(user.user_id)
self.user_repository.save(user) self.chat_repository.save(chat)
except NotFoundException:
self.user_repository.save_by_inbound_message(self.inbound)
Replier.markdown(update, Replier.interpolate(joined, self.inbound)) Replier.markdown(update, Replier.interpolate(joined, self.inbound))
Logger.action(self.inbound, self.action)

View File

@ -5,27 +5,34 @@ from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
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.invalidActionException import InvalidActionException
from logger import Logger
from repository.userRepository import UserRepository from repository.userRepository import UserRepository
from repository.chatRepository import ChatRepository
class LeaveHandler(AbstractHandler): class LeaveHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
user_repository: UserRepository user_repository: UserRepository
chat_repository: ChatRepository
action: str = 'leave' action: str = 'leave'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler(self.action, self.wrap)
self.user_repository = UserRepository() self.user_repository = UserRepository()
self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: user = self.user_repository.provide(self.inbound)
user = self.user_repository.get_by_id_and_chat_id(self.inbound.user_id, self.inbound.chat_id) chat = self.chat_repository.provide(self.inbound)
user.remove_from_chat(self.inbound.chat_id) group = chat.groups.get(self.inbound.group_name)
self.user_repository.save(user)
Replier.markdown(update, Replier.interpolate(left, self.inbound)) if user.user_id not in group:
Logger.action(self.inbound, self.action) raise InvalidActionException(Replier.interpolate(not_left, self.inbound))
except NotFoundException:
return Replier.markdown(update, Replier.interpolate(not_left, self.inbound)) group.remove(user.user_id)
if not group:
chat.groups.pop(self.inbound.group_name)
self.chat_repository.save(chat)
Replier.markdown(update, Replier.interpolate(left, self.inbound))

View File

@ -6,7 +6,6 @@ import names
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.accessValidator import AccessValidator
from validator.groupNameValidator import GroupNameValidator from validator.groupNameValidator import GroupNameValidator
@ -18,22 +17,21 @@ class InboundMessage:
group_name: str group_name: str
username: str username: str
default_group: str = 'default'
@staticmethod @staticmethod
def create(update: Update, context: CallbackContext, group_specific: bool) -> InboundMessage: 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 = InboundMessage.default_group
if context.args and context.args[0] and group_specific: 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)
if group_name is not Group.default_name:
chat_id += f'~{group_name}'
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

@ -30,10 +30,11 @@ class Client(metaclass=Singleton):
def find_many(self, collection: str, filter: dict) -> dict: def find_many(self, collection: str, filter: dict) -> dict:
return self.database.get_collection(collection).find(filter) return self.database.get_collection(collection).find(filter)
def update_one(self, collection: str, filter: dict, data: dict) -> None: def save(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},
upsert=True
) )
def aggregate(self, collection, pipeline: list): def aggregate(self, collection, pipeline: list):

32
src/entity/chat.py Normal file
View File

@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from bot.message.inboundMessage import InboundMessage
@dataclass
class Chat:
chat_id: str
groups: dict
mongo_chat_id_index: str = '_id'
mongo_groups_index: str = 'groups'
def to_mongo_document(self) -> dict:
return {
self.mongo_chat_id_index: self.chat_id,
self.mongo_groups_index: self.groups
}
@staticmethod
def from_mongo_document(mongo_document: dict) -> Chat:
return Chat(
mongo_document[Chat.mongo_chat_id_index],
mongo_document[Chat.mongo_groups_index]
)
@staticmethod
def from_inbound_message(inbound: InboundMessage) -> Chat:
return Chat(inbound.chat_id, {inbound.group_name: []})

View File

@ -1,12 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class Group:
chat_id: str
group_name: str
users_count: int
default_name: str = 'default'

View File

@ -1,40 +1,31 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import List
from bot.message.inboundMessage import InboundMessage
@dataclass @dataclass
class User: class User:
user_id: str user_id: str
username: str username: str
chats: List[str]
collection: str = 'users' mongo_user_id_index: str = '_id'
id_index: str = '_id' mongo_username_index: str = 'username'
chats_index: str = 'chats'
username_index: str = 'username'
def is_in_chat(self, chat_id: str) -> bool:
return chat_id in self.chats
def add_to_chat(self, chat_id: str) -> None:
self.chats.append(chat_id)
def remove_from_chat(self, chat_id: str) -> None:
if chat_id in self.chats:
self.chats.remove(chat_id)
def to_mongo_document(self) -> dict: def to_mongo_document(self) -> dict:
return { return {
self.username_index: self.username, self.mongo_user_id_index: self.user_id,
self.chats_index: self.chats self.mongo_username_index: self.username
} }
@staticmethod @staticmethod
def from_mongo_document(mongo_document: dict) -> User: def from_mongo_document(mongo_document: dict) -> User:
return User( return User(
mongo_document[User.id_index], mongo_document[User.mongo_user_id_index],
mongo_document[User.username_index], mongo_document[User.mongo_username_index]
mongo_document[User.chats_index]
) )
@staticmethod
def from_inbound_message(inbound: InboundMessage) -> User:
return User(inbound.user_id, inbound.username)

View File

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

View File

@ -0,0 +1,9 @@
from database.client import Client
class AbstractRepository:
collection_name: str
database_client: Client
def __init__(self):
self.database_client = Client()

View File

@ -0,0 +1,55 @@
from typing import Iterable
from bot.message.inboundMessage import InboundMessage
from entity.chat import Chat
from entity.user import User
from exception.notFoundException import NotFoundException
from repository.abstractRepository import AbstractRepository
from repository.userRepository import UserRepository
class ChatRepository(AbstractRepository):
collection_name: str = 'chats'
user_repository: UserRepository
def __init__(self):
super().__init__()
self.user_repository = UserRepository()
def provide(self, inbound: InboundMessage) -> Chat:
try:
chat = self.get(inbound.chat_id)
if not chat.groups.get(inbound.group_name):
chat.groups[inbound.group_name] = []
except NotFoundException:
chat = Chat.from_inbound_message(inbound)
return chat
def get(self, chat_id: str) -> Chat:
chat = self.database_client.find_one(
self.collection_name,
{
Chat.mongo_chat_id_index: chat_id
}
)
if not chat:
raise NotFoundException
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):
raise NotFoundException
return [self.user_repository.get(user_id) for user_id in chat.groups.get(group)]
def save(self, chat: Chat) -> None:
self.database_client.save(
self.collection_name,
{Chat.mongo_chat_id_index: chat.chat_id},
chat.to_mongo_document()
)

View File

@ -1,56 +0,0 @@
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:
client: Client
count: str = 'count'
def __init__(self) -> None:
self.client = Client()
def get_by_chat_id(self, chat_id: str) -> Iterable[Group]:
groups = self.client.aggregate(
User.collection,
[
{"$unwind": f'${User.chats_index}'},
{
"$match": {
User.chats_index: {"$regex": re.compile(f'^{chat_id}.*$')},
},
},
{
"$group": {
"_id": {
"$last": {"$split": [f'${User.chats_index}', "~"]},
},
self.count: {"$count": {}},
},
},
{
"$sort": {'_id': 1}
}
]
)
result = []
for group in groups:
group_name = group['_id']
if group_name == chat_id:
group_name = Group.default_name
result.append(
Group(chat_id, group_name, group[self.count])
)
if not result:
raise NotFoundException
return result

View File

@ -1,74 +1,43 @@
from typing import Iterable
from bot.message.inboundMessage import InboundMessage from bot.message.inboundMessage import InboundMessage
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
from repository.abstractRepository import AbstractRepository
class UserRepository: class UserRepository(AbstractRepository):
client: Client collection_name: str = 'users'
def __init__(self) -> None: def __init__(self):
self.client = Client() super().__init__()
def get_by_id(self, user_id: str) -> User: def provide(self, inbound: InboundMessage) -> User:
user = self.client.find_one( user = User.from_inbound_message(inbound)
User.collection,
try:
entity = self.get(user.user_id)
if entity != user:
self.save(user)
except NotFoundException:
self.save(user)
return user
def get(self, user_id: str) -> User:
user = self.database_client.find_one(
self.collection_name,
{ {
User.id_index: user_id User.mongo_user_id_index: user_id
} }
) )
if not user: if not user:
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 raise NotFoundException
return user return User.from_mongo_document(user)
def save(self, user: User) -> None: def save(self, user: User) -> None:
self.client.update_one( self.database_client.save(
User.collection, self.collection_name,
{User.id_index: user.user_id}, {User.mongo_user_id_index: user.user_id},
user.to_mongo_document() user.to_mongo_document()
) )
def save_by_inbound_message(self, inbound_message: InboundMessage) -> None:
self.client.insert_one(
User.collection,
{
User.id_index: inbound_message.user_id,
User.username_index: inbound_message.username,
User.chats_index: [inbound_message.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]
}
}
)
for record in users:
result.append(User.from_mongo_document(record))
if not result:
raise NotFoundException
return result

View File

@ -3,16 +3,16 @@ from typing import Iterable
from prettytable import prettytable from prettytable import prettytable
from telegram.utils.helpers import mention_markdown from telegram.utils.helpers import mention_markdown
from entity.group import Group
from entity.user import User from entity.user import User
class MessageBuilder: class MessageBuilder:
@staticmethod @staticmethod
def group_message(groups: Iterable[Group]) -> str: def group_message(groups: dict) -> str:
table = prettytable.PrettyTable(['Name', 'Members']) table = prettytable.PrettyTable(['Name', 'Members'])
table.add_rows([[record.group_name, record.users_count] for record in groups]) for group in groups:
table.add_row([group, len(groups[group])])
return f'<pre>{str(table)}</pre>' return f'<pre>{str(table)}</pre>'