added logging, added /start command

This commit is contained in:
miloszowi 2021-10-05 19:20:04 +02:00
parent ff1d037be9
commit c588fa439e
13 changed files with 175 additions and 24 deletions

3
.gitignore vendored

@ -129,3 +129,6 @@ dmypy.json
# Pyre type checker
.pyre/
# logs
logs/

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

@ -1,19 +1,27 @@
from logging import Logger
import logging
from telegram.ext import Updater
from telegram.ext.dispatcher import Dispatcher
from logger import Logger
from config.credentials import BOT_TOKEN, PORT, WEBHOOK_URL
from handler import (groupsHandler, inHandler, mentionHandler, outHandler,
silentMentionHandler, startHandler)
from handler.abstractHandler import AbstractHandler
from handler import (inHandler, mentionHandler, outHandler, silentMentionHandler, groupsHandler)
class App:
updater: Updater
dispatcher: Dispatcher
log_file: str = '/var/log/bot.log'
log_format: str = '%(levelname)s-%(asctime)s: %(message)s'
def __init__(self):
self.updater = Updater(BOT_TOKEN)
def run(self) -> None:
self.setup_logging()
self.register_handlers()
self.register_webhook()
@ -33,6 +41,14 @@ class App:
webhook_url="/".join([WEBHOOK_URL, BOT_TOKEN])
)
Logger.get_logger(Logger.action_logger).info(
f'Webhook configured, listening on {WEBHOOK_URL}/<bot-token>'
)
def setup_logging(self) -> None:
logger = Logger()
logger.setup()
if __name__ == "__main__":
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.')
mention_failed = re.escape('There are no users to mention.')
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 logger import Logger
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.handler import Handler
from telegram.update import Update
@ -14,14 +15,20 @@ class AbstractHandler:
@abstractmethod
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:
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:
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:
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
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
@ -20,18 +22,22 @@ class GroupsHandler(AbstractHandler):
self.group_repository = GroupRepository()
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:
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)
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 build_groups_message(self, groups: Iterable[Group]) -> str:
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 exception.invalidArgumentException import InvalidArgumentException
from exception.notFoundException import NotFoundException
@ -31,11 +33,14 @@ class InHandler(AbstractHandler):
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}')

@ -3,6 +3,8 @@ from typing import Iterable
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
@ -21,20 +23,24 @@ class MentionHandler(AbstractHandler):
def handle(self, update: Update, context: CallbackContext) -> None:
try:
updateData = self.get_update_data(update, context)
update_data = self.get_update_data(update, context)
except InvalidArgumentException as 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:
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)
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 build_mention_message(self, users: Iterable[User]) -> str:
result = ''

@ -1,6 +1,8 @@
from config.contents import opted_off, opted_off_failed
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
@ -19,22 +21,26 @@ class OutHandler(AbstractHandler):
def handle(self, update: Update, context: CallbackContext) -> None:
try:
updateData = self.get_update_data(update, context)
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(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()
except NotFoundException:
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.reply_markdown(update, opted_off)
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} left {update_data.chat_id}')

@ -1,10 +1,12 @@
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):
@ -19,3 +21,6 @@ class MentionHandler(MentionHandler, AbstractHandler):
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}')

@ -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
@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)
if context.args and context.args[0]:
group_name = str(context.args[0])
if not re.match(r"^[A-Za-z]+$", context.args[0]):
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 context.args[0] == Group.default_name:
if group_name == 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.'))
chat_id += f'~{context.args[0]}'.lower()
chat_id += f'~{group_name}'
user_id = str(update.effective_user.id)

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)