0.1.0 Version : changed in to join and out to leave, folder structure and naming changes

This commit is contained in:
miloszowi 2021-10-06 19:44:03 +02:00
parent c588fa439e
commit 29c0fd84bb
28 changed files with 207 additions and 209 deletions

13
CHANGELOG.md Normal file
View File

@ -0,0 +1,13 @@
# Change Log
All notable changes to this project will be documented in this file.
## [0.1.0] - 06.10.2021
### Features
- `/join` command
- `/leave` command
- `/groups` command
- `/everyone` command
- `/start` command
- `/silent` command
- possibility to have multiple contexts for one chat
- docker setup & docker commands

View File

@ -1,57 +1,59 @@
# <p align="center"> [everyone-mention-telegram-bot](http://t.me/everyone_mention_bot)
<p align="center"> <img src="docs/logo.png" width="150"/>
<p align="center"> simple, but useful telegram bot to gather all of group members attention!
<!-- Icon made by https://www.freepik.com from https://www.flaticon.com/ -->
# Contents
* [Description](#description)
* [Getting started.](#getting-started)
* [Requirements](#requirements)
* [Installation](#installation)
* [Logs](#logs)
* [Env files](#env-files)
* [Commands](#commands)
* [`/in`](#in)
* [`/out`](#out)
* [`/join`](#join)
* [`/leave`](#leave)
* [`/everyone`](#everyone)
* [`/groups`](#groups)
* [`/silent`](#silent)
* [`/start`](#start)
## 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.
## Getting started
### Requirements
- `docker-compose` in version `1.25.0`
- `docker` in version `20.10.7`
### Installation
- copy the repository
```bash
git clone https://github.com/miloszowi/everyone-mention-telegram-bot.git
```
after that, you need to copy env files and fulfill it with correct values
- 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
```
and finally, you can run the bot by launching docker containers
- start the project (`-d` flag will run containers in detached mode)
```bash
docker-compose up -d
```
(`-d` flag will run containers in detached mode)
### Logs
You can use
```bash
docker/logs <container>
```
to check container logs
### Env files
`.env`
- `.env`
- `MONGODB_INTERNAL_PORT` - Mongodb internal port (should be the same as declared in `app.env`)
- `APP_INTERNAL_PORT` - App internal port (should be the same as declared in `app.env`)
- `APP_EXPOSED_PORT` - App exposed port (if you are not using any reverse proxy it should be also the same as declared in `app.env`)
`app.env`
- `app.env`
- `BOT_TOKEN` - your telegram bot token from [BotFather](https://telegram.me/BotFather)
- `WEBHOOK_URL` - url for telegram webhooks (withour the bot token)
- `PORT` - port used for initializing webhook & app
@ -61,61 +63,55 @@ to check container logs
- `MONGODB_HOSTNAME` - MongoDB host (default `database` - container name)
- `MONGODB_PORT` - MongoDB port (default `27017` - given in docker-compose configuration)
`database.env`
- `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
### `/in`
### `/join`
```
/in <group_name>
/join <group_name>
```
(blank `group_name` will assign you to `default` group)
Joins the group (and create if it did not exist before) given in message (`default` if not given)
Will sign you in for everyone-mentions.
![join command example](docs/join.png)
![in command example](docs/in_command.png)
If you have already opted-in before, alternative reply will be displayed.
![in command when someone already opted in example](docs/in_command_already_opted_in.png)
### `/out`
### `/leave`
```
/out <group_name>
/leave <group_name>
```
Will sign you off for everyone-mentions.
Leaves the group given in message (`default` if not given)
![out command example](docs/out_command.png)
If you haven't opted-in before, alternative reply will be displayed.
![out command when someone did not opt in example](docs/out_command_did_not_opt_in_before.png)
![leave command example](docs/leave.png)
### `/everyone`
```
/everyone <group_id>
```
Will mention everyone that opted-in for everyone-mentions separated by spaces.
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
![everybody command example](docs/everyone_command.png)
If there are no users that opted-in for mentioning, alternative reply will be displayed.
![everybone noone to mention example](docs/everyone_noone_to_mention.png)
![everyone command example](docs/everyone.png)
### `/groups`
Will display available groups for this chat as well with members count that opted-in for specific group
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 users that opted-in but without notyfing them.
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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/everyone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

BIN
docs/groups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

BIN
docs/join.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 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: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

BIN
docs/silent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -5,9 +5,9 @@ from telegram.ext.dispatcher import Dispatcher
from logger import Logger
from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
from handler import (groupsHandler, inHandler, mentionHandler, outHandler,
from bot.handler import (groupsHandler, joinHandler, mentionHandler, leaveHandler,
silentMentionHandler, startHandler)
from handler.abstractHandler import AbstractHandler
from bot.handler.abstractHandler import AbstractHandler
class App:

View File

@ -1,11 +1,11 @@
from abc import abstractmethod
from bot.message.messageData import MessageData
from logger import Logger
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.handler import Handler
from telegram.update import Update
from handler.vo.updateData import UpdateData
from telegram.utils.helpers import mention_markdown
class AbstractHandler:
@ -16,10 +16,13 @@ class AbstractHandler:
def handle(self, update: Update, context: CallbackContext) -> None: raise Exception('handle method is not implemented')
@abstractmethod
def log_action(self, update_data: UpdateData) -> None: raise Exception('log_action method is not implemented')
def log_action(self, message_data: MessageData) -> None: raise Exception('log_action method is not implemented')
def get_update_data(self, update: Update, context: CallbackContext) -> UpdateData:
return UpdateData.create_from_arguments(update, context)
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:

View File

@ -1,17 +1,16 @@
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 handler.vo.updateData import UpdateData
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 handler.abstractHandler import AbstractHandler
class GroupsHandler(AbstractHandler):
bot_handler: CommandHandler
@ -22,21 +21,21 @@ class GroupsHandler(AbstractHandler):
self.group_repository = GroupRepository()
def handle(self, update: Update, context: CallbackContext) -> None:
update_data = UpdateData.create_from_arguments(update, context, False)
message_data = MessageData.create_from_arguments(update, context, False)
groups = self.group_repository.get_by_chat_id(update_data.chat_id)
groups = self.group_repository.get_by_chat_id(message_data.chat_id)
if groups:
self.reply_html(update, self.build_groups_message(groups))
return self.log_action(update_data)
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, update_data: UpdateData) -> None:
Logger.info(f'User {update_data.username} called /groups for {update_data.chat_id}')
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'])

46
src/bot/handler/joinHandler.py Executable file
View File

@ -0,0 +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.commandhandler import CommandHandler
from telegram.update import Update
class JoinHandler(AbstractHandler):
bot_handler: CommandHandler
user_repository: UserRepository
def __init__(self) -> None:
self.bot_handler = CommandHandler('join', 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))
try:
user = self.user_repository.get_by_id(message_data.user_id)
if user.is_in_chat(message_data.chat_id):
return self.reply_markdown(update, self.interpolate_reply(not_joined, message_data))
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,46 +1,45 @@
from config.contents import opted_off, opted_off_failed
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 handler.vo.updateData import UpdateData
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 handler.abstractHandler import AbstractHandler
class OutHandler(AbstractHandler):
class LeaveHandler(AbstractHandler):
bot_handler: CommandHandler
user_repository: UserRepository
def __init__(self) -> None:
self.bot_handler = CommandHandler('out', self.handle)
self.bot_handler = CommandHandler('leave', self.handle)
self.user_repository = UserRepository()
def handle(self, update: Update, context: CallbackContext) -> None:
try:
update_data = self.get_update_data(update, context)
message_data = MessageData.create_from_arguments(update, context)
except InvalidArgumentException as e:
return self.reply_markdown(update, str(e))
try:
user = self.user_repository.get_by_id(update_data.user_id)
user = self.user_repository.get_by_id(message_data.user_id)
if not user.is_in_chat(update_data.chat_id):
if not user.is_in_chat(message_data.chat_id):
raise NotFoundException()
except NotFoundException:
return self.reply_markdown(update, opted_off_failed)
return self.reply_markdown(update, self.interpolate_reply(not_left, message_data))
user.remove_from_chat(update_data.chat_id)
user.remove_from_chat(message_data.chat_id)
self.user_repository.save(user)
self.reply_markdown(update, opted_off)
self.log_action(update_data)
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, update_data: UpdateData) -> None:
Logger.info(f'User {update_data.username} left {update_data.chat_id}')
def log_action(self, message_data: MessageData) -> None:
Logger.info(f'User {message_data.username} left {message_data.chat_id}')

View File

@ -1,17 +1,18 @@
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 handler.vo.updateData import UpdateData
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 handler.abstractHandler import AbstractHandler
class MentionHandler(AbstractHandler):
bot_handler: CommandHandler
@ -23,28 +24,24 @@ class MentionHandler(AbstractHandler):
def handle(self, update: Update, context: CallbackContext) -> None:
try:
update_data = self.get_update_data(update, context)
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(update_data.chat_id)
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(update_data)
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, update_data: UpdateData) -> None:
Logger.info(f'User {update_data.username} called /everyone for {update_data.chat_id}')
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:
result = ''
return ' '.join([mention_markdown(user.user_id, user.username) for user in users])
for user in users:
result += f'*[{user.username}](tg://user?id={user.user_id})* '
return result

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

@ -4,8 +4,8 @@ from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update
from handler.abstractHandler import AbstractHandler
from handler.vo.updateData import UpdateData
from bot.handler.abstractHandler import AbstractHandler
from bot.message.messageData import MessageData
class StartHandler(AbstractHandler):
@ -16,10 +16,10 @@ class StartHandler(AbstractHandler):
def handle(self, update: Update, context: CallbackContext) -> None:
self.reply_markdown(update, start_text)
self.log_action(UpdateData.create_from_arguments(update, context))
self.log_action(MessageData.create_from_arguments(update, context))
def get_bot_handler(self) -> CommandHandler:
return self.bot_handler
def log_action(self, update_data: UpdateData) -> None:
Logger.info(f'User {update_data.username} called /start for {update_data.chat_id}')
def log_action(self, message_data: MessageData) -> None:
Logger.info(f'User {message_data.username} called /start for {message_data.chat_id}')

View File

@ -1,25 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
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
from entity.group import Group
from exception.invalidArgumentException import InvalidArgumentException
@dataclass
class UpdateData():
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) -> UpdateData:
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()
@ -41,4 +42,4 @@ class UpdateData():
if not username:
username = names.get_first_name()
return UpdateData(user_id, chat_id, username)
return MessageData(user_id, chat_id, group_name, username)

View File

@ -1,28 +1,25 @@
import re
# These are MarkdownV2 python-telegram-bot specific
opted_in = re.escape('You have opted-in for everyone-mentions.')
opted_in_failed = re.escape('You already opted-in for everyone-mentions.')
opted_off = re.escape('You have opted-off for everyone-mentions.')
opted_off_failed = re.escape('You need to opt-in first before processing this command.')
mention_failed = re.escape('There are no users to mention.')
no_groups = re.escape('There are no groups for this chat.')
joined = '{} joined group `{}`'
not_joined = '{} is already in group `{}`'
left = '{} left group `{}`'
not_left = '{} did not join group `{}` before'
mention_failed = 'There are no users to mention'
no_groups = 'There are no groups for this chat'
start_text = re.escape("""
Hello there.
I am `@everyone_mention_bot`.
I am here to help you with mass notifies.
start_text = """
Hello there
I am @everyone\_mention\_bot
I am here to help you with mass notifies
Please take a look at available commands.
Parameter `<group-name>` is not required, if not given, I will assign you to `default` group.
Please take a look at available commands
`<group-name>` is not required, if not given, it is set to `default`
To opt-in for everyone-mentions use:
`/in <group-name>`
for example: `/in gaming`
To join group:
`/join <group-name>`
for example: `/join games`
To opt-off for everyone mentions use:
`/out <group-name>`
To leave group:
`/leave <group-name>`
To gather everyone attention use:
`/everyone <group-name>`
@ -30,8 +27,6 @@ To gather everyone attention use:
To see all available groups use:
`/groups`
To display all users that opted-in for everyone-mentions use:
To display all members in a group:
`/silent <group-name>`
In case questions regarding my usage please reach out to @miloszowi
""")
"""

View File

@ -1,46 +0,0 @@
from handler.vo.updateData import UpdateData
from logger import Logger
from config.contents import opted_in, opted_in_failed
from exception.invalidArgumentException import InvalidArgumentException
from exception.notFoundException import NotFoundException
from repository.userRepository import UserRepository
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.commandhandler import CommandHandler
from telegram.update import Update
from handler.abstractHandler import AbstractHandler
class InHandler(AbstractHandler):
bot_handler: CommandHandler
user_repository: UserRepository
def __init__(self) -> None:
self.bot_handler = CommandHandler('in', self.handle)
self.user_repository = UserRepository()
def handle(self, update: Update, context: CallbackContext) -> None:
try:
update_data = self.get_update_data(update, context)
except InvalidArgumentException as e:
return self.reply_markdown(update, str(e))
try:
user = self.user_repository.get_by_id(update_data.user_id)
if user.is_in_chat(update_data.chat_id):
return self.reply_markdown(update, opted_in_failed)
user.add_to_chat(update_data.chat_id)
self.user_repository.save(user)
except NotFoundException:
self.user_repository.save_by_update_data(update_data)
self.reply_markdown(update, opted_in)
self.log_action(update_data)
def get_bot_handler(self) -> CommandHandler:
return self.bot_handler
def log_action(self, update_data: UpdateData) -> None:
Logger.info(f'User {update_data.username} joined {update_data.chat_id}')

View File

@ -1,26 +0,0 @@
from typing import Iterable
from entity.user import User
from logger import Logger
from telegram.ext.commandhandler import CommandHandler
from handler.abstractHandler import AbstractHandler
from handler.mentionHandler import MentionHandler
from handler.vo.updateData import UpdateData
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:
result = ''
for user in users:
result += f'*{user.username}\({user.user_id}\)*\n'
return result
def log_action(self, update_data: UpdateData) -> None:
Logger.info(f'User {update_data.username} called /silent for {update_data.chat_id}')

View File

@ -1,9 +1,9 @@
from typing import Iterable, Optional
from bot.message.messageData import MessageData
from database.client import Client
from entity.user import User
from exception.notFoundException import NotFoundException
from handler.vo.updateData import UpdateData
class UserRepository():
@ -36,7 +36,7 @@ class UserRepository():
user.to_mongo_document()
)
def save_by_update_data(self, data: UpdateData) -> None:
def save_by_message_data(self, data: MessageData) -> None:
self.client.insert_one(
User.collection,
{