Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

54 changed files with 509 additions and 763 deletions

View File

@ -1,32 +1,6 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.3.2] - 28.02.2023
### Changed
- fixed bug with missing '+' in replier
## [0.3.1] - 28.02.2023
### Changed
- fixed markdown replier to respect restricted characters provided in the [api docs](https://core.telegram.org/bots/api#markdownv2-style)
## [0.3.0] - 12.11.2021
### Added
- Dynamic mentioning by `@` character
### Changed
- `start` text
- Group name validation - those are forbidden now - `all`, `channel`, `chat`, `everyone`, `group`, `here`
### Deleted
- mentioning user that performed `/everyone` or dynamic mention in bot response
## [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 ## [0.1.0] - 06.10.2021
### Features ### Features
- `/join` command - `/join` command

163
README.md
View File

@ -5,121 +5,23 @@
# Contents # Contents
* [Description](#description) * [Description](#description)
* [Usage](#usage)
* [Dynamic Mentioning](#dynamic-mentioning)
* [Commands](#commands)
* [`/join`](#join)
* [`/leave`](#leave)
* [`/everyone`](#everyone)
* [`/groups`](#groups)
* [`/start`](#start)
* [Example command flow](#example-command-flow)
* [Inline Mode Usage](#inline-mode-usage)
* [Getting started.](#getting-started) * [Getting started.](#getting-started)
* [Requirements](#requirements) * [Requirements](#requirements)
* [Installation](#installation) * [Installation](#installation)
* [Logs](#logs) * [Logs](#logs)
* [Env files](#env-files) * [Env files](#env-files)
* [Commands](#commands)
* [`/join`](#join)
* [`/leave`](#leave)
* [`/everyone`](#everyone)
* [`/groups`](#groups)
* [`/silent`](#silent)
* [`/start`](#start)
## Description ## Description
Everyone Mention Bot is simple, but useful telegram bot to gather group members attention. Everyone Mention Bot is simple, but useful telegram bot to gather group members attention.
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. 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.
## Usage
First, users need to join the group to let mentioning them, to do that, they simply need to join specific group.
It can be done in 2 ways:
- Command [`/join`](#join)
- [Inline Mode](#inline-mode-usage)
Users that have joined the group can be mentioned in 3 ways:
- [Dynaminc Mentioning](#dynamic-mentioning) by `@`, for example `@everyone`
- Command [`/everyone`](#everyone)
- [Inline Mode](#inline-mode-usage)
To leave the group use one of the two ways:
- Command [`/leave`](#leave)
- [Inline Mode](#inline-mode-usage)
To display available groups:
- Command [`/groups`](#groups)
## Dynamic mentioning
You can use `@` character (as you would mention a user) to mention specific group.
All the below will mention users from `default` group.
`@all`, `@channel`, `@chat`, `@everyone`, `@group`, `@here`.
If you did create a group named `gaming`, you can simply use `@gaming` in your text to mention them all.
## 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 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 ## Getting started
### Requirements ### Requirements
- `docker-compose` in version `1.25.0` - `docker-compose` in version `1.25.0`
@ -133,8 +35,8 @@ git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git
- copy environment files and fulfill empty values - copy environment files and fulfill empty values
```bash ```bash
cp .env.local .env cp .env.local .env
cp docker/config/app.env.local docker/config/app.env cp docker/config/app.dist.env docker/config/app.env
cp docker/config/database.env.local docker/config/app.env cp docker/config/database.dist.env docker/config/app.env
``` ```
- start the project (`-d` flag will run containers in detached mode) - start the project (`-d` flag will run containers in detached mode)
```bash ```bash
@ -160,7 +62,6 @@ docker/logs <container>
- `MONGODB_PASSWORD` - MongoDB password - `MONGODB_PASSWORD` - MongoDB password
- `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name) - `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name)
- `MONGODB_PORT` - MongoDB port (default `27017` - given in docker-compose configuration) - `MONGODB_PORT` - MongoDB port (default `27017` - given in docker-compose configuration)
- `BANNED_USERS` - user ids separated by comma that are not allowed to use the bot
- `database.env` - `database.env`
- `MONGO_INITDB_ROOT_USERNAME` - conf from `app.env` - `MONGO_INITDB_ROOT_USERNAME` - conf from `app.env`
@ -168,3 +69,49 @@ docker/logs <container>
- `MONGO_INITDB_DATABASE` - conf from `app.env` - `MONGO_INITDB_DATABASE` - conf from `app.env`
- `MONGODB_DATA_DIR` - directory to store MongoDB documents (inside a container) - `MONGODB_DATA_DIR` - directory to store MongoDB documents (inside a container)
- `MONDODB_LOG_DIR` - path to logs storage - `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)

View File

@ -22,6 +22,7 @@ services:
- ./docker/config/app.env - ./docker/config/app.env
volumes: volumes:
- ./src:/src - ./src:/src
- ./logs:/var/log/bot
ports: ports:
- $APP_EXPOSED_PORT:$APP_INTERNAL_PORT - $APP_EXPOSED_PORT:$APP_INTERNAL_PORT
depends_on: depends_on:

View File

@ -7,5 +7,3 @@ MONGODB_USERNAME=
MONGODB_PASSWORD= MONGODB_PASSWORD=
MONGODB_HOSTNAME=localhost MONGODB_HOSTNAME=localhost
MONGODB_PORT=27017 MONGODB_PORT=27017
BANNED_USERS=

View File

@ -1,8 +0,0 @@
#!/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

BIN
docs/everyone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/join.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/leave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/silent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

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

View File

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

View File

@ -1,40 +1,37 @@
from abc import abstractmethod from abc import abstractmethod
from telegram.ext import Handler from bot.message.messageData import MessageData
from telegram.ext.callbackcontext import CallbackContext
from telegram.update import Update
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 exception.notFoundException import NotFoundException
from logger import Logger from logger import Logger
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.handler import Handler
from telegram.update import Update
from telegram.utils.helpers import mention_markdown
class AbstractHandler: class AbstractHandler:
bot_handler: Handler @abstractmethod
inbound: InboundMessage def get_bot_handler(self) -> Handler: raise Exception('get_bot_handler method is not implemented')
action: str
@abstractmethod @abstractmethod
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented')
raise Exception('handle method is not implemented')
def wrap(self, update: Update, context: CallbackContext) -> None: @abstractmethod
def log_action(self, message_data: MessageData) -> None: raise Exception('log_action method is not implemented')
def interpolate_reply(self, reply: str, message_data: MessageData):
return reply.format(
mention_markdown(message_data.user_id, message_data.username),
message_data.group_name
)
def reply_markdown(self, update: Update, message: str) -> None:
try: try:
group_specific = self.is_group_specific() update.effective_message.reply_markdown_v2(text=message)
except Exception as err:
Logger.error(str(err))
self.inbound = InboundMessage.create(update, context, group_specific) def reply_html(self, update: Update, html: str) -> None:
self.handle(update, context) try:
Logger.action(self.inbound, self.action) update.effective_message.reply_html(text=html)
except (InvalidActionException, InvalidArgumentException, ActionNotAllowedException) as e: except Exception as err:
Replier.markdown(update, str(e)) Logger.error(str(err))
except NotFoundException:
pass # probably just mentioning user
except Exception as e:
Logger.exception(e)
def is_group_specific(self) -> bool:
return True

View File

@ -1,31 +0,0 @@
import re
from telegram.ext import Filters, MessageHandler
from telegram.ext.callbackcontext import CallbackContext
from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler
from bot.message.replier import Replier
from repository.chatRepository import ChatRepository
from utils.messageBuilder import MessageBuilder
class DynamicMentionHandler(AbstractHandler):
bot_handler: MessageHandler
chat_repository: ChatRepository
action: str = 'dynamic-mention'
def __init__(self) -> None:
self.bot_handler = MessageHandler(
Filters.regex(re.compile(r'@[^ ]')),
self.wrap
)
self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None:
if hasattr(update, 'message_reaction'):
return
users = self.chat_repository.get_users_for_group(self.inbound)
Replier.markdown(update, MessageBuilder.mention_message(users))

View File

@ -1,29 +0,0 @@
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 utils.messageBuilder import MessageBuilder
class EveryoneHandler(AbstractHandler):
bot_handler: CommandHandler
chat_repository: ChatRepository
action: str = 'everyone'
def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap)
self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None:
try:
users = self.chat_repository.get_users_for_group(self.inbound)
Replier.markdown(update, MessageBuilder.mention_message(users))
except NotFoundException as e:
raise InvalidActionException(mention_failed) from e

View File

@ -1,34 +1,45 @@
from typing import Iterable
import prettytable as pt
from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from config.contents import no_groups
from entity.group import Group
from logger import Logger
from repository.groupRepository import GroupRepository
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler
from bot.message.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): class GroupsHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
chat_repository: ChatRepository group_repository: GroupRepository
action: str = 'groups'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler('groups', self.handle)
self.chat_repository = ChatRepository() self.group_repository = GroupRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
try: message_data = MessageData.create_from_arguments(update, context, False)
chat = self.chat_repository.get(self.inbound.chat_id)
if not chat.groups:
raise NotFoundException
Replier.html(update, MessageBuilder.group_message(chat.groups)) groups = self.group_repository.get_by_chat_id(message_data.chat_id)
except NotFoundException:
raise InvalidActionException(no_groups)
def is_group_specific(self) -> bool: if groups:
return False 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>'

View File

@ -1,51 +0,0 @@
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)

View File

@ -1,34 +1,46 @@
from telegram.utils.helpers import mention_markdown
from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from config.contents import joined, not_joined
from exception.invalidArgumentException import InvalidArgumentException
from exception.notFoundException import NotFoundException
from logger import Logger
from repository.userRepository import UserRepository
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler
from bot.message.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): class JoinHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
user_repository: UserRepository user_repository: UserRepository
action: str = 'join'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler('join', self.handle)
self.user_repository = UserRepository() self.user_repository = UserRepository()
self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
user = self.user_repository.provide(self.inbound) try:
chat = self.chat_repository.provide(self.inbound) message_data = MessageData.create_from_arguments(update, context)
users = chat.groups.get(self.inbound.group_name) except InvalidArgumentException as e:
return self.reply_markdown(update, str(e))
if user.user_id in users: try:
raise InvalidActionException(Replier.interpolate(not_joined, self.inbound)) user = self.user_repository.get_by_id(message_data.user_id)
users.append(user.user_id) if user.is_in_chat(message_data.chat_id):
self.chat_repository.save(chat) return self.reply_markdown(update, self.interpolate_reply(not_joined, message_data))
Replier.markdown(update, Replier.interpolate(joined, self.inbound)) 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}')

View File

@ -1,38 +1,45 @@
from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from config.contents import left, not_left
from exception.invalidArgumentException import InvalidArgumentException
from exception.notFoundException import NotFoundException
from logger import Logger
from repository.userRepository import UserRepository
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler
from bot.message.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): class LeaveHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
user_repository: UserRepository user_repository: UserRepository
chat_repository: ChatRepository
action: str = 'leave'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler('leave', self.handle)
self.user_repository = UserRepository() self.user_repository = UserRepository()
self.chat_repository = ChatRepository()
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
user = self.user_repository.provide(self.inbound) try:
chat = self.chat_repository.provide(self.inbound) message_data = MessageData.create_from_arguments(update, context)
group = chat.groups.get(self.inbound.group_name) except InvalidArgumentException as e:
return self.reply_markdown(update, str(e))
if user.user_id not in group: try:
raise InvalidActionException(Replier.interpolate(not_left, self.inbound)) user = self.user_repository.get_by_id(message_data.user_id)
group.remove(user.user_id) if not user.is_in_chat(message_data.chat_id):
if not group: raise NotFoundException()
chat.groups.pop(self.inbound.group_name) except NotFoundException:
return self.reply_markdown(update, self.interpolate_reply(not_left, message_data))
self.chat_repository.save(chat) user.remove_from_chat(message_data.chat_id)
self.user_repository.save(user)
Replier.markdown(update, Replier.interpolate(left, self.inbound)) self.reply_markdown(update, self.interpolate_reply(left, message_data))
self.log_action(message_data)
def get_bot_handler(self) -> CommandHandler:
return self.bot_handler
def log_action(self, message_data: MessageData) -> None:
Logger.info(f'User {message_data.username} left {message_data.chat_id}')

View File

@ -0,0 +1,47 @@
from typing import Iterable
from telegram.utils.helpers import mention_markdown
from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
from config.contents import mention_failed
from entity.user import User
from exception.invalidArgumentException import InvalidArgumentException
from logger import Logger
from repository.userRepository import UserRepository
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update
class MentionHandler(AbstractHandler):
bot_handler: CommandHandler
user_repository: UserRepository
def __init__(self) -> None:
self.bot_handler = CommandHandler('everyone', self.handle)
self.user_repository = UserRepository()
def handle(self, update: Update, context: CallbackContext) -> None:
try:
message_data = MessageData.create_from_arguments(update, context)
except InvalidArgumentException as e:
return self.reply_markdown(update, str(e))
users = self.user_repository.get_all_for_chat(message_data.chat_id)
if users:
self.reply_markdown(update, self.build_mention_message(users))
return self.log_action(message_data)
self.reply_markdown(update, mention_failed)
def get_bot_handler(self) -> CommandHandler:
return self.bot_handler
def log_action(self, message_data: MessageData) -> None:
Logger.info(f'User {message_data.username} called /everyone for {message_data.chat_id}')
def build_mention_message(self, users: Iterable[User]) -> str:
return ' '.join([mention_markdown(user.user_id, user.username) for user in users])

View File

@ -0,0 +1,21 @@
from typing import Iterable
from entity.user import User
from logger import Logger
from telegram.ext.commandhandler import CommandHandler
from bot.handler.abstractHandler import AbstractHandler
from bot.handler.mentionHandler import MentionHandler
from bot.message.messageData import MessageData
class MentionHandler(MentionHandler, AbstractHandler):
def __init__(self) -> None:
super().__init__()
self.bot_handler = CommandHandler('silent', self.handle)
def build_mention_message(self, users: Iterable[User]) -> str:
return ' '.join([user.username for user in users])
def log_action(self, message_data: MessageData) -> None:
Logger.info(f'User {message_data.username} called /silent for {message_data.chat_id}')

View File

@ -1,35 +1,25 @@
from telegram import InlineKeyboardButton, InlineKeyboardMarkup from config.contents import start_text
from logger import Logger
from telegram.ext.callbackcontext import CallbackContext from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update from telegram.update import Update
from bot.handler.abstractHandler import AbstractHandler from bot.handler.abstractHandler import AbstractHandler
from bot.message.replier import Replier from bot.message.messageData import MessageData
from config.contents import start_text
from logger import Logger
class StartHandler(AbstractHandler): class StartHandler(AbstractHandler):
bot_handler: CommandHandler bot_handler: CommandHandler
action: str = 'start'
def __init__(self) -> None: def __init__(self) -> None:
self.bot_handler = CommandHandler(self.action, self.wrap) self.bot_handler = CommandHandler('start', self.handle)
def handle(self, update: Update, context: CallbackContext) -> None: def handle(self, update: Update, context: CallbackContext) -> None:
markup = InlineKeyboardMarkup( self.reply_markdown(update, start_text)
[ self.log_action(MessageData.create_from_arguments(update, context))
[
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')
]
]
)
Replier.html(update, start_text, markup) def get_bot_handler(self) -> CommandHandler:
return self.bot_handler
def is_group_specific(self) -> bool: def log_action(self, message_data: MessageData) -> None:
return False Logger.info(f'User {message_data.username} called /start for {message_data.chat_id}')

View File

@ -1,53 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import names
import re
from telegram.ext.callbackcontext import CallbackContext
from telegram.update import Update
from exception.invalidActionException import InvalidActionException
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)
message_content = update.edited_message.text if update.edited_message else update.message.text
message_content = message_content.replace("\n", " ")
chat_id = str(update.effective_chat.id)
group_name = InboundMessage.default_group
# done upon resolving a command action
if context.args and context.args[0] and group_specific:
group_name = str(context.args[0]).lower()
GroupNameValidator.validate(group_name)
# done upon resolving a message handler action
if '@' in message_content and '@everyone_mention_bot' not in message_content:
searched_message_part = [part for part in message_content.split(' ') if '@' in part][0]
group_name = re.sub(r'\W+', '', searched_message_part).lower()
if group_name in GroupNameValidator.FORBIDDEN_GROUP_NAMES:
group_name = InboundMessage.default_group
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)

View File

@ -0,0 +1,45 @@
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)

View File

@ -1,37 +0,0 @@
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):
formatted = content.format(
mention_markdown(inbound_message.user_id, inbound_message.username),
inbound_message.group_name
)
telegramRestrictionCharacters = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for character in telegramRestrictionCharacters:
formatted.replace(character, r'\{character}')
return formatted
@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("replier.markdown 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("replier.html error: " + str(err))

View File

@ -1,4 +1,3 @@
# markdownv2 python-telegram-bot specific
joined = '{} joined group `{}`' joined = '{} joined group `{}`'
not_joined = '{} is already in group `{}`' not_joined = '{} is already in group `{}`'
left = '{} left group `{}`' left = '{} left group `{}`'
@ -6,37 +5,28 @@ not_left = '{} did not join group `{}` before'
mention_failed = 'There are no users to mention' mention_failed = 'There are no users to mention'
no_groups = 'There are no groups for this chat' no_groups = 'There are no groups for this chat'
# html python-telegram-bot specific
start_text = """ start_text = """
Hello! Hello there
@everyone_mention_bot here. I am @everyone\_mention\_bot
I am here to help you with multiple user mentions. I am here to help you with mass notifies
<b>Usage</b>: Please take a look at available commands
Users that joined the group by <code>/join</code> command, `<group-name>` is not required, if not given, it is set to `default`
can be mentioned after typing one of those in your message:
<code>@all</code>, <code>@channel</code>, <code>@chat</code>, <code>@everyone</code>, <code>@group</code> or <code>@here</code>.
If you did create a group named <code>gaming</code>, simply use <code>@gaming</code> to call users from that group. To join group:
`/join <group-name>`
for example: `/join games`
You can also use <code>/everyone</code> command. To leave group:
`/leave <group-name>`
<b>Commands</b>: To gather everyone attention use:
<pre>/join {group-name}</pre> `/everyone <group-name>`
Joins (or creates if group did not exist before) group.
<pre>/leave {group-name}</pre> To see all available groups use:
Leaves (or deletes if no other users are left) the group `/groups`
<pre>/everyone {group-name}</pre> To display all members in a group:
Mentions everyone that joined the group. `/silent <group-name>`
<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.
""" """

15
src/config/credentials.py Executable file
View File

@ -0,0 +1,15 @@
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']

View File

@ -1,18 +0,0 @@
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 []

View File

@ -1,25 +1,24 @@
from __future__ import annotations from urllib.parse import quote_plus
from config.credentials import (MONGODB_DATABASE, MONGODB_HOSTNAME,
MONGODB_PASSWORD, MONGODB_PORT,
MONGODB_USERNAME)
from pymongo import MongoClient from pymongo import MongoClient
from pymongo.database import Database 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 mongo_client: MongoClient
database: Database database: Database
# allow only 10 minutes on idle, close connection after
max_idle_time: int = 10 * (60 * 1000)
def __init__(self) -> None: def __init__(self) -> None:
self.mongo_client = MongoClient( uri = "mongodb://%s:%s@%s:%s/%s?authSource=admin" % (
MONGO_CONNECTION_STRING, MONGODB_USERNAME, quote_plus(MONGODB_PASSWORD),
connect=False, MONGODB_HOSTNAME, MONGODB_PORT, MONGODB_DATABASE
maxIdleTimeMS=self.max_idle_time
) )
self.database = self.mongo_client[MONGO_DATABASE]
self.mongo_client = MongoClient(uri)
self.database = self.mongo_client[MONGODB_DATABASE]
def insert_one(self, collection: str, data: dict) -> None: def insert_one(self, collection: str, data: dict) -> None:
self.database.get_collection(collection).insert_one(data) self.database.get_collection(collection).insert_one(data)
@ -30,11 +29,10 @@ class Client(metaclass=Singleton):
def find_many(self, collection: str, filter: dict) -> dict: def find_many(self, collection: str, filter: dict) -> dict:
return self.database.get_collection(collection).find(filter) return self.database.get_collection(collection).find(filter)
def save(self, collection: str, filter: dict, data: dict) -> None: def update_one(self, collection: str, filter: dict, data: dict) -> None:
self.database.get_collection(collection).update_one( self.database.get_collection(collection).update_one(
filter, filter,
{"$set": data}, { "$set" : data }
upsert=True
) )
def aggregate(self, collection, pipeline: list): def aggregate(self, collection, pipeline: list):

View File

@ -1,8 +0,0 @@
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]

View File

@ -1,32 +0,0 @@
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: []})

12
src/entity/group.py Normal file
View File

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

View File

@ -1,31 +1,40 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterable
from bot.message.inboundMessage import InboundMessage
@dataclass @dataclass
class User: class User():
user_id: str user_id: str
username: str username: str
chats: Iterable[str]
mongo_user_id_index: str = '_id' collection: str = 'users'
mongo_username_index: str = 'username' 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)
def to_mongo_document(self) -> dict: def to_mongo_document(self) -> dict:
return { return {
self.mongo_user_id_index: self.user_id, self.username_index: self.username,
self.mongo_username_index: self.username self.chats_index: self.chats
} }
@staticmethod @staticmethod
def from_mongo_document(mongo_document: dict) -> User: def from_mongo_document(mongo_document: dict) -> User:
return User( return User(
mongo_document[User.mongo_user_id_index], mongo_document[User.id_index],
mongo_document[User.mongo_username_index] mongo_document[User.username_index],
mongo_document[User.chats_index]
) )
@staticmethod
def from_inbound_message(inbound: InboundMessage) -> User:
return User(inbound.user_id, inbound.username)

View File

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

View File

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

View File

@ -3,10 +3,7 @@ from __future__ import annotations
import logging import logging
import os import os
from bot.message.inboundMessage import InboundMessage
# noinspection SpellCheckingInspection
class Logger: class Logger:
action_logger: str = 'action-logger' action_logger: str = 'action-logger'
action_logger_file: str = '/var/log/bot/action.log' action_logger_file: str = '/var/log/bot/action.log'
@ -14,7 +11,9 @@ class Logger:
main_logger: str = 'main-logger' main_logger: str = 'main-logger'
main_logger_file: str = '/var/log/bot/app.log' main_logger_file: str = '/var/log/bot/app.log'
def __init__(self): formatter: str = logging.Formatter('%(asctime)s[%(levelname)s]: %(message)s')
def setup(self) -> None:
self.configure(self.action_logger, self.action_logger_file, logging.INFO) self.configure(self.action_logger, self.action_logger_file, logging.INFO)
self.configure(self.main_logger, self.main_logger_file, logging.ERROR) self.configure(self.main_logger, self.main_logger_file, logging.ERROR)
@ -24,38 +23,21 @@ class Logger:
os.makedirs(directory) os.makedirs(directory)
logger = logging.getLogger(logger_name) logger = logging.getLogger(logger_name)
logger.propagate = False
file_handler = logging.FileHandler(log_file, mode='w') file_handler = logging.FileHandler(log_file, mode='w')
formatter = logging.Formatter('%(asctime)s [%(levelname)s]: %(message)s', datefmt='%H:%M:%S %Y/%m/%d') file_handler.setFormatter(self.formatter)
file_handler.setFormatter(formatter)
stream_handler = logging.StreamHandler() stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter) stream_handler.setFormatter(self.formatter)
logger.setLevel(level) logger.setLevel(level)
logger.addHandler(file_handler) logger.addHandler(file_handler)
logger.addHandler(stream_handler) logger.addHandler(stream_handler)
@staticmethod @staticmethod
def register() -> None: def get_logger(logger_name) -> logging.Logger:
Logger()
@staticmethod
def get(logger_name: str) -> logging.Logger:
return logging.getLogger(logger_name) return logging.getLogger(logger_name)
@staticmethod
def info(message: str) -> None: def info(message: str) -> None:
Logger.get(Logger.action_logger).info(message) Logger.get_logger(Logger.action_logger).info(message)
@staticmethod
def error(message: str) -> None: def error(message: str) -> None:
Logger.get(Logger.main_logger).error(message) Logger.get_logger(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})')

View File

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

View File

@ -1,60 +0,0 @@
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, inbound: InboundMessage) -> Iterable[User]:
chat = self.get(inbound.chat_id)
if not chat.groups.get(inbound.group_name):
raise NotFoundException
users = [self.user_repository.get(user_id) for user_id in chat.groups.get(inbound.group_name) if user_id != inbound.user_id]
if not users:
raise NotFoundException
return users
def save(self, chat: Chat) -> None:
self.database_client.save(
self.collection_name,
{Chat.mongo_chat_id_index: chat.chat_id},
chat.to_mongo_document()
)

View File

@ -0,0 +1,53 @@
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

View File

@ -1,43 +1,63 @@
from bot.message.inboundMessage import InboundMessage from typing import Iterable, Optional
from bot.message.messageData import MessageData
from database.client import Client
from entity.user import User from entity.user import User
from exception.notFoundException import NotFoundException from exception.notFoundException import NotFoundException
from repository.abstractRepository import AbstractRepository
class UserRepository(AbstractRepository): class UserRepository():
collection_name: str = 'users' client: Client
def __init__(self): def __init__(self) -> None:
super().__init__() self.client = Client()
def provide(self, inbound: InboundMessage) -> User: def get_by_id(self, id: str) -> User:
user = User.from_inbound_message(inbound) user = self.client.find_one(
User.collection,
try:
entity = self.get(user.user_id)
if entity != user:
self.save(user)
except NotFoundException:
self.save(user)
return user
def get(self, user_id: str) -> User:
user = self.database_client.find_one(
self.collection_name,
{ {
User.mongo_user_id_index: user_id User.id_index: id
} }
) )
if not user: if not user:
raise NotFoundException raise NotFoundException(f'Could not find user with "{id}" id')
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: def save(self, user: User) -> None:
self.database_client.save( self.client.update_one(
self.collection_name, User.collection,
{User.mongo_user_id_index: user.user_id}, { User.id_index: user.user_id },
user.to_mongo_document() user.to_mongo_document()
) )
def save_by_message_data(self, data: MessageData) -> None:
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

View File

@ -1,21 +0,0 @@
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])

View File

@ -1,10 +0,0 @@
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')

View File

@ -1,21 +0,0 @@
import re
from exception.invalidArgumentException import InvalidArgumentException
class GroupNameValidator:
MAX_GROUP_NAME_LENGTH: int = 40
FORBIDDEN_GROUP_NAMES = ['all', 'channel', 'chat', 'everyone', 'group', 'here']
@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}.'))
if group in GroupNameValidator.FORBIDDEN_GROUP_NAMES:
raise InvalidArgumentException(re.escape(f'This group name is forbidden, please try with other name.'))