From cd8e3507caaf3041f27bd344ac51f65b5097847b Mon Sep 17 00:00:00 2001
From: miloszowi <miloszweb@gmail.com>
Date: Sat, 25 Sep 2021 16:49:11 +0200
Subject: [PATCH] changed mongoDb collection structure, removed group entity &
 repository, updated README.md, changed folder names to singular forms

---
 README.md                                     | 60 +++++++++--------
 docker-compose.yml                            |  4 +-
 docker/config/{app => }/app.dist.env          |  0
 .../config/{database => }/database.dist.env   |  0
 src/app.py                                    | 23 +++----
 src/config/contents.py                        |  4 +-
 src/config/credentials.py                     |  3 +-
 src/config/handlers.py                        |  9 ---
 src/database/{databaseClient.py => client.py} | 28 ++++----
 src/entities/chat.py                          | 28 --------
 src/entities/chatPerson.py                    | 34 ----------
 src/entities/person.py                        | 44 -------------
 src/entity/user.py                            | 52 +++++++++++++++
 src/exception/alreadyExistsException.py       |  2 +
 src/exception/notFoundException.py            |  2 +
 .../abstractHandler.py}                       |  9 ++-
 src/handler/inHandler.py                      | 38 +++++++++++
 src/handler/mentionHandler.py                 | 40 +++++++++++
 src/handler/outHandler.py                     | 36 ++++++++++
 src/handler/vo/updateData.py                  | 36 ++++++++++
 src/handlers/inHandler.py                     | 38 -----------
 src/handlers/mentionHandler.py                | 45 -------------
 src/handlers/outHandler.py                    | 38 -----------
 src/repositories/chatRepository.py            | 18 -----
 src/repositories/personRepository.py          | 27 --------
 src/repositories/relationRepository.py        | 56 ----------------
 src/repository/userRepository.py              | 66 +++++++++++++++++++
 27 files changed, 347 insertions(+), 393 deletions(-)
 rename docker/config/{app => }/app.dist.env (100%)
 rename docker/config/{database => }/database.dist.env (100%)
 delete mode 100755 src/config/handlers.py
 rename src/database/{databaseClient.py => client.py} (53%)
 delete mode 100755 src/entities/chat.py
 delete mode 100755 src/entities/chatPerson.py
 delete mode 100755 src/entities/person.py
 create mode 100644 src/entity/user.py
 create mode 100644 src/exception/alreadyExistsException.py
 create mode 100644 src/exception/notFoundException.py
 rename src/{handlers/handlerInterface.py => handler/abstractHandler.py} (76%)
 create mode 100755 src/handler/inHandler.py
 create mode 100755 src/handler/mentionHandler.py
 create mode 100755 src/handler/outHandler.py
 create mode 100644 src/handler/vo/updateData.py
 delete mode 100755 src/handlers/inHandler.py
 delete mode 100755 src/handlers/mentionHandler.py
 delete mode 100755 src/handlers/outHandler.py
 delete mode 100755 src/repositories/chatRepository.py
 delete mode 100755 src/repositories/personRepository.py
 delete mode 100755 src/repositories/relationRepository.py
 create mode 100644 src/repository/userRepository.py

diff --git a/README.md b/README.md
index f578ee3..5b2ff7d 100755
--- a/README.md
+++ b/README.md
@@ -3,53 +3,61 @@
 <p align="center"> simple, but useful telegram bot to gather all of group members attention!
 <!-- Icon made by https://www.freepik.com from https://www.flaticon.com/ -->
 
-## Contents
+# Contents
 
 * [Getting started.](#getting-started)
-    * [Installation](#installation)
     * [Requirements](#requirements)
-    * [Env file](#env-file)
+    * [Installation](#installation)
+    * [Logs](#logs)
+    * [Env files](#env-files)
 * [Commands](#commands)
     * [`/in`](#in)
     * [`/out`](#out)
     * [`/everyone`](#everyone)
 
-### Getting started
-#### Installation
+## Getting started
+
+### Requirements
+- `docker-compose` in version `1.25.0`
+- `docker` in version `20.10.7`
+
+### Installation
 ```bash
 git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git
-pip install -r requirements.txt
-python entrypoint.py
 ```
-
-#### Requirements
-- `python` with version specified in `runtime.txt`
-- `pip` with version `20.0.2`
-
-#### Env files
-First, copy env files for database and app containers
+after that, you need to copy env files and fulfill it with correct values
 ```bash
-cp docker/config/app/app.dist.env docker/config/app/app.env
-cp docker/config/database/database.dist.env docker/config/app/app.env
+cp docker/config/app.dist.env docker/config/app.env
+cp docker/config/database.dist.env docker/config/app.env
 ```
-and then fulfill copied `.env` files with required values
-
+and finally, you can run the bot by launching docker containers
+```bash
+docker-compose up -d
+```
+(`-d` flag will run containers in detached mode)
+### Logs
+You can use
+```bash
+docker/logs <container>
+```
+to check container logs
+### Env files
 app.env
-- `bot_token` - your telegram bot token from [BotFather](https://telegram.me/BotFather)
+- `BOT_TOKEN` - your telegram bot token from [BotFather](https://telegram.me/BotFather)
 - `MONGODB_DATABASE` - MongoDB database name
 - `MONGODB_USERNAME` - MongoDB username
 - `MONGODB_PASSWORD` - MongoDB password
 - `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name)
-- `MONGODB_PORT` - MongoDB port (default `port` - given in docker-compose configuration)
+- `MONGODB_PORT` - MongoDB port (default `27017` - given in docker-compose configuration)
 
 database.env
 - `MONGO_INITDB_ROOT_USERNAME` - conf from `app.env`
 - `MONGO_INITDB_ROOT_PASSWORD` - conf from `app.env`
 - `MONGO_INITDB_DATABASE` - conf from `app.env`
 - `MONGODB_DATA_DIR` - directory to store MongoDB documents (inside a container)
-- `MONDODB_LOG_DIR` - log file 
-### Commands
-#### `/in`
+- `MONDODB_LOG_DIR` - path to logs storage 
+## Commands
+### `/in`
 Will sign you in for everyone-mentions.
 
 ![in command example](docs/in_command.png)
@@ -58,7 +66,7 @@ If you have already opted-in before, alternative reply will be displayed.
 
 ![in command when someone already opted in example](docs/in_command_already_opted_in.png)
 
-#### `/out`
+### `/out`
 Will sign you off for everyone-mentions.
 
 ![out command example](docs/out_command.png)
@@ -67,10 +75,10 @@ If you haven't opted-in before, alternative reply will be displayed.
 
 ![out command when someone did not opt in example](docs/out_command_did_not_opt_in_before.png)
 
-#### `/everone`
+### `/everyone`
 Will mention everyone that opted-in for everyone-mentions separated by spaces.
 
-If user does not have nickname, it will assign random name from `names` python library to his ID
+If user does not have nickname, it will first try to assign his firstname, then random firstname from `names` python library
 
 ![everybody command example](docs/everyone_command.png)
 
diff --git a/docker-compose.yml b/docker-compose.yml
index 21047b1..3d7b5b8 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,7 +6,7 @@ services:
     image: mongo:4.0.8
     restart: unless-stopped
     env_file:
-      - ./docker/config/database/database.env
+      - ./docker/config/database.env
     volumes: 
       - db-data:/data/db
     ports:
@@ -18,7 +18,7 @@ services:
     build: .
     command: python app.py
     env_file:
-      - ./docker/config/app/app.env
+      - ./docker/config/app.env
     volumes:
       - ./src:/src
     ports:
diff --git a/docker/config/app/app.dist.env b/docker/config/app.dist.env
similarity index 100%
rename from docker/config/app/app.dist.env
rename to docker/config/app.dist.env
diff --git a/docker/config/database/database.dist.env b/docker/config/database.dist.env
similarity index 100%
rename from docker/config/database/database.dist.env
rename to docker/config/database.dist.env
diff --git a/src/app.py b/src/app.py
index 70eea6c..ffefb32 100755
--- a/src/app.py
+++ b/src/app.py
@@ -1,15 +1,17 @@
-from config.credentials import bot_token
-from config.handlers import handlers
-from handlers.handlerInterface import HandlerInterface
-from telegram.ext.dispatcher import Dispatcher
 from telegram.ext import Updater
+from telegram.ext.dispatcher import Dispatcher
+
+from config.credentials import BOT_TOKEN
+from handler.abstractHandler import AbstractHandler
+from handler import (inHandler, mentionHandler, outHandler)
+
 
 class App:
     updater: Updater
     dispatcher: Dispatcher
 
     def __init__(self):
-        self.updater = Updater(bot_token)
+        self.updater = Updater(BOT_TOKEN)
 
     def run(self) -> None:
         self.registerHandlers()
@@ -18,14 +20,13 @@ class App:
         self.updater.idle()
 
     def registerHandlers(self) -> None:
-        for handler in handlers:
-            if not isinstance(handler, HandlerInterface):
-                raise Exception('Invalid list of handlers provided. Handler must implement HandlerInterface')
-
-            self.updater.dispatcher.add_handler(handler.getBotHandler())
+        for handler in AbstractHandler.__subclasses__():
+            self.updater.dispatcher.add_handler(
+                handler().getBotHandler()
+            )
 
 
 if __name__ == "__main__":
     app = App()
 
-    app.run()
\ No newline at end of file
+    app.run()
diff --git a/src/config/contents.py b/src/config/contents.py
index d36c2fc..a3345c1 100755
--- a/src/config/contents.py
+++ b/src/config/contents.py
@@ -1,8 +1,8 @@
 import re
 
 # These are MarkdownV2 python-telegram-bot specific
-opted_in_successfully = re.escape('You have opted-in for everyone-mentions.')
+opted_in = re.escape('You have opted-in for everyone-mentions.')
 opted_in_failed = re.escape('You already opted-in for everyone-mentions.')
-opted_off_successfully = re.escape('You have opted-off for everyone-mentions.')
+opted_off = re.escape('You have opted-off for everyone-mentions.')
 opted_off_failed = re.escape('You need to opt-in first before processing this command.')
 mention_failed = re.escape('There are no users to mention.')
diff --git a/src/config/credentials.py b/src/config/credentials.py
index 3bcc710..7a1ffaa 100755
--- a/src/config/credentials.py
+++ b/src/config/credentials.py
@@ -1,9 +1,10 @@
 import os
+
 from dotenv import load_dotenv
 
 load_dotenv()
 
-bot_token = os.environ['bot_token']
+BOT_TOKEN = os.environ['BOT_TOKEN']
 
 MONGODB_DATABASE=os.environ['MONGODB_DATABASE']
 MONGODB_USERNAME=os.environ['MONGODB_USERNAME']
diff --git a/src/config/handlers.py b/src/config/handlers.py
deleted file mode 100755
index 0ea3e9c..0000000
--- a/src/config/handlers.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from handlers.inHandler import InHandler
-from handlers.outHandler import OutHandler
-from handlers.mentionHandler import MentionHandler
-
-handlers = [
-    InHandler(),
-    OutHandler(),
-    MentionHandler()
-]
diff --git a/src/database/databaseClient.py b/src/database/client.py
similarity index 53%
rename from src/database/databaseClient.py
rename to src/database/client.py
index 97ac57e..2295067 100755
--- a/src/database/databaseClient.py
+++ b/src/database/client.py
@@ -1,10 +1,13 @@
-from pymongo.errors import ServerSelectionTimeoutError
-from config.credentials import MONGODB_USERNAME, MONGODB_PASSWORD, MONGODB_DATABASE, MONGODB_HOSTNAME, MONGODB_PORT
-from pymongo import MongoClient
-from pymongo.database import Database
 from urllib.parse import quote_plus
 
-class DatabaseClient():
+from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME,
+                                MONGODB_PASSWORD, MONGODB_PORT,
+                                MONGODB_USERNAME)
+from pymongo import MongoClient
+from pymongo.database import Database
+
+
+class Client():
     mongoClient: MongoClient
     database: Database
 
@@ -17,14 +20,17 @@ class DatabaseClient():
         self.mongoClient = MongoClient(uri)
         self.database = self.mongoClient[MONGODB_DATABASE]
 
-    def insert(self, collection: str, data: dict) -> None:
+    def insertOne(self, collection: str, data: dict) -> None:
         self.database.get_collection(collection).insert_one(data)
 
-    def find(self, collection: str, query: dict) -> dict:
-        return self.database.get_collection(collection).find(query)
-
     def findOne(self, collection: str, query: dict) -> dict:
         return self.database.get_collection(collection).find_one(query)
 
-    def remove(self, collection: str, data: dict) -> None:
-        self.database.get_collection(collection).remove(data)
\ No newline at end of file
+    def findMany(self, collection: str, filter: dict) -> dict:
+        return self.database.get_collection(collection).find(filter)
+
+    def updateOne(self, collection: str, filter: dict, data: dict) -> None:
+        self.database.get_collection(collection).update_one(
+            filter, 
+            { "$set" : data }
+        )
diff --git a/src/entities/chat.py b/src/entities/chat.py
deleted file mode 100755
index ea6217e..0000000
--- a/src/entities/chat.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from __future__ import annotations
-from typing import Optional
-
-
-class Chat():
-    id: str
-
-    def __init__(self, id: str) -> None:
-        self.id = id
-
-    def getId(self) -> str:
-        return self.id
-
-    def toDict(self) -> dict:
-        return {
-            '_id': self.id
-        }
-    
-    @staticmethod
-    def getMongoRoot() -> str:
-        return 'chat'
-
-    @staticmethod
-    def fromDocument(document: Optional[dict]) -> Optional[Chat]:
-        if not document:
-            return None
-
-        return Chat(document['_id'])
\ No newline at end of file
diff --git a/src/entities/chatPerson.py b/src/entities/chatPerson.py
deleted file mode 100755
index 3a38791..0000000
--- a/src/entities/chatPerson.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from __future__ import annotations
-from typing import Optional
-
-
-class ChatPerson():
-    chat_id: str
-    person_id: str
-
-    def __init__(self, chatId: str, personId: str) -> None:
-        self.chat_id = chatId
-        self.person_id = personId
-
-    def getChatId(self) -> str:
-        return self.chat_id
-
-    def getPersonId(self) -> str:
-        return self.person_id
-
-    def toDict(self) -> dict:
-        return {
-            '_id': f'{self.chat_id}-{self.person_id}',
-            'chat_id': self.chat_id,
-            'person_id': self.person_id
-        }
-
-    @staticmethod
-    def getMongoRoot() -> str:
-        return 'chat_person'
-    
-    @staticmethod
-    def fromDocument(document: Optional[dict]) -> Optional[ChatPerson]:
-        if not document:
-            return None
-        return ChatPerson(document['chat_id'], document['person_id'])
\ No newline at end of file
diff --git a/src/entities/person.py b/src/entities/person.py
deleted file mode 100755
index 2c1cfdf..0000000
--- a/src/entities/person.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from __future__ import annotations
-from abc import abstractmethod
-from typing import Optional
-import names
-
-
-class Person():
-    id: str
-    username: str
-
-    def __init__(self, id: str, username: Optional[str] = None) -> None:
-        self.id = id
-        
-        if not username:
-            self.username = names.get_first_name()
-        else:
-            self.username = username
-
-    def getId(self) -> str:
-        return self.id
-
-    def getUsername(self) -> str:
-        return self.username
-
-    def toDict(self, withUsername: bool = True) -> dict:
-        result = {
-            '_id': self.id
-        }
-
-        if withUsername:
-            result['username'] = self.username
-        
-        return result
-
-    @staticmethod
-    def getMongoRoot() -> str:
-        return 'person'
-
-    @staticmethod
-    def fromDocument(document: Optional[dict]) -> Optional[Person]:
-        if not document:
-            return None
-
-        return Person(document['_id'], document['username'])
\ No newline at end of file
diff --git a/src/entity/user.py b/src/entity/user.py
new file mode 100644
index 0000000..58fd2b3
--- /dev/null
+++ b/src/entity/user.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from typing import Iterable
+
+
+class User():
+    collection: str = 'users'
+    idIndex: str = '_id'
+    chatsIndex: str = 'chats'
+    usernameIndex: str = 'username'
+
+    userId: str
+    username: str
+    chats: Iterable[str]
+
+    def __init__(self, userId, username, chats) -> None:
+        self.userId = userId
+        self.username = username
+        self.chats = chats
+
+    def getUserId(self) -> str:
+        return self.userId
+
+    def getUsername(self) -> str:
+        return self.username
+
+    def getChats(self) -> Iterable[str]:
+        return self.chats
+
+    def isInChat(self, chatId: str) -> bool:
+        return chatId in self.getChats()
+    
+    def addToChat(self, chatId: str) -> None:
+        self.chats.append(chatId)
+
+    def removeFromChat(self, chatId: str) -> None:
+        if chatId in self.getChats():
+            self.chats.remove(chatId)
+
+    def toMongoDocument(self) -> dict:
+        return {
+            self.usernameIndex: self.getUsername(),
+            self.chatsIndex: self.getChats()
+        }
+    
+    @staticmethod
+    def fromMongoDocument(mongoDocument: dict) -> User:
+        return User(
+            mongoDocument[User.idIndex],
+            mongoDocument[User.usernameIndex],
+            mongoDocument[User.chatsIndex]
+        )
diff --git a/src/exception/alreadyExistsException.py b/src/exception/alreadyExistsException.py
new file mode 100644
index 0000000..d3498b1
--- /dev/null
+++ b/src/exception/alreadyExistsException.py
@@ -0,0 +1,2 @@
+class AlreadyExistsException(Exception):
+    pass
\ No newline at end of file
diff --git a/src/exception/notFoundException.py b/src/exception/notFoundException.py
new file mode 100644
index 0000000..11e1ffd
--- /dev/null
+++ b/src/exception/notFoundException.py
@@ -0,0 +1,2 @@
+class NotFoundException(Exception):
+    pass
\ No newline at end of file
diff --git a/src/handlers/handlerInterface.py b/src/handler/abstractHandler.py
similarity index 76%
rename from src/handlers/handlerInterface.py
rename to src/handler/abstractHandler.py
index 5a4607c..e5ba4c7 100755
--- a/src/handlers/handlerInterface.py
+++ b/src/handler/abstractHandler.py
@@ -1,10 +1,13 @@
 from abc import abstractmethod
+
 from telegram.ext.callbackcontext import CallbackContext
 from telegram.ext.handler import Handler
 from telegram.update import Update
 
+from handler.vo.updateData import UpdateData
 
-class HandlerInterface: 
+
+class AbstractHandler: 
     def __init__(self) -> None:
         pass
 
@@ -14,8 +17,8 @@ class HandlerInterface:
     @abstractmethod
     def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented')
 
-    @abstractmethod
-    def getCommandName(self) -> str: raise Exception('getCommandName method is not implemented')
+    def getUpdateData(self, update: Update) -> UpdateData:
+        return UpdateData.createFromUpdate(update)
 
     def reply(self, update: Update, message: str) -> None:
         update.effective_message.reply_markdown_v2(text=message)
diff --git a/src/handler/inHandler.py b/src/handler/inHandler.py
new file mode 100755
index 0000000..045e306
--- /dev/null
+++ b/src/handler/inHandler.py
@@ -0,0 +1,38 @@
+from config.contents import opted_in, opted_in_failed
+from exception.notFoundException import NotFoundException
+from repository.userRepository import UserRepository
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.ext.commandhandler import CommandHandler
+from telegram.update import Update
+
+from handler.abstractHandler import AbstractHandler
+
+
+class InHandler(AbstractHandler):
+    botHandler: CommandHandler
+    userRepository: UserRepository
+
+    def __init__(self) -> None:
+        self.botHandler = CommandHandler('in', self.handle)
+        self.userRepository = UserRepository()
+
+    def handle(self, update: Update, context: CallbackContext) -> None:
+        updateData = self.getUpdateData(update)
+
+        try:
+            user = self.userRepository.getById(updateData.getUserId())
+
+            if user.isInChat(updateData.getChatId()):
+                self.reply(update, opted_in_failed)
+                return
+
+            user.addToChat(updateData.getChatId())
+            self.userRepository.save(user)
+            
+        except NotFoundException:
+            self.userRepository.saveByUpdateData(updateData)
+
+        self.reply(update, opted_in)
+
+    def getBotHandler(self) -> CommandHandler:
+        return self.botHandler
diff --git a/src/handler/mentionHandler.py b/src/handler/mentionHandler.py
new file mode 100755
index 0000000..72079d7
--- /dev/null
+++ b/src/handler/mentionHandler.py
@@ -0,0 +1,40 @@
+from typing import Iterable
+
+from config.contents import mention_failed
+from entity.user import User
+from repository.userRepository import UserRepository
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.ext.commandhandler import CommandHandler
+from telegram.update import Update
+
+from handler.abstractHandler import AbstractHandler
+
+
+class MentionHandler(AbstractHandler):
+    botHandler: CommandHandler
+    userRepository: UserRepository
+
+    def __init__(self) -> None:
+        self.botHandler = CommandHandler('everyone', self.handle)
+        self.userRepository = UserRepository()
+
+    def handle(self, update: Update, context: CallbackContext) -> None:
+        updateData = self.getUpdateData(update)
+        users = self.userRepository.getAllForChat(updateData.getChatId())
+        
+        if users:
+            self.reply(update, self.buildMentionMessage(users))
+            return
+
+        self.reply(update, mention_failed)
+
+    def getBotHandler(self) -> CommandHandler:
+        return self.botHandler
+
+    def buildMentionMessage(self, users: Iterable[User]) -> str:
+        result = ''
+
+        for user in users:
+            result += f'*[{user.getUsername()}](tg://user?id={user.getUserId()})* '
+
+        return result
diff --git a/src/handler/outHandler.py b/src/handler/outHandler.py
new file mode 100755
index 0000000..2648e11
--- /dev/null
+++ b/src/handler/outHandler.py
@@ -0,0 +1,36 @@
+from config.contents import opted_off, opted_off_failed
+from exception.notFoundException import NotFoundException
+from repository.userRepository import UserRepository
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.ext.commandhandler import CommandHandler
+from telegram.update import Update
+
+from handler.abstractHandler import AbstractHandler
+
+
+class OutHandler(AbstractHandler):
+    botHandler: CommandHandler
+    userRepository: UserRepository
+
+    def __init__(self) -> None:
+        self.botHandler = CommandHandler('out', self.handle)
+        self.userRepository = UserRepository()
+
+    def handle(self, update: Update, context: CallbackContext) -> None:
+        updateData = self.getUpdateData(update)
+
+        try:
+            user = self.userRepository.getById(updateData.getUserId())
+            if not user.isInChat(updateData.getChatId()):
+                raise NotFoundException()
+        except NotFoundException:
+            self.reply(update, opted_off_failed)
+            return
+
+        user.removeFromChat(updateData.getChatId())
+        self.userRepository.save(user)
+
+        self.reply(update, opted_off)
+
+    def getBotHandler(self) -> CommandHandler:
+        return self.botHandler
diff --git a/src/handler/vo/updateData.py b/src/handler/vo/updateData.py
new file mode 100644
index 0000000..7c6737e
--- /dev/null
+++ b/src/handler/vo/updateData.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+import names
+from telegram.update import Update
+
+
+class UpdateData():
+    userId: str
+    chatId: str
+    username: str
+
+    def __init__(self, userId: str, chatId: str, username: str) -> None:
+        self.userId = userId
+        self.chatId = chatId
+        self.username = username
+
+    def getUserId(self) -> str:
+        return self.userId
+
+    def getChatId(self) -> str:
+        return self.chatId
+
+    def getUsername(self) -> str:
+        return self.username
+
+    @staticmethod
+    def createFromUpdate(update: Update) -> UpdateData:
+        userId = str(update.effective_user.id)
+        chatId = str(update.effective_chat.id)
+        chatId = "-284685928"
+        username = update.effective_user.username or update.effective_user.first_name
+
+        if not username:
+            username = names.get_first_name()
+
+        return UpdateData(userId, chatId, username)
diff --git a/src/handlers/inHandler.py b/src/handlers/inHandler.py
deleted file mode 100755
index 49f5840..0000000
--- a/src/handlers/inHandler.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from config.contents import opted_in_successfully, opted_in_failed
-from repositories.relationRepository import RelationRepository
-from database.databaseClient import DatabaseClient
-from handlers.handlerInterface import HandlerInterface
-from telegram.ext.callbackcontext import CallbackContext
-from telegram.ext.commandhandler import CommandHandler
-from telegram.update import Update
-
-class InHandler(HandlerInterface):
-    botHandler: CommandHandler
-    commandName: str = 'in'
-
-    def __init__(self) -> None:
-        self.botHandler = CommandHandler(
-            self.getCommandName(),
-            self.handle
-        )
-
-    def handle(self, update: Update, context: CallbackContext) -> None:
-        personId = update.effective_user.id
-        chatId = update.effective_chat.id
-        username = update.effective_user.username
-
-        relationRepository = RelationRepository()
-        relation = relationRepository.get(chatId, personId)
-
-        if relation:
-            self.reply(update, opted_in_failed)
-            return
-        
-        relationRepository.save(chatId, personId, username)
-        self.reply(update, opted_in_successfully)
-
-    def getBotHandler(self) -> CommandHandler:
-        return self.botHandler
-
-    def getCommandName(self) -> str:
-        return self.commandName
diff --git a/src/handlers/mentionHandler.py b/src/handlers/mentionHandler.py
deleted file mode 100755
index 4cad530..0000000
--- a/src/handlers/mentionHandler.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from typing import Iterable
-from config.contents import mention_failed
-from entities.person import Person
-from handlers.handlerInterface import HandlerInterface
-from repositories.relationRepository import RelationRepository
-from repositories.personRepository import PersonRepository
-from telegram.ext.callbackcontext import CallbackContext
-from telegram.ext.commandhandler import CommandHandler
-from telegram.update import Update
-
-
-class MentionHandler(HandlerInterface):
-    botHandler: CommandHandler
-    commandName: str = 'everyone'
-
-    def __init__(self) -> None:
-        self.botHandler = CommandHandler(
-            self.getCommandName(), 
-            self.handle
-        )
-
-    def handle(self, update: Update, context: CallbackContext) -> None:
-        relationRepository = RelationRepository()
-        persons = relationRepository.getPersonsForChat(update.effective_chat.id)
-
-        if not persons:
-            self.reply(update, mention_failed)
-            return
-
-        self.reply(update, self.buildMentionMessage(persons))
-
-
-    def getBotHandler(self) -> CommandHandler:
-        return self.botHandler
-
-    def getCommandName(self) -> str:
-        return self.commandName
-
-    def buildMentionMessage(self, persons: Iterable[Person]) -> str:
-        result = ''
-
-        for person in persons:
-            result +=  f'*[{person.getUsername()}](tg://user?id={person.getId()})* '
-
-        return result
diff --git a/src/handlers/outHandler.py b/src/handlers/outHandler.py
deleted file mode 100755
index f0597a5..0000000
--- a/src/handlers/outHandler.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from config.contents import opted_off_successfully, opted_off_failed
-from handlers.handlerInterface import HandlerInterface
-from telegram.ext.callbackcontext import CallbackContext
-from telegram.ext.commandhandler import CommandHandler
-from telegram.update import Update
-
-from repositories.relationRepository import RelationRepository
-
-
-class OutHandler(HandlerInterface):
-    botHandler: CommandHandler
-    commandName: str = 'out'
-
-    def __init__(self) -> None:
-        self.botHandler = CommandHandler(
-            self.getCommandName(), 
-            self.handle
-        )
-
-    def handle(self, update: Update, context: CallbackContext) -> None:
-        personId = update.effective_user.id
-        chatId = update.effective_chat.id
-
-        relationRepository = RelationRepository()
-        relation = relationRepository.get(chatId, personId)
-        
-        if not relation:
-            self.reply(update, opted_off_failed)
-            return
-        
-        relationRepository.remove(relation)
-        self.reply(update, opted_off_successfully)
-
-    def getBotHandler(self) -> CommandHandler:
-        return self.botHandler
-
-    def getCommandName(self) -> str:
-        return self.commandName
diff --git a/src/repositories/chatRepository.py b/src/repositories/chatRepository.py
deleted file mode 100755
index ec57f0a..0000000
--- a/src/repositories/chatRepository.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from database.databaseClient import DatabaseClient
-from entities.chat import Chat
-from typing import Optional
-
-class ChatRepository:
-    database: DatabaseClient
-
-    def __init__(self) -> None:
-        self.database = DatabaseClient()
-
-    def get(self, id: str) -> Optional[Chat]:
-        chat = Chat(id)
-        search = self.database.findOne(Chat.getMongoRoot(), chat.toDict())
-        
-        return Chat.fromDocument(search)
-        
-    def save(self, chat: Chat) -> None:
-        self.database.insert(Chat.getMongoRoot(), chat.toDict())
\ No newline at end of file
diff --git a/src/repositories/personRepository.py b/src/repositories/personRepository.py
deleted file mode 100755
index b3ee6bb..0000000
--- a/src/repositories/personRepository.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from database.databaseClient import DatabaseClient
-from entities.person import Person
-from typing import Iterable, Optional
-
-class PersonRepository:
-    database: DatabaseClient
-
-    def __init__(self) -> None:
-        self.database = DatabaseClient()
-
-    def get(self, id: str) -> Optional[Person]:
-        person = Person(id)
-        search = self.database.findOne(Person.getMongoRoot(), person.toDict(False))
-        
-        return Person.fromDocument(search)        
-        
-    def find(self, query: dict) -> Iterable[Person]:
-        result = []
-        search = self.database.find(Person.getMongoRoot(), query)
-
-        for document in search:
-            result.append(Person.fromDocument(document))
-
-        return result
-
-    def save(self, person: Person) -> None:
-        self.database.insert(Person.getMongoRoot(), person.toDict())
\ No newline at end of file
diff --git a/src/repositories/relationRepository.py b/src/repositories/relationRepository.py
deleted file mode 100755
index 3482544..0000000
--- a/src/repositories/relationRepository.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from typing import Iterable, Optional
-from database.databaseClient import DatabaseClient
-from entities.chat import Chat
-from entities.chatPerson import ChatPerson
-from entities.person import Person
-from repositories.personRepository import PersonRepository
-from repositories.chatRepository import ChatRepository
-
-
-class RelationRepository():
-    client: DatabaseClient
-
-    def __init__(self) -> None:
-        self.client = DatabaseClient()
-
-    def get(self, chatId: str, personId: str) -> Optional[ChatPerson]:
-        relation = ChatPerson(chatId, personId)
-        search = self.client.findOne(ChatPerson.getMongoRoot(), relation.toDict())
-        
-        return ChatPerson.fromDocument(search)
-
-    def save(self, chatId: str, personId: str, username: Optional[str] = None) -> None:
-        relation = ChatPerson(chatId, personId)
-
-        self.client.insert(ChatPerson.getMongoRoot(), relation.toDict())
-        personRepository = PersonRepository()
-        person = personRepository.get(personId)
-
-        if not person:
-            person = Person(personId, username)
-            personRepository.save(person)
-
-        chatRepository = ChatRepository()
-        chat = chatRepository.get(chatId)
-        
-        if not chat:
-            chat = Chat(chatId)
-            chatRepository.save(chat)
-
-    def getPersonsForChat(self, chatId: str) -> Iterable[ChatPerson]:
-        result = []
-        relations = self.client.find(ChatPerson.getMongoRoot(), {'chat_id': chatId})
-        
-        search = {}
-        for relation in relations:
-            search['_id'] = relation['person_id']
-
-        if not search:
-            return result
-
-        personRepository = PersonRepository()
-        return personRepository.find(search)
-
-    def remove(self, relation: ChatPerson) -> None:
-        self.client.remove(ChatPerson.getMongoRoot() ,relation.toDict())
-
diff --git a/src/repository/userRepository.py b/src/repository/userRepository.py
new file mode 100644
index 0000000..2f16f81
--- /dev/null
+++ b/src/repository/userRepository.py
@@ -0,0 +1,66 @@
+from typing import Iterable, Optional
+
+from database.client import Client
+from entity.user import User
+from exception.notFoundException import NotFoundException
+from handler.vo.updateData import UpdateData
+
+
+class UserRepository():
+    client: Client
+
+    def __init__(self) -> None:
+        self.client = Client()
+
+    def getById(self, id: str) -> User:
+        user = self.client.findOne(
+            User.collection,
+            {
+                User.idIndex: id
+            }
+        )
+
+        if not user:
+            raise NotFoundException(f'Could not find user with "{id}" id')
+
+        return User(
+            user[User.idIndex],
+            user[User.usernameIndex],
+            user[User.chatsIndex]
+        )
+        
+    def save(self, user: User) -> None:
+        self.client.updateOne(
+            User.collection,
+            { User.idIndex: user.getUserId() },
+            user.toMongoDocument()
+        )
+
+    def saveByUpdateData(self, data: UpdateData) -> None:
+        self.client.insertOne(
+            User.collection, 
+            {
+                User.idIndex: data.getUserId(),
+                User.usernameIndex: data.getUsername(),
+                User.chatsIndex: [data.getChatId()]
+            }
+        )
+    
+    def getAllForChat(self, chatId: str) -> Iterable[User]:
+        result = []
+        users = self.client.findMany(
+            User.collection,
+            {
+                User.chatsIndex: {
+                    "$in" : [chatId]
+                }
+            }
+        )
+        
+        for record in users:
+            result.append(User.fromMongoDocument(record))
+
+        return result
+
+    
+