mirror of
https://github.com/miloszowi/everyone-mention-telegram-bot.git
synced 2025-06-03 16:14:09 +00:00
added logging, added /start command
This commit is contained in:
parent
ff1d037be9
commit
c588fa439e
3
.gitignore
vendored
3
.gitignore
vendored
@ -129,3 +129,6 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs/
|
@ -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:
|
||||||
|
18
src/app.py
18
src/app.py
@ -1,19 +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 logger import Logger
|
||||||
from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
|
from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
|
||||||
|
from handler import (groupsHandler, inHandler, mentionHandler, outHandler,
|
||||||
|
silentMentionHandler, startHandler)
|
||||||
from handler.abstractHandler import AbstractHandler
|
from handler.abstractHandler import AbstractHandler
|
||||||
from handler import (inHandler, mentionHandler, outHandler, silentMentionHandler, groupsHandler)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
self.setup_logging()
|
||||||
self.register_handlers()
|
self.register_handlers()
|
||||||
self.register_webhook()
|
self.register_webhook()
|
||||||
|
|
||||||
@ -33,6 +41,14 @@ class App:
|
|||||||
webhook_url="/".join([WEBHOOK_URL, BOT_TOKEN])
|
webhook_url="/".join([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()
|
||||||
|
|
||||||
|
@ -7,3 +7,31 @@ 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.')
|
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.')
|
mention_failed = re.escape('There are no users to mention.')
|
||||||
no_groups = re.escape('There are no groups for this chat.')
|
no_groups = re.escape('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.
|
||||||
|
|
||||||
|
Please take a look at available commands.
|
||||||
|
Parameter `<group-name>` is not required, if not given, I will assign you to `default` group.
|
||||||
|
|
||||||
|
To opt-in for everyone-mentions use:
|
||||||
|
`/in <group-name>`
|
||||||
|
for example: `/in gaming`
|
||||||
|
|
||||||
|
To opt-off for everyone mentions use:
|
||||||
|
`/out <group-name>`
|
||||||
|
|
||||||
|
To gather everyone attention use:
|
||||||
|
`/everyone <group-name>`
|
||||||
|
|
||||||
|
To see all available groups use:
|
||||||
|
`/groups`
|
||||||
|
|
||||||
|
To display all users that opted-in for everyone-mentions use:
|
||||||
|
`/silent <group-name>`
|
||||||
|
|
||||||
|
In case questions regarding my usage please reach out to @miloszowi
|
||||||
|
""")
|
@ -1,5 +1,6 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from logger import Logger
|
||||||
from telegram.ext.callbackcontext import CallbackContext
|
from telegram.ext.callbackcontext import CallbackContext
|
||||||
from telegram.ext.handler import Handler
|
from telegram.ext.handler import Handler
|
||||||
from telegram.update import Update
|
from telegram.update import Update
|
||||||
@ -14,14 +15,20 @@ class AbstractHandler:
|
|||||||
@abstractmethod
|
@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, update_data: UpdateData) -> None: raise Exception('log_action method is not implemented')
|
||||||
|
|
||||||
def get_update_data(self, update: Update, context: CallbackContext) -> UpdateData:
|
def get_update_data(self, update: Update, context: CallbackContext) -> UpdateData:
|
||||||
return UpdateData.create_from_arguments(update, context)
|
return UpdateData.create_from_arguments(update, context)
|
||||||
|
|
||||||
def reply(self, update: Update, text: str) -> None:
|
|
||||||
update.effective_message.reply(text=text)
|
|
||||||
|
|
||||||
def reply_markdown(self, update: Update, message: str) -> None:
|
def reply_markdown(self, update: Update, message: str) -> None:
|
||||||
update.effective_message.reply_markdown_v2(text=message)
|
try:
|
||||||
|
update.effective_message.reply_markdown_v2(text=message)
|
||||||
|
except Exception as err:
|
||||||
|
Logger.error(str(err))
|
||||||
|
|
||||||
def reply_html(self, update: Update, html: str) -> None:
|
def reply_html(self, update: Update, html: str) -> None:
|
||||||
update.effective_message.reply_html(text=html)
|
try:
|
||||||
|
update.effective_message.reply_html(text=html)
|
||||||
|
except Exception as err:
|
||||||
|
Logger.error(str(err))
|
||||||
|
@ -3,6 +3,8 @@ from typing import Iterable
|
|||||||
import prettytable as pt
|
import prettytable as pt
|
||||||
from config.contents import no_groups
|
from config.contents import no_groups
|
||||||
from entity.group import Group
|
from entity.group import Group
|
||||||
|
from handler.vo.updateData import UpdateData
|
||||||
|
from logger import Logger
|
||||||
from repository.groupRepository import GroupRepository
|
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
|
||||||
@ -20,18 +22,22 @@ class GroupsHandler(AbstractHandler):
|
|||||||
self.group_repository = GroupRepository()
|
self.group_repository = GroupRepository()
|
||||||
|
|
||||||
def handle(self, update: Update, context: CallbackContext) -> None:
|
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||||
real_chat_id = str(update.effective_chat.id)
|
update_data = UpdateData.create_from_arguments(update, context, False)
|
||||||
|
|
||||||
groups = self.group_repository.get_by_chat_id(real_chat_id)
|
groups = self.group_repository.get_by_chat_id(update_data.chat_id)
|
||||||
|
|
||||||
if groups:
|
if groups:
|
||||||
return self.reply_html(update, self.build_groups_message(groups))
|
self.reply_html(update, self.build_groups_message(groups))
|
||||||
|
return self.log_action(update_data)
|
||||||
|
|
||||||
self.reply_markdown(update, no_groups)
|
self.reply_markdown(update, no_groups)
|
||||||
|
|
||||||
def get_bot_handler(self) -> CommandHandler:
|
def get_bot_handler(self) -> CommandHandler:
|
||||||
return self.bot_handler
|
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 build_groups_message(self, groups: Iterable[Group]) -> str:
|
def build_groups_message(self, groups: Iterable[Group]) -> str:
|
||||||
resultTable = pt.PrettyTable(['Name', 'Members'])
|
resultTable = pt.PrettyTable(['Name', 'Members'])
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from handler.vo.updateData import UpdateData
|
||||||
|
from logger import Logger
|
||||||
from config.contents import opted_in, opted_in_failed
|
from config.contents import opted_in, opted_in_failed
|
||||||
from exception.invalidArgumentException import InvalidArgumentException
|
from exception.invalidArgumentException import InvalidArgumentException
|
||||||
from exception.notFoundException import NotFoundException
|
from exception.notFoundException import NotFoundException
|
||||||
@ -31,11 +33,14 @@ class InHandler(AbstractHandler):
|
|||||||
|
|
||||||
user.add_to_chat(update_data.chat_id)
|
user.add_to_chat(update_data.chat_id)
|
||||||
self.user_repository.save(user)
|
self.user_repository.save(user)
|
||||||
|
|
||||||
except NotFoundException:
|
except NotFoundException:
|
||||||
self.user_repository.save_by_update_data(update_data)
|
self.user_repository.save_by_update_data(update_data)
|
||||||
|
|
||||||
self.reply_markdown(update, opted_in)
|
self.reply_markdown(update, opted_in)
|
||||||
|
self.log_action(update_data)
|
||||||
|
|
||||||
def get_bot_handler(self) -> CommandHandler:
|
def get_bot_handler(self) -> CommandHandler:
|
||||||
return self.bot_handler
|
return self.bot_handler
|
||||||
|
|
||||||
|
def log_action(self, update_data: UpdateData) -> None:
|
||||||
|
Logger.info(f'User {update_data.username} joined {update_data.chat_id}')
|
||||||
|
@ -3,6 +3,8 @@ from typing import Iterable
|
|||||||
from config.contents import mention_failed
|
from config.contents import mention_failed
|
||||||
from entity.user import User
|
from entity.user import User
|
||||||
from exception.invalidArgumentException import InvalidArgumentException
|
from exception.invalidArgumentException import InvalidArgumentException
|
||||||
|
from handler.vo.updateData import UpdateData
|
||||||
|
from logger import Logger
|
||||||
from repository.userRepository import UserRepository
|
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
|
||||||
@ -21,20 +23,24 @@ class MentionHandler(AbstractHandler):
|
|||||||
|
|
||||||
def handle(self, update: Update, context: CallbackContext) -> None:
|
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||||
try:
|
try:
|
||||||
updateData = self.get_update_data(update, context)
|
update_data = self.get_update_data(update, context)
|
||||||
except InvalidArgumentException as e:
|
except InvalidArgumentException as e:
|
||||||
return self.reply_markdown(update, str(e))
|
return self.reply_markdown(update, str(e))
|
||||||
|
|
||||||
users = self.user_repository.get_all_for_chat(updateData.chat_id)
|
users = self.user_repository.get_all_for_chat(update_data.chat_id)
|
||||||
|
|
||||||
if users:
|
if users:
|
||||||
return self.reply_markdown(update, self.build_mention_message(users))
|
self.reply_markdown(update, self.build_mention_message(users))
|
||||||
|
return self.log_action(update_data)
|
||||||
|
|
||||||
self.reply_markdown(update, mention_failed)
|
self.reply_markdown(update, mention_failed)
|
||||||
|
|
||||||
def get_bot_handler(self) -> CommandHandler:
|
def get_bot_handler(self) -> CommandHandler:
|
||||||
return self.bot_handler
|
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 build_mention_message(self, users: Iterable[User]) -> str:
|
def build_mention_message(self, users: Iterable[User]) -> str:
|
||||||
result = ''
|
result = ''
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from config.contents import opted_off, opted_off_failed
|
from config.contents import opted_off, opted_off_failed
|
||||||
from exception.invalidArgumentException import InvalidArgumentException
|
from exception.invalidArgumentException import InvalidArgumentException
|
||||||
from exception.notFoundException import NotFoundException
|
from exception.notFoundException import NotFoundException
|
||||||
|
from handler.vo.updateData import UpdateData
|
||||||
|
from logger import Logger
|
||||||
from repository.userRepository import UserRepository
|
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
|
||||||
@ -19,22 +21,26 @@ class OutHandler(AbstractHandler):
|
|||||||
|
|
||||||
def handle(self, update: Update, context: CallbackContext) -> None:
|
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||||
try:
|
try:
|
||||||
updateData = self.get_update_data(update, context)
|
update_data = self.get_update_data(update, context)
|
||||||
except InvalidArgumentException as e:
|
except InvalidArgumentException as e:
|
||||||
return self.reply_markdown(update, str(e))
|
return self.reply_markdown(update, str(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = self.user_repository.get_by_id(updateData.user_id)
|
user = self.user_repository.get_by_id(update_data.user_id)
|
||||||
|
|
||||||
if not user.is_in_chat(updateData.chat_id):
|
if not user.is_in_chat(update_data.chat_id):
|
||||||
raise NotFoundException()
|
raise NotFoundException()
|
||||||
except NotFoundException:
|
except NotFoundException:
|
||||||
return self.reply_markdown(update, opted_off_failed)
|
return self.reply_markdown(update, opted_off_failed)
|
||||||
|
|
||||||
user.remove_from_chat(updateData.chat_id)
|
user.remove_from_chat(update_data.chat_id)
|
||||||
self.user_repository.save(user)
|
self.user_repository.save(user)
|
||||||
|
|
||||||
self.reply_markdown(update, opted_off)
|
self.reply_markdown(update, opted_off)
|
||||||
|
self.log_action(update_data)
|
||||||
|
|
||||||
def get_bot_handler(self) -> CommandHandler:
|
def get_bot_handler(self) -> CommandHandler:
|
||||||
return self.bot_handler
|
return self.bot_handler
|
||||||
|
|
||||||
|
def log_action(self, update_data: UpdateData) -> None:
|
||||||
|
Logger.info(f'User {update_data.username} left {update_data.chat_id}')
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from entity.user import User
|
from entity.user import User
|
||||||
|
from logger import Logger
|
||||||
from telegram.ext.commandhandler import CommandHandler
|
from telegram.ext.commandhandler import CommandHandler
|
||||||
|
|
||||||
from handler.abstractHandler import AbstractHandler
|
from handler.abstractHandler import AbstractHandler
|
||||||
from handler.mentionHandler import MentionHandler
|
from handler.mentionHandler import MentionHandler
|
||||||
|
from handler.vo.updateData import UpdateData
|
||||||
|
|
||||||
|
|
||||||
class MentionHandler(MentionHandler, AbstractHandler):
|
class MentionHandler(MentionHandler, AbstractHandler):
|
||||||
@ -19,3 +21,6 @@ class MentionHandler(MentionHandler, AbstractHandler):
|
|||||||
result += f'*{user.username}\({user.user_id}\)*\n'
|
result += f'*{user.username}\({user.user_id}\)*\n'
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def log_action(self, update_data: UpdateData) -> None:
|
||||||
|
Logger.info(f'User {update_data.username} called /silent for {update_data.chat_id}')
|
||||||
|
25
src/handler/startHandler.py
Normal file
25
src/handler/startHandler.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from config.contents import start_text
|
||||||
|
from logger import Logger
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class StartHandler(AbstractHandler):
|
||||||
|
bot_handler: CommandHandler
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.bot_handler = CommandHandler('start', self.handle)
|
||||||
|
|
||||||
|
def handle(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
self.reply_markdown(update, start_text)
|
||||||
|
self.log_action(UpdateData.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}')
|
@ -18,21 +18,21 @@ class UpdateData():
|
|||||||
username: str
|
username: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_from_arguments(update: Update, context: CallbackContext) -> UpdateData:
|
def create_from_arguments(update: Update, context: CallbackContext, include_group: bool = True) -> UpdateData:
|
||||||
chat_id = str(update.effective_chat.id)
|
chat_id = str(update.effective_chat.id)
|
||||||
|
|
||||||
if context.args and context.args[0]:
|
if context.args and context.args[0] and include_group:
|
||||||
group_name = str(context.args[0])
|
group_name = str(context.args[0]).lower()
|
||||||
if not re.match(r"^[A-Za-z]+$", context.args[0]):
|
if not re.match(r"^[A-Za-z]+$", group_name):
|
||||||
raise InvalidArgumentException(re.escape('Group name must contain only letters.'))
|
raise InvalidArgumentException(re.escape('Group name must contain only letters.'))
|
||||||
|
|
||||||
if context.args[0] == Group.default_name:
|
if group_name == Group.default_name:
|
||||||
raise InvalidArgumentException(re.escape(f'Group can not be `{Group.default_name}`.'))
|
raise InvalidArgumentException(re.escape(f'Group can not be `{Group.default_name}`.'))
|
||||||
|
|
||||||
if len(context.args[0]) > 20:
|
if len(group_name) > 20:
|
||||||
raise InvalidArgumentException(re.escape(f'Group name length can not be greater than 20.'))
|
raise InvalidArgumentException(re.escape(f'Group name length can not be greater than 20.'))
|
||||||
|
|
||||||
chat_id += f'~{context.args[0]}'.lower()
|
chat_id += f'~{group_name}'
|
||||||
|
|
||||||
|
|
||||||
user_id = str(update.effective_user.id)
|
user_id = str(update.effective_user.id)
|
||||||
|
43
src/logger.py
Normal file
43
src/logger.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
action_logger: str = 'action-logger'
|
||||||
|
action_logger_file: str = '/var/log/bot/action.log'
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.configure(self.action_logger, self.action_logger_file, logging.INFO)
|
||||||
|
self.configure(self.main_logger, self.main_logger_file, logging.ERROR)
|
||||||
|
|
||||||
|
def configure(self, logger_name, log_file, level) -> None:
|
||||||
|
directory = os.path.dirname(log_file)
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
|
||||||
|
logger = logging.getLogger(logger_name)
|
||||||
|
file_handler = logging.FileHandler(log_file, mode='w')
|
||||||
|
file_handler.setFormatter(self.formatter)
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setFormatter(self.formatter)
|
||||||
|
|
||||||
|
logger.setLevel(level)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
logger.addHandler(stream_handler)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_logger(logger_name) -> logging.Logger:
|
||||||
|
return logging.getLogger(logger_name)
|
||||||
|
|
||||||
|
def info(message: str) -> None:
|
||||||
|
Logger.get_logger(Logger.action_logger).info(message)
|
||||||
|
|
||||||
|
def error(message: str) -> None:
|
||||||
|
Logger.get_logger(Logger.main_logger).error(message)
|
Loading…
x
Reference in New Issue
Block a user