Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,6 @@ dmypy.json

# Logs
*.session
*.session-journal
*.session-journal
logs
database
Comment on lines +119 to +120
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logs
database
logs/
database/

Maybe we can add the / at the back to denote directories

49 changes: 49 additions & 0 deletions Bot/Logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import logging
import os

class LoggingFormatter(logging.Formatter):
# Colors
black = "\x1b[30m"
red = "\x1b[31m"
green = "\x1b[32m"
yellow = "\x1b[33m"
blue = "\x1b[34m"
gray = "\x1b[38m"
# Styles
reset = "\x1b[0m"
bold = "\x1b[1m"

COLORS = {
logging.DEBUG: gray + bold,
logging.INFO: blue + bold,
logging.WARNING: yellow + bold,
logging.ERROR: red,
logging.CRITICAL: red + bold,
}

def format(self, record):
log_color = self.COLORS[record.levelno]
format = "(black){asctime}(reset) (levelcolor){levelname}(reset) (green){name}(reset) {message}"
format = format.replace("(black)", self.black + self.bold)
format = format.replace("(reset)", self.reset)
format = format.replace("(levelcolor)", log_color)
format = format.replace("(green)", self.green + self.bold)
Comment on lines +26 to +30
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
format = "(black){asctime}(reset) (levelcolor){levelname}(reset) (green){name}(reset) {message}"
format = format.replace("(black)", self.black + self.bold)
format = format.replace("(reset)", self.reset)
format = format.replace("(levelcolor)", log_color)
format = format.replace("(green)", self.green + self.bold)
format = "{black}{{asctime}}{reset} {levelcolor}{{levelname}}{reset} {green}{{name}}{reset} {{message}}".format(
black= self.black + self.bold,
reset = self.reset,
levelcolor=log_color,
green=self.green+self.bold,
)

We can make use of {{ and }} to escape the { and } so that we can format them later.

formatter = logging.Formatter(format, "%Y-%m-%d %H:%M:%S", style="{")
return formatter.format(record)

def setlogger(name):
'''Setup the logger'''
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
console_handler.setFormatter(LoggingFormatter())
os.makedirs(f'./logs', exist_ok=True)
file_handler = logging.FileHandler(f'./logs/{name}.log', 'a', 'utf-8')
file_handler_formatter = logging.Formatter(
"[{asctime}] [{levelname}] {name}: {message}", "%Y-%m-%d %H:%M:%S", style="{"
)
file_handler.setFormatter(file_handler_formatter)

logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
43 changes: 34 additions & 9 deletions Bot/Storage.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
import datetime
from datetime import datetime
import json
import os
from typing import Optional

from logging import Logger

class Storage:
def __init__(self) -> None:
def __init__(self, logger : Logger) -> None:
"""Stores the information for each user"""
# Load the shelve db if possible
self.storage = {}
if os.path.isfile(f"./database/storage.json"):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  ...
  path = os.path.join(BASE_DIR,"database","storage.json")

Filepath can be improved using the snippet above instead of ./ as the location may vary based on the directory where the command was run.

logger.info("Loading stored events...")
with open("./database/storage.json", "r") as storageFile:
try:
self.storage = json.load(storageFile)
except Exception as e:
logger.error("Error loading stored events", {str(e)})
self.storage = {}
else:
logger.info("No saved events found.")
self.storage = {}
os.makedirs("./database", exist_ok=True)
with open("./database/storage.json", "w") as storageFile:
json.dump(self.storage, storageFile, indent=4)

Comment on lines +11 to +25
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an ideal solution for writing into a file.
If server is stopped unexpectedly, the events will not saved.

In this case it might be better to use something like sqlite3.


def add_event(self, chat_id: int, event_name: str, event_time: str) -> datetime.datetime:
def add_event(self, chat_id: int, event_name: str, event_time: str) -> datetime:
"""
Throws ValueError when the format is incorrect
"""
chat_id = str(chat_id)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can create another class to deal with the file logging (opening/closing/writing to files ) and only write to the file when the destructor of the class is called.

if chat_id not in self.storage:
self.storage[chat_id] = {}
deadline = datetime.datetime.strptime(
deadline = datetime.strptime(
event_time, '%d/%m/%Y %H:%M')
self.storage[chat_id][event_name] = deadline
self.storage[chat_id][event_name] = event_time
with open("./database/storage.json", "w") as storageFile:
json.dump(self.storage, storageFile, indent=4)
return deadline

def get_events(self, chat_id: int, event_name) -> Optional[datetime.datetime]:
return self.storage.get(chat_id, {}).get(event_name, None)
def get_events(self, chat_id: int, event_name) -> Optional[datetime]:
chat_id = str(chat_id)
event_time = self.storage.get(chat_id, {}).get(event_name, None)
if event_time is None: return None
return datetime.strptime(event_time, '%d/%m/%Y %H:%M')

def delete_event(self, chat_id: int, event_name: str) -> bool:
chat_id = str(chat_id)
try:
del self.storage[chat_id][event_name]
with open("./database/storage.json", "w") as storageFile:
json.dump(self.storage, storageFile, indent=4)
return True
except:
return False
44 changes: 24 additions & 20 deletions Bot/__main__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import os
import datetime
import logging

from time import sleep
from Storage import Storage
from Logger import setlogger
from dotenv import load_dotenv
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.client import Client
from constants import CALLBACK_DICT, ERROR_CMD_MSG, ZERO_TIME_DELTA, TIMER_FORMAT, EVENT_ENDED_FORMAT, POLLING_INTERVAL, TIME_FORMAT, CANCEL_MSG, ERROR_CANCEL_MSG, EVENT_CANCELLED_FORMAT, CMD_START, CMD_DEFAULT, CMD_CANCEL, CMD_TIMER, BOT_NAME, LOGGER_FORMAT
from constants import CALLBACK_DICT, ERROR_CMD_MSG, ZERO_TIME_DELTA, TIMER_FORMAT, EVENT_ENDED_FORMAT, POLLING_INTERVAL, CANCEL_MSG, ERROR_CANCEL_MSG, EVENT_CANCELLED_FORMAT, CMD_START, CMD_DEFAULT, CMD_CANCEL, CMD_TIMER, BOT_NAME, FOOTER

load_dotenv()
storage = Storage()
logger = setlogger(BOT_NAME)
storage = Storage(logger)

app = Client(
BOT_NAME,
Expand All @@ -19,20 +21,16 @@
bot_token=os.environ.get("BOT_TOKEN", ""),
)

logging.basicConfig(format=LOGGER_FORMAT)
logger = logging.getLogger(__name__)


@app.on_message(filters.command(CMD_START))
async def start(_, message):
async def start(_, message: Message):
await message.reply(
text=CALLBACK_DICT[CMD_START].get_msg(),
reply_markup=CALLBACK_DICT[CMD_START].get_markup()
)


@app.on_message(filters.command(CMD_CANCEL))
async def cancel(_, message):
async def cancel(_, message: Message):
try:
_, event_name = message.text.split(' ', 1)
if not storage.delete_event(message.chat.id, event_name):
Expand All @@ -47,7 +45,7 @@ async def cancel(_, message):


@app.on_message(filters.command(CMD_TIMER))
async def start_timer(_, message):
async def start_timer(_, message: Message):
"""The main method for the timer message"""
try:
# [command, date, time, event_name]
Expand All @@ -68,15 +66,18 @@ async def start_timer(_, message):

await refresh_msg(msg, deadline, event_name)

except (ValueError, TypeError):
except (ValueError, TypeError) as e:
logger.error(str(e))
await message.reply(text=ERROR_CMD_MSG)


async def refresh_msg(msg, deadline: datetime.datetime, event_name: str):
async def refresh_msg(msg: Message, deadline: datetime.datetime, event_name: str):
"""Updates the event message until it is pass the deadline"""
sleep_time = max(POLLING_INTERVAL, 5)
while True:
sleep(POLLING_INTERVAL)
sleep(sleep_time)
time_left = deadline - datetime.datetime.now()
if not time_left.days and time_left.seconds < 10: sleep_time = 1
if storage.get_events(msg.chat.id, event_name) is None:
format = EVENT_CANCELLED_FORMAT
logger.info(f"Event {event_name} was cancelled")
Expand All @@ -87,21 +88,24 @@ async def refresh_msg(msg, deadline: datetime.datetime, event_name: str):
break
event_string = get_event_string(time_left, event_name)
await msg.edit(event_string)
logger.info(f"Event {event_name} updated for {time_left}")
# logger.info(f"Event {event_name} updated for {time_left}")
await msg.edit(format.format(event_name=event_name))


def get_event_string(time: datetime.timedelta, event_name: str):
"""Get the string format for event message"""
return TIMER_FORMAT.format(time=get_time_string(time), event_name=event_name)
return TIMER_FORMAT.format(time=get_time_string(time), event_name=event_name, footer = FOOTER)


def get_time_string(time: datetime.timedelta):
hours = time.seconds // 3600
minutes = (time.seconds % 3600) // 60
seconds = time.seconds % 60
return TIME_FORMAT.format(days=time.days, hours=hours, minutes=minutes, seconds=seconds)

time_string = ""
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting logic here can be handled by changing the TIME_FORMAT variable

if time.days: time_string += f"{time.days}**d** "
minutes, seconds = divmod(time.seconds, 60)
hours, minutes = divmod(minutes, 60)
if hours: time_string += f"{hours}**h** {minutes}**m** {seconds}**s**"
elif minutes: time_string += f"{minutes}**m** {seconds}**s**"
else: time_string += f"{seconds}**s**"
return time_string

@app.on_callback_query()
async def callback(_, query) -> None:
Expand Down
15 changes: 9 additions & 6 deletions Bot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
CMD_CANCEL = 'cancel'

# Logger Format
LOGGER_FORMAT = '%(asctime)s %(clientip)-15s %(user)-8s %(message)s'
# LOGGER_FORMAT = '%(asctime)s %(clientip)-15s %(user)-8s %(message)s'
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can remove commented code. If we have a need to retrieve it, we can look at the previous versions of the code.


# Interval to edit the message (Default 30 seconds)
POLLING_INTERVAL = 10
POLLING_INTERVAL = 1
FOOTER = "My developer is the greatest"

# Format for Display
TIMER_FORMAT = "**{event_name}**\n⏳{time}\nThis updates every " + \
str(POLLING_INTERVAL) + " seconds."
EVENT_ENDED_FORMAT = "{event_name} has already ended :("
# TIMER_FORMAT = "**{event_name}**\n⏳{time}\nThis updates every " + \
# str(POLLING_INTERVAL) + " seconds."
TIMER_FORMAT = "**{event_name}**\n\n⏳{time}\n\n__{footer}__"

EVENT_ENDED_FORMAT = "{event_name} has already ended!"
EVENT_CANCELLED_FORMAT = "{event_name} is cancelled :("
TIME_FORMAT = "{days} Days, {hours} Hours, {minutes} Minutes {seconds} Seconds left"
# TIME_FORMAT = "{days} Days, {hours} Hours, {minutes} Minutes {seconds} Seconds left"

START_MSG = f'Welcome to the {BOT_NAME} bot, feel free to look around'
CANCEL_MSG = "{event_name} is cancelled."
Expand Down