mirror of
https://github.com/miloszowi/everyone-mention-telegram-bot.git
synced 2025-06-01 23:24:06 +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/
|
||||
|
||||
# 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:
|
||||
|
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.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}')
|
||||
|
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
|
||||
|
||||
@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
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