diff --git a/CHANGELOG.md b/CHANGELOG.md
index c14ce3a..8046c8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,18 @@
 # Change Log
 All notable changes to this project will be documented in this file.
  
+## [0.2.0] - 26.10.2021
+### Added
+- Inline Mode for `join`, `leave` & `everyone`
+- Banned users environment variable
+- Buttons for `start` message
+### Changed
+- Code quality improvements
+- `start` text
+- mongodb data structure
+- group name max length to 40
+### Deleted
+- `/silent` command
 ## [0.1.0] - 06.10.2021
 ### Features
 - `/join` command
diff --git a/README.md b/README.md
index d99b678..2204f60 100755
--- a/README.md
+++ b/README.md
@@ -5,23 +5,97 @@
 # Contents
 
 * [Description](#description)
-* [Getting started.](#getting-started)
-    * [Requirements](#requirements)
-    * [Installation](#installation)
-    * [Logs](#logs)
-    * [Env files](#env-files)
 * [Commands](#commands)
     * [`/join`](#join)
     * [`/leave`](#leave)
     * [`/everyone`](#everyone)
     * [`/groups`](#groups)
-    * [`/silent`](#silent)
     * [`/start`](#start)
+    * [Example command flow](#example-command-flow)
+* [Inline Mode](#inline-mode)
+    * [Usage](#usage)
+* [Getting started.](#getting-started)
+    * [Requirements](#requirements)
+    * [Installation](#installation)
+    * [Logs](#logs)
+    * [Env files](#env-files)
 ## Description
 Everyone Mention Bot is simple, but useful telegram bot to gather group members attention.
 
 You can create groups per chat to mention every user that joined the group by calling one command instead of mentioning them one by one.
 
+## Commands
+*Important*: `{group-name}` is not required, if not given, it will be set to `default`.
+### `/join`
+Joins the group (if group did not exist before, it will be created).
+```
+/join {group_name}
+```
+*Examples*
+
+without group name
+
+![join default command example](docs/join_default.png)
+
+with group name
+
+![join group command example](docs/join_group.png)
+
+### `/leave`
+Leaves the group (and deletes if no members are left).
+```
+/leave {group_name}
+```
+*Examples*
+
+without group name
+
+![leave default command example](docs/leave_default.png)
+
+with group name
+
+![leave group command example](docs/leave_group.png)
+
+### `/everyone`
+Mention everyone that joined requested group.
+```
+/everyone {group-name}
+```
+
+*Examples*
+
+without group name
+
+![everyone default command example](docs/everyone_default.png)
+
+with group name
+
+![everyone group command example](docs/everyone_group.png)
+
+
+### `/groups`
+Will display available groups for this chat as well with members count.
+
+![groups command example](docs/groups.png)
+### `/start`
+Start & Help message
+
+![start command example](docs/start.png)
+
+### Example command flow
+![example command flow](docs/flow_command.png)
+
+## Inline Mode
+Using Inline Mode is recommended because policy of bots with privacy mode enabled (https://core.telegram.org/bots/faq#what-messages-will-my-bot-get) says that command trigger is sent (without mentioning the bot) only to the last mentioned bot. So if you do have multiple bots in current chat, I might not receive your command!
+
+### Usage
+To use inline mode, type `@everyone_mention_bot` in telegram message input or click on the `Inline Mode` button from `/start` command.
+
+![inline popup](docs/inline_mode_1.png)
+
+Type in your group name (you can leave it blank for `default` group name), then click on one of the three following options.
+
+![inline answer](docs/inline_mode_2.png)
 ## Getting started
 ### Requirements
 - `docker-compose` in version `1.25.0`
@@ -35,8 +109,8 @@ git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git
 - copy environment files and fulfill empty values
 ```bash
 cp .env.local .env
-cp docker/config/app.dist.env docker/config/app.env
-cp docker/config/database.dist.env docker/config/app.env
+cp docker/config/app.env.local 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)
 ```bash
@@ -62,56 +136,11 @@ docker/logs <container>
   - `MONGODB_PASSWORD` - MongoDB password
   - `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name)
   - `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`
   - `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` - path to logs storage 
-## Commands
-### `/join`
-```
-/join <group_name>
-```
-Joins the group (and create if it did not exist before) given in message (`default` if not given)
-
-![join command example](docs/join.png)
-
-### `/leave`
-```
-/leave <group_name>
-```
-
-Leaves the group given in message (`default` if not given)
-
-![leave command example](docs/leave.png)
-
-### `/everyone`
-```
-/everyone <group_id>
-```
-Will mention every member of given group (`default` if not given).
-
-If user does not have nickname, it will first try to assign his firstname, then random firstname from `names` python library
-
-![everyone command example](docs/everyone.png)
-
-### `/groups`
-Will display available groups for this chat as well with members count.
-
-![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 & Help message
-
-![start command example](docs/start.png)
+  - `MONDODB_LOG_DIR` - path to logs storage 
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 32d7996..1677662 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,7 +22,6 @@ services:
       - ./docker/config/app.env
     volumes:
       - ./src:/src
-      - ./logs:/var/log/bot
     ports:
       - $APP_EXPOSED_PORT:$APP_INTERNAL_PORT
     depends_on:
diff --git a/docker/config/app.dist.env b/docker/config/app.env.local
similarity index 77%
rename from docker/config/app.dist.env
rename to docker/config/app.env.local
index cc14bd0..de9bb54 100755
--- a/docker/config/app.dist.env
+++ b/docker/config/app.env.local
@@ -6,4 +6,6 @@ MONGODB_DATABASE=
 MONGODB_USERNAME=
 MONGODB_PASSWORD=
 MONGODB_HOSTNAME=localhost
-MONGODB_PORT=27017
\ No newline at end of file
+MONGODB_PORT=27017
+
+BANNED_USERS=
\ No newline at end of file
diff --git a/docker/config/database.dist.env b/docker/config/database.env.local
similarity index 100%
rename from docker/config/database.dist.env
rename to docker/config/database.env.local
diff --git a/docker/mongosh b/docker/mongosh
new file mode 100755
index 0000000..a87a43e
--- /dev/null
+++ b/docker/mongosh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+if [ -f docker/config/app.env ]
+then
+  source docker/config/app.env
+fi
+
+docker-compose exec database mongosh --port $MONGODB_PORT --host $MONGODB_HOSTNAME --username $MONGODB_USERNAME --password $MONGODB_PASSWORD --authenticationDatabase admin $MONGODB_DATABASE
\ No newline at end of file
diff --git a/docs/everyone.png b/docs/everyone.png
deleted file mode 100644
index 63b2550..0000000
Binary files a/docs/everyone.png and /dev/null differ
diff --git a/docs/everyone_default.png b/docs/everyone_default.png
new file mode 100644
index 0000000..1f555ab
Binary files /dev/null and b/docs/everyone_default.png differ
diff --git a/docs/everyone_group.png b/docs/everyone_group.png
new file mode 100644
index 0000000..a35ae94
Binary files /dev/null and b/docs/everyone_group.png differ
diff --git a/docs/flow_command.png b/docs/flow_command.png
new file mode 100644
index 0000000..842e0fe
Binary files /dev/null and b/docs/flow_command.png differ
diff --git a/docs/groups.png b/docs/groups.png
index c4e1ead..8ab9d69 100644
Binary files a/docs/groups.png and b/docs/groups.png differ
diff --git a/docs/inline_mode_1.png b/docs/inline_mode_1.png
new file mode 100644
index 0000000..242cfb7
Binary files /dev/null and b/docs/inline_mode_1.png differ
diff --git a/docs/inline_mode_2.png b/docs/inline_mode_2.png
new file mode 100644
index 0000000..a11d380
Binary files /dev/null and b/docs/inline_mode_2.png differ
diff --git a/docs/join.png b/docs/join.png
deleted file mode 100644
index 85749ec..0000000
Binary files a/docs/join.png and /dev/null differ
diff --git a/docs/join_default.png b/docs/join_default.png
new file mode 100644
index 0000000..328eefb
Binary files /dev/null and b/docs/join_default.png differ
diff --git a/docs/join_group.png b/docs/join_group.png
new file mode 100644
index 0000000..dcf3467
Binary files /dev/null and b/docs/join_group.png differ
diff --git a/docs/leave.png b/docs/leave.png
deleted file mode 100644
index 2d60996..0000000
Binary files a/docs/leave.png and /dev/null differ
diff --git a/docs/leave_default.png b/docs/leave_default.png
new file mode 100644
index 0000000..23deef8
Binary files /dev/null and b/docs/leave_default.png differ
diff --git a/docs/leave_group.png b/docs/leave_group.png
new file mode 100644
index 0000000..9d6f5dc
Binary files /dev/null and b/docs/leave_group.png differ
diff --git a/docs/silent.png b/docs/silent.png
deleted file mode 100644
index 1db4c9b..0000000
Binary files a/docs/silent.png and /dev/null differ
diff --git a/docs/start.png b/docs/start.png
index 0e48122..719b676 100644
Binary files a/docs/start.png and b/docs/start.png differ
diff --git a/src/app.py b/src/app.py
index b70a888..a5ccf9b 100755
--- a/src/app.py
+++ b/src/app.py
@@ -1,27 +1,21 @@
-from logging import Logger
-import logging
 from telegram.ext import Updater
 from telegram.ext.dispatcher import Dispatcher
 
-from logger import Logger
-from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
-from bot.handler import (groupsHandler, joinHandler, mentionHandler, leaveHandler,
-                     silentMentionHandler, startHandler)
+from bot.handler import *
 from bot.handler.abstractHandler import AbstractHandler
+from config.envs import BOT_TOKEN, PORT, WEBHOOK_URL
+from logger import Logger
 
 
 class App:
     updater: Updater
     dispatcher: Dispatcher
 
-    log_file: str = '/var/log/bot.log'
-    log_format: str = '%(levelname)s-%(asctime)s: %(message)s'
-
     def __init__(self):
         self.updater = Updater(BOT_TOKEN)
 
     def run(self) -> None:
-        self.setup_logging()
+        Logger.register()
         self.register_handlers()
         self.register_webhook()
         
@@ -29,9 +23,7 @@ class App:
 
     def register_handlers(self) -> None:
         for handler in AbstractHandler.__subclasses__():
-            self.updater.dispatcher.add_handler(
-                handler().get_bot_handler()
-            )
+            self.updater.dispatcher.add_handler(handler().bot_handler)
 
     def register_webhook(self) -> None:
         self.updater.start_webhook(
@@ -41,15 +33,9 @@ class App:
             webhook_url="/".join([WEBHOOK_URL, BOT_TOKEN])
         )
 
-        Logger.get_logger(Logger.action_logger).info(
-            f'Webhook configured, listening on {WEBHOOK_URL}/<bot-token>'
-        )
+        Logger.info(f'Webhook configured, listening on {WEBHOOK_URL}/<bot-token>')
 
-    def setup_logging(self) -> None:
-        logger = Logger()
-        logger.setup()
 
 if __name__ == "__main__":
     app = App()
-
     app.run()
diff --git a/src/bot/handler/__init__.py b/src/bot/handler/__init__.py
new file mode 100644
index 0000000..3a55bbb
--- /dev/null
+++ b/src/bot/handler/__init__.py
@@ -0,0 +1,5 @@
+__all__ = [
+    'abstractHandler', 'everyoneHandler', 'groupsHandler',
+    'inlineQueryHandler', 'joinHandler', 'leaveHandler',
+    'startHandler'
+]
diff --git a/src/bot/handler/abstractHandler.py b/src/bot/handler/abstractHandler.py
index 553f4d8..d686924 100755
--- a/src/bot/handler/abstractHandler.py
+++ b/src/bot/handler/abstractHandler.py
@@ -1,37 +1,37 @@
 from abc import abstractmethod
 
-from bot.message.messageData import MessageData
-from logger import Logger
+from telegram.ext import Handler
 from telegram.ext.callbackcontext import CallbackContext
-from telegram.ext.handler import Handler
 from telegram.update import Update
-from telegram.utils.helpers import mention_markdown
+
+from bot.message.inboundMessage import InboundMessage
+from bot.message.replier import Replier
+from exception.actionNotAllowedException import ActionNotAllowedException
+from exception.invalidActionException import InvalidActionException
+from exception.invalidArgumentException import InvalidArgumentException
+from logger import Logger
 
 
-class AbstractHandler: 
-    @abstractmethod
-    def get_bot_handler(self) -> Handler: raise Exception('get_bot_handler method is not implemented')
+class AbstractHandler:
+    bot_handler: Handler
+    inbound: InboundMessage
+    action: str
 
     @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:
+    def wrap(self, update: Update, context: CallbackContext) -> None:
         try:
-            update.effective_message.reply_markdown_v2(text=message)
-        except Exception as err:
-            Logger.error(str(err))
+            group_specific = self.is_group_specific()
 
-    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))
+            self.inbound = InboundMessage.create(update, context, group_specific)
+            self.handle(update, context)
+            Logger.action(self.inbound, self.action)
+        except (InvalidActionException, InvalidArgumentException, ActionNotAllowedException) as e:
+            Replier.markdown(update, str(e))
+        except Exception as e:
+            Logger.exception(e)
+
+    def is_group_specific(self) -> bool:
+        return True
diff --git a/src/bot/handler/everyoneHandler.py b/src/bot/handler/everyoneHandler.py
new file mode 100755
index 0000000..a31feac
--- /dev/null
+++ b/src/bot/handler/everyoneHandler.py
@@ -0,0 +1,32 @@
+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.replier import Replier
+from config.contents import mention_failed
+from exception.invalidActionException import InvalidActionException
+from exception.notFoundException import NotFoundException
+from repository.chatRepository import ChatRepository
+from repository.userRepository import UserRepository
+from utils.messageBuilder import MessageBuilder
+
+
+class EveryoneHandler(AbstractHandler):
+    bot_handler: CommandHandler
+    chat_repository: ChatRepository
+    user_repository: UserRepository
+    action: str = 'everyone'
+
+    def __init__(self) -> None:
+        self.bot_handler = CommandHandler(self.action, self.wrap)
+        self.chat_repository = ChatRepository()
+        self.user_repository = UserRepository()
+
+    def handle(self, update: Update, context: CallbackContext) -> None:
+        try:
+            users = self.chat_repository.get_users_for_group(self.inbound.chat_id, self.inbound.group_name)
+
+            Replier.markdown(update, MessageBuilder.mention_message(users))
+        except NotFoundException as e:
+            raise InvalidActionException(mention_failed) from e
diff --git a/src/bot/handler/groupsHandler.py b/src/bot/handler/groupsHandler.py
index 0270404..9190b45 100644
--- a/src/bot/handler/groupsHandler.py
+++ b/src/bot/handler/groupsHandler.py
@@ -1,45 +1,34 @@
-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.commandhandler import CommandHandler
 from telegram.update import Update
 
+from bot.handler.abstractHandler import AbstractHandler
+from bot.message.replier import Replier
+from config.contents import no_groups
+from exception.invalidActionException import InvalidActionException
+from exception.notFoundException import NotFoundException
+from repository.chatRepository import ChatRepository
+from utils.messageBuilder import MessageBuilder
+
 
 class GroupsHandler(AbstractHandler):
     bot_handler: CommandHandler
-    group_repository: GroupRepository
+    chat_repository: ChatRepository
+    action: str = 'groups'
 
     def __init__(self) -> None:
-        self.bot_handler = CommandHandler('groups', self.handle)
-        self.group_repository = GroupRepository()
+        self.bot_handler = CommandHandler(self.action, self.wrap)
+        self.chat_repository = ChatRepository()
 
     def handle(self, update: Update, context: CallbackContext) -> None:
-        message_data = MessageData.create_from_arguments(update, context, False)
+        try:
+            chat = self.chat_repository.get(self.inbound.chat_id)
+            if not chat.groups:
+                raise NotFoundException
 
-        groups = self.group_repository.get_by_chat_id(message_data.chat_id)
+            Replier.html(update, MessageBuilder.group_message(chat.groups))
+        except NotFoundException:
+            raise InvalidActionException(no_groups)
 
-        if groups:
-            self.reply_html(update, self.build_groups_message(groups))
-            return self.log_action(message_data)
-
-        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>'
+    def is_group_specific(self) -> bool:
+        return False
diff --git a/src/bot/handler/inlineQueryHandler.py b/src/bot/handler/inlineQueryHandler.py
new file mode 100644
index 0000000..baa9cd3
--- /dev/null
+++ b/src/bot/handler/inlineQueryHandler.py
@@ -0,0 +1,51 @@
+from telegram import InlineQueryResultArticle
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.ext.inlinequeryhandler import \
+    InlineQueryHandler as CoreInlineQueryHandler
+from telegram.inline.inputtextmessagecontent import InputTextMessageContent
+from telegram.update import Update
+
+from bot.handler.abstractHandler import AbstractHandler
+from bot.message.inboundMessage import InboundMessage
+from exception.actionNotAllowedException import ActionNotAllowedException
+from validator.accessValidator import AccessValidator
+
+
+class InlineQueryHandler(AbstractHandler):
+    bot_handler: CoreInlineQueryHandler
+
+    def __init__(self) -> None:
+        self.bot_handler = CoreInlineQueryHandler(self.handle)
+
+    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 InboundMessage.default_group
+        group = '' if group_display == InboundMessage.default_group else group_display
+
+        results = [
+            InlineQueryResultArticle(
+                id='everyone',
+                title='MENTION',
+                description=f'Mention members in group "{group_display}"',
+                input_message_content=InputTextMessageContent(f'/everyone {group}')
+            ),
+            InlineQueryResultArticle(
+                id='join',
+                title='JOIN',
+                description=f'Joins group "{group_display}"',
+                input_message_content=InputTextMessageContent(f'/join {group}')
+            ),
+            InlineQueryResultArticle(
+                id='leave',
+                title='LEAVE',
+                description=f'Leaves group "{group_display}"',
+                input_message_content=InputTextMessageContent(f'/leave {group}')
+            )
+        ]
+
+        update.inline_query.answer(results, cache_time=4800)
diff --git a/src/bot/handler/joinHandler.py b/src/bot/handler/joinHandler.py
index 5262a16..92a8a69 100755
--- a/src/bot/handler/joinHandler.py
+++ b/src/bot/handler/joinHandler.py
@@ -1,46 +1,34 @@
-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.commandhandler import CommandHandler
 from telegram.update import Update
 
+from bot.handler.abstractHandler import AbstractHandler
+from bot.message.replier import Replier
+from config.contents import joined, not_joined
+from exception.invalidActionException import InvalidActionException
+from repository.chatRepository import ChatRepository
+from repository.userRepository import UserRepository
+
 
 class JoinHandler(AbstractHandler):
     bot_handler: CommandHandler
     user_repository: UserRepository
+    action: str = 'join'
 
     def __init__(self) -> None:
-        self.bot_handler = CommandHandler('join', self.handle)
+        self.bot_handler = CommandHandler(self.action, self.wrap)
         self.user_repository = UserRepository()
+        self.chat_repository = ChatRepository()
 
     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))
+        user = self.user_repository.provide(self.inbound)
+        chat = self.chat_repository.provide(self.inbound)
+        users = chat.groups.get(self.inbound.group_name)
 
-        try:
-            user = self.user_repository.get_by_id(message_data.user_id)
+        if user.user_id in users:
+            raise InvalidActionException(Replier.interpolate(not_joined, self.inbound))
 
-            if user.is_in_chat(message_data.chat_id):
-                return self.reply_markdown(update, self.interpolate_reply(not_joined, message_data))
+        users.append(user.user_id)
+        self.chat_repository.save(chat)
 
-            user.add_to_chat(message_data.chat_id)
-            self.user_repository.save(user)
-        except NotFoundException:
-            self.user_repository.save_by_message_data(message_data)
-
-        self.reply_markdown(update, self.interpolate_reply(joined, 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} joined {message_data.chat_id}')
+        Replier.markdown(update, Replier.interpolate(joined, self.inbound))
diff --git a/src/bot/handler/leaveHandler.py b/src/bot/handler/leaveHandler.py
index 73b7588..2c29b4a 100755
--- a/src/bot/handler/leaveHandler.py
+++ b/src/bot/handler/leaveHandler.py
@@ -1,45 +1,38 @@
-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.commandhandler import CommandHandler
 from telegram.update import Update
 
+from bot.handler.abstractHandler import AbstractHandler
+from bot.message.replier import Replier
+from config.contents import left, not_left
+from exception.invalidActionException import InvalidActionException
+from repository.userRepository import UserRepository
+from repository.chatRepository import ChatRepository
+
 
 class LeaveHandler(AbstractHandler):
     bot_handler: CommandHandler
     user_repository: UserRepository
+    chat_repository: ChatRepository
+    action: str = 'leave'
 
     def __init__(self) -> None:
-        self.bot_handler = CommandHandler('leave', self.handle)
+        self.bot_handler = CommandHandler(self.action, self.wrap)
         self.user_repository = UserRepository()
+        self.chat_repository = ChatRepository()
 
     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))
+        user = self.user_repository.provide(self.inbound)
+        chat = self.chat_repository.provide(self.inbound)
+        group = chat.groups.get(self.inbound.group_name)
 
-        try:
-            user = self.user_repository.get_by_id(message_data.user_id)
+        if user.user_id not in group:
+            raise InvalidActionException(Replier.interpolate(not_left, self.inbound))
 
-            if not user.is_in_chat(message_data.chat_id):
-                raise NotFoundException()
-        except NotFoundException:
-            return self.reply_markdown(update, self.interpolate_reply(not_left, message_data))
+        group.remove(user.user_id)
+        if not group:
+            chat.groups.pop(self.inbound.group_name)
 
-        user.remove_from_chat(message_data.chat_id)
-        self.user_repository.save(user)
+        self.chat_repository.save(chat)
 
-        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}')
+        Replier.markdown(update, Replier.interpolate(left, self.inbound))
diff --git a/src/bot/handler/mentionHandler.py b/src/bot/handler/mentionHandler.py
deleted file mode 100755
index d44e8cb..0000000
--- a/src/bot/handler/mentionHandler.py
+++ /dev/null
@@ -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])
-
diff --git a/src/bot/handler/silentMentionHandler.py b/src/bot/handler/silentMentionHandler.py
deleted file mode 100644
index c1a0248..0000000
--- a/src/bot/handler/silentMentionHandler.py
+++ /dev/null
@@ -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}')
diff --git a/src/bot/handler/startHandler.py b/src/bot/handler/startHandler.py
index 8454d08..be4b323 100644
--- a/src/bot/handler/startHandler.py
+++ b/src/bot/handler/startHandler.py
@@ -1,25 +1,35 @@
-from config.contents import start_text
-from logger import Logger
+from telegram import InlineKeyboardButton, InlineKeyboardMarkup
 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 start_text
+from logger import Logger
 
 
 class StartHandler(AbstractHandler):
     bot_handler: CommandHandler
+    action: str = 'start'
 
     def __init__(self) -> None:
-        self.bot_handler = CommandHandler('start', self.handle)
+        self.bot_handler = CommandHandler(self.action, self.wrap)
 
     def handle(self, update: Update, context: CallbackContext) -> None:
-        self.reply_markdown(update, start_text)
-        self.log_action(MessageData.create_from_arguments(update, context))
+        markup = InlineKeyboardMarkup(
+            [
+                [
+                    InlineKeyboardButton('Inline Mode', switch_inline_query_current_chat='example'),
+                ],
+                [
+                    InlineKeyboardButton('GitHub', url='https://github.com/miloszowi/everyone-mention-telegram-bot'),
+                    InlineKeyboardButton('Creator', url='https://t.me/miloszowi')
+                ]
+            ]
+        )
 
-    def get_bot_handler(self) -> CommandHandler:
-        return self.bot_handler
+        Replier.html(update, start_text, markup)
 
-    def log_action(self, message_data: MessageData) -> None:
-        Logger.info(f'User {message_data.username} called /start for {message_data.chat_id}')
+    def is_group_specific(self) -> bool:
+        return False
diff --git a/src/bot/message/inboundMessage.py b/src/bot/message/inboundMessage.py
new file mode 100644
index 0000000..c0f5505
--- /dev/null
+++ b/src/bot/message/inboundMessage.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+import names
+from telegram.ext.callbackcontext import CallbackContext
+from telegram.update import Update
+
+from validator.accessValidator import AccessValidator
+from validator.groupNameValidator import GroupNameValidator
+
+
+@dataclass
+class InboundMessage:
+    user_id: str
+    chat_id: str
+    group_name: str
+    username: str
+
+    default_group: str = 'default'
+
+    @staticmethod
+    def create(update: Update, context: CallbackContext, group_specific: bool) -> InboundMessage:
+        user_id = str(update.effective_user.id)
+        AccessValidator.validate(user_id)
+
+        chat_id = str(update.effective_chat.id)
+        group_name = InboundMessage.default_group
+
+        if context.args and context.args[0] and group_specific:
+            group_name = str(context.args[0]).lower()
+
+            GroupNameValidator.validate(group_name)
+
+        username = update.effective_user.username or update.effective_user.first_name
+
+        if not username:
+            username = names.get_first_name()
+
+        return InboundMessage(user_id, chat_id, group_name, username)
diff --git a/src/bot/message/messageData.py b/src/bot/message/messageData.py
deleted file mode 100644
index 49ab6ff..0000000
--- a/src/bot/message/messageData.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from __future__ import annotations
-
-import re
-from dataclasses import dataclass
-
-import names
-from entity.group import Group
-from exception.invalidArgumentException import InvalidArgumentException
-from telegram.ext.callbackcontext import CallbackContext
-from telegram.update import Update
-
-
-@dataclass
-class MessageData():
-    user_id: str
-    chat_id: str
-    group_name: str
-    username: str
-
-    @staticmethod
-    def create_from_arguments(update: Update, context: CallbackContext, include_group: bool = True) -> MessageData:
-        chat_id = str(update.effective_chat.id)
-        group_name = Group.default_name
-
-        if context.args and context.args[0] and include_group:
-            group_name = str(context.args[0]).lower()
-            if not re.match(r"^[A-Za-z]+$", group_name):
-                raise InvalidArgumentException(re.escape('Group name must contain only letters.'))
-
-            if group_name == Group.default_name:
-                raise InvalidArgumentException(re.escape(f'Group can not be `{Group.default_name}`.'))
-
-            if len(group_name) > 20:
-                raise InvalidArgumentException(re.escape(f'Group name length can not be greater than 20.'))
-
-            chat_id += f'~{group_name}'
-            
-
-        user_id = str(update.effective_user.id)
-        username = update.effective_user.username or update.effective_user.first_name
-
-        if not username:
-            username = names.get_first_name()
-
-        return MessageData(user_id, chat_id, group_name, username)
diff --git a/src/bot/message/replier.py b/src/bot/message/replier.py
new file mode 100644
index 0000000..6d207dd
--- /dev/null
+++ b/src/bot/message/replier.py
@@ -0,0 +1,31 @@
+from typing import Optional
+
+from telegram import InlineKeyboardMarkup, Update
+from telegram.utils.helpers import mention_markdown
+
+from bot.message.inboundMessage import InboundMessage
+from logger import Logger
+
+
+class Replier:
+
+    @staticmethod
+    def interpolate(content: str, inbound_message: InboundMessage):
+        return content.format(
+            mention_markdown(inbound_message.user_id, inbound_message.username),
+            inbound_message.group_name
+        )
+
+    @staticmethod
+    def markdown(update: Update, message: str, reply_markup: Optional[InlineKeyboardMarkup] = None) -> None:
+        try:
+            update.effective_message.reply_markdown_v2(message, reply_markup=reply_markup)
+        except Exception as err:
+            Logger.error(str(err))
+
+    @staticmethod
+    def html(update: Update, html: str, reply_markup: Optional[InlineKeyboardMarkup] = None) -> None:
+        try:
+            update.effective_message.reply_html(html, reply_markup=reply_markup, disable_web_page_preview=True)
+        except Exception as err:
+            Logger.error(str(err))
diff --git a/src/config/contents.py b/src/config/contents.py
index 6ad7118..952a2bc 100755
--- a/src/config/contents.py
+++ b/src/config/contents.py
@@ -1,3 +1,4 @@
+# markdownv2 python-telegram-bot specific
 joined = '{} joined group `{}`'
 not_joined = '{} is already in group `{}`'
 left = '{} left group `{}`'
@@ -5,28 +6,36 @@ not_left = '{} did not join group `{}` before'
 mention_failed = 'There are no users to mention'
 no_groups = 'There are no groups for this chat'
 
-
+# html python-telegram-bot specific
 start_text = """
-Hello there
-I am @everyone\_mention\_bot
-I am here to help you with mass notifies
+Hello!
+@everyone_mention_bot here.
 
-Please take a look at available commands
-`<group-name>` is not required, if not given, it is set to `default`
+<b>Description</b>:
+I <b>do not</b> have access to your messages! 
+I am here to help you with multiple user mentions.
 
-To join group:
-`/join <group-name>`
-for example: `/join games`
+<b>Usage</b>:
+Users that joined the group by <code>/join</code> command, can be mentioned after calling <code>/everyone</code> command.
 
-To leave group:
-`/leave <group-name>`
+<b>Commands</b>:
+<pre>/join {group-name}</pre>
+Joins (or creates if group did not exist before) group.
 
-To gather everyone attention use:
-`/everyone <group-name>`
+<pre>/leave {group-name}</pre>
+Leaves (or deletes if no other users are left) the group
 
-To see all available groups use:
-`/groups`
+<pre>/everyone {group-name}</pre>
+Mentions everyone that joined the group.
 
-To display all members in a group:
-`/silent <group-name>`
-"""
\ No newline at end of file
+<pre>/groups</pre>
+Show all created groups in this chat.
+
+<pre>/start</pre>
+Show start & help text
+
+<b>Please note</b>
+<code>{group-name}</code> is not required, <code>default</code> if not given.
+
+If your chat does have multiple bots <b>I might not receive your command</b> according to <a href="https://core.telegram.org/bots/faq#what-messages-will-my-bot-get">policy of bots with privacy mode enabled</a> - use <code>Inline Mode</code> to avoid this.
+"""
diff --git a/src/config/credentials.py b/src/config/credentials.py
deleted file mode 100755
index ef89331..0000000
--- a/src/config/credentials.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import os
-
-from dotenv import load_dotenv
-
-load_dotenv()
-
-BOT_TOKEN = os.environ['BOT_TOKEN']
-WEBHOOK_URL = os.environ['WEBHOOK_URL']
-PORT = os.environ['PORT']
-
-MONGODB_DATABASE = os.environ['MONGODB_DATABASE']
-MONGODB_USERNAME = os.environ['MONGODB_USERNAME']
-MONGODB_PASSWORD = os.environ['MONGODB_PASSWORD']
-MONGODB_HOSTNAME = os.environ['MONGODB_HOSTNAME']
-MONGODB_PORT = os.environ['MONGODB_PORT']
diff --git a/src/config/envs.py b/src/config/envs.py
new file mode 100755
index 0000000..76b45e7
--- /dev/null
+++ b/src/config/envs.py
@@ -0,0 +1,18 @@
+import os
+from urllib.parse import quote_plus
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+BOT_TOKEN = os.environ['BOT_TOKEN']
+WEBHOOK_URL = os.environ['WEBHOOK_URL']
+PORT = os.environ['PORT']
+
+MONGO_DATABASE = os.environ['MONGODB_DATABASE']
+MONGO_CONNECTION_STRING = "mongodb://%s:%s@%s:%s/%s?authSource=admin" % (
+    os.environ['MONGODB_USERNAME'], quote_plus(os.environ['MONGODB_PASSWORD']),
+    os.environ['MONGODB_HOSTNAME'], os.environ['MONGODB_PORT'], MONGO_DATABASE
+)
+
+BANNED_USERS = os.environ['BANNED_USERS'].split(',') or []
diff --git a/src/database/client.py b/src/database/client.py
index e3ef7be..37d9f4e 100755
--- a/src/database/client.py
+++ b/src/database/client.py
@@ -1,24 +1,25 @@
-from urllib.parse import quote_plus
-
-from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME,
-                                MONGODB_PASSWORD, MONGODB_PORT,
-                                MONGODB_USERNAME)
+from __future__ import annotations
 from pymongo import MongoClient
 from pymongo.database import Database
 
+from config.envs import MONGO_CONNECTION_STRING, MONGO_DATABASE
+from decorators.singleton import Singleton
 
-class Client():
+
+class Client(metaclass=Singleton):
     mongo_client: MongoClient
     database: Database
 
-    def __init__(self) -> None:
-        uri = "mongodb://%s:%s@%s:%s/%s?authSource=admin" % (
-            MONGODB_USERNAME, quote_plus(MONGODB_PASSWORD),
-            MONGODB_HOSTNAME, MONGODB_PORT, MONGODB_DATABASE
-        )
+    # allow only 10 minutes on idle, close connection after
+    max_idle_time: int = 10 * (60 * 1000)
 
-        self.mongo_client = MongoClient(uri)
-        self.database = self.mongo_client[MONGODB_DATABASE]
+    def __init__(self) -> None:
+        self.mongo_client = MongoClient(
+            MONGO_CONNECTION_STRING,
+            connect=False,
+            maxIdleTimeMS=self.max_idle_time
+        )
+        self.database = self.mongo_client[MONGO_DATABASE]
 
     def insert_one(self, collection: str, data: dict) -> None:
         self.database.get_collection(collection).insert_one(data)
@@ -29,10 +30,11 @@ class Client():
     def find_many(self, collection: str, filter: dict) -> dict:
         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(
             filter, 
-            { "$set" : data }
+            {"$set": data},
+            upsert=True
         )
 
     def aggregate(self, collection, pipeline: list):
diff --git a/src/decorators/singleton.py b/src/decorators/singleton.py
new file mode 100644
index 0000000..3886cfd
--- /dev/null
+++ b/src/decorators/singleton.py
@@ -0,0 +1,8 @@
+class Singleton(type):
+    _instances: dict = {}
+
+    def __call__(cls, *args, **kwargs):
+        if cls not in cls._instances:
+            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
+
+        return cls._instances[cls]
diff --git a/src/entity/chat.py b/src/entity/chat.py
new file mode 100644
index 0000000..3d70978
--- /dev/null
+++ b/src/entity/chat.py
@@ -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: []})
diff --git a/src/entity/group.py b/src/entity/group.py
deleted file mode 100644
index ff526f9..0000000
--- a/src/entity/group.py
+++ /dev/null
@@ -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'
diff --git a/src/entity/user.py b/src/entity/user.py
index 50a32a8..c8b58b4 100644
--- a/src/entity/user.py
+++ b/src/entity/user.py
@@ -1,40 +1,31 @@
 from __future__ import annotations
 
 from dataclasses import dataclass
-from typing import Iterable
+
+from bot.message.inboundMessage import InboundMessage
 
 
 @dataclass
-class User():
+class User:
     user_id: str
     username: str
-    chats: Iterable[str]
 
-    collection: str = 'users'
-    id_index: str = '_id'
-    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)
+    mongo_user_id_index: str = '_id'
+    mongo_username_index: str = 'username'
 
     def to_mongo_document(self) -> dict:
         return {
-            self.username_index: self.username,
-            self.chats_index: self.chats
+            self.mongo_user_id_index: self.user_id,
+            self.mongo_username_index: self.username
         }
     
     @staticmethod
     def from_mongo_document(mongo_document: dict) -> User:
         return User(
-            mongo_document[User.id_index],
-            mongo_document[User.username_index],
-            mongo_document[User.chats_index]
+            mongo_document[User.mongo_user_id_index],
+            mongo_document[User.mongo_username_index]
         )
+
+    @staticmethod
+    def from_inbound_message(inbound: InboundMessage) -> User:
+        return User(inbound.user_id, inbound.username)
diff --git a/src/exception/actionNotAllowedException.py b/src/exception/actionNotAllowedException.py
new file mode 100644
index 0000000..e20e369
--- /dev/null
+++ b/src/exception/actionNotAllowedException.py
@@ -0,0 +1,2 @@
+class ActionNotAllowedException(Exception):
+    pass
diff --git a/src/exception/invalidActionException.py b/src/exception/invalidActionException.py
new file mode 100644
index 0000000..e7df135
--- /dev/null
+++ b/src/exception/invalidActionException.py
@@ -0,0 +1,2 @@
+class InvalidActionException(Exception):
+    pass
\ No newline at end of file
diff --git a/src/logger.py b/src/logger.py
index 4d0bbba..0a19e01 100644
--- a/src/logger.py
+++ b/src/logger.py
@@ -3,7 +3,10 @@ from __future__ import annotations
 import logging
 import os
 
+from bot.message.inboundMessage import InboundMessage
 
+
+# noinspection SpellCheckingInspection
 class Logger:
     action_logger: str = 'action-logger'
     action_logger_file: str = '/var/log/bot/action.log'
@@ -11,9 +14,7 @@ class Logger:
     main_logger: str = 'main-logger'
     main_logger_file: str = '/var/log/bot/app.log'
 
-    formatter: str = 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.main_logger, self.main_logger_file, logging.ERROR)
 
@@ -23,21 +24,38 @@ class Logger:
             os.makedirs(directory)
 
         logger = logging.getLogger(logger_name)
+        logger.propagate = False
+
         file_handler = logging.FileHandler(log_file, mode='w')
-        file_handler.setFormatter(self.formatter)
+        formatter = logging.Formatter('%(asctime)s [%(levelname)s]: %(message)s', datefmt='%H:%M:%S %Y/%m/%d')
+        file_handler.setFormatter(formatter)
         stream_handler = logging.StreamHandler()
-        stream_handler.setFormatter(self.formatter)
+        stream_handler.setFormatter(formatter)
 
         logger.setLevel(level)
         logger.addHandler(file_handler)
         logger.addHandler(stream_handler)
 
     @staticmethod
-    def get_logger(logger_name) -> logging.Logger:
+    def register() -> None:
+        Logger()
+
+    @staticmethod
+    def get(logger_name: str) -> logging.Logger:
         return logging.getLogger(logger_name)
 
+    @staticmethod
     def info(message: str) -> None:
-        Logger.get_logger(Logger.action_logger).info(message)
+        Logger.get(Logger.action_logger).info(message)
 
+    @staticmethod
     def error(message: str) -> None:
-        Logger.get_logger(Logger.main_logger).error(message)
\ No newline at end of file
+        Logger.get(Logger.main_logger).error(message)
+
+    @staticmethod
+    def exception(exception: Exception) -> None:
+        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}({inbound.group_name})')
diff --git a/src/repository/abstractRepository.py b/src/repository/abstractRepository.py
new file mode 100644
index 0000000..320caed
--- /dev/null
+++ b/src/repository/abstractRepository.py
@@ -0,0 +1,9 @@
+from database.client import Client
+
+
+class AbstractRepository:
+    collection_name: str
+    database_client: Client
+
+    def __init__(self):
+        self.database_client = Client()
diff --git a/src/repository/chatRepository.py b/src/repository/chatRepository.py
new file mode 100644
index 0000000..c8786f3
--- /dev/null
+++ b/src/repository/chatRepository.py
@@ -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()
+        )
+
diff --git a/src/repository/groupRepository.py b/src/repository/groupRepository.py
deleted file mode 100644
index 82363df..0000000
--- a/src/repository/groupRepository.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import itertools
-import re
-from typing import Iterable
-
-from database.client import Client
-from entity.group import Group
-from entity.user import User
-
-
-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])
-            )
-
-        return result
diff --git a/src/repository/userRepository.py b/src/repository/userRepository.py
index d4371f6..60dff49 100644
--- a/src/repository/userRepository.py
+++ b/src/repository/userRepository.py
@@ -1,63 +1,43 @@
-from typing import Iterable, Optional
-
-from bot.message.messageData import MessageData
-from database.client import Client
+from bot.message.inboundMessage import InboundMessage
 from entity.user import User
 from exception.notFoundException import NotFoundException
+from repository.abstractRepository import AbstractRepository
 
 
-class UserRepository():
-    client: Client
+class UserRepository(AbstractRepository):
+    collection_name: str = 'users'
 
-    def __init__(self) -> None:
-        self.client = Client()
+    def __init__(self):
+        super().__init__()
 
-    def get_by_id(self, id: str) -> User:
-        user = self.client.find_one(
-            User.collection,
+    def provide(self, inbound: InboundMessage) -> User:
+        user = User.from_inbound_message(inbound)
+
+        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: id
+                User.mongo_user_id_index: user_id
             }
         )
 
         if not user:
-            raise NotFoundException(f'Could not find user with "{id}" id')
+            raise NotFoundException
+
+        return User.from_mongo_document(user)
 
-        return User(
-            user[User.id_index],
-            user[User.username_index],
-            user[User.chats_index]
-        )
-        
     def save(self, user: User) -> None:
-        self.client.update_one(
-            User.collection,
-            { User.id_index: user.user_id },
+        self.database_client.save(
+            self.collection_name,
+            {User.mongo_user_id_index: user.user_id},
             user.to_mongo_document()
         )
-
-    def save_by_message_data(self, data: MessageData) -> None:
-        self.client.insert_one(
-            User.collection, 
-            {
-                User.id_index: data.user_id,
-                User.username_index: data.username,
-                User.chats_index: [data.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))
-
-        return result
diff --git a/src/utils/messageBuilder.py b/src/utils/messageBuilder.py
new file mode 100644
index 0000000..4cfaf52
--- /dev/null
+++ b/src/utils/messageBuilder.py
@@ -0,0 +1,21 @@
+from typing import Iterable
+
+from prettytable import prettytable
+from telegram.utils.helpers import mention_markdown
+
+from entity.user import User
+
+
+class MessageBuilder:
+    @staticmethod
+    def group_message(groups: dict) -> str:
+        table = prettytable.PrettyTable(['Name', 'Members'])
+
+        for group in groups:
+            table.add_row([group, len(groups[group])])
+
+        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])
diff --git a/src/validator/accessValidator.py b/src/validator/accessValidator.py
new file mode 100644
index 0000000..135bce2
--- /dev/null
+++ b/src/validator/accessValidator.py
@@ -0,0 +1,10 @@
+from config.envs 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('User is banned')
diff --git a/src/validator/groupNameValidator.py b/src/validator/groupNameValidator.py
new file mode 100644
index 0000000..c910ae6
--- /dev/null
+++ b/src/validator/groupNameValidator.py
@@ -0,0 +1,17 @@
+import re
+
+from exception.invalidArgumentException import InvalidArgumentException
+
+
+class GroupNameValidator:
+    MAX_GROUP_NAME_LENGTH: int = 40
+
+    @staticmethod
+    def validate(group: str) -> None:
+        group = group.lower()
+
+        if len(group) > 0 and not re.match('^\w+$', group):
+            raise InvalidArgumentException(re.escape('Special characters are not allowed.'))
+
+        if len(group) > GroupNameValidator.MAX_GROUP_NAME_LENGTH:
+            raise InvalidArgumentException(re.escape(f'Group name length can not be greater than {GroupNameValidator.MAX_GROUP_NAME_LENGTH}.'))