modular-bot-framework-for-t.../modules/auto-schedule-pro-v2/main.py

478 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
import json
import os
def readfile(filename):
with open(module_path + filename) as f:
return f.read()
def writefile(filename, data):
with open(module_path + filename, 'w') as f:
f.write(data)
# global constants
# Accusative - znahidnyj
WEEKDAYS_ACCUSATIVE = ["понеділок", "вівторок", "середу", "четвер", "п'ятницю", "суботу", "неділю"]
# Genitive - rodovyj
WEEKDAYS_GENITIVE_NEXT = ["наступного понеділка", "наступного вівторка", "наступної середи", "наступного четверга",
"наступної п'ятниці", "наступної суботи", "наступної неділі"]
WEEKDAYS_GENITIVE_THIS = ["цього понеділка", "цього вівторка", "цієї середи", "цього четверга", "цієї п'ятниці",
"цієї суботи", "цієї неділі"]
lesson_types_to_strings = {
"lec": "лекція",
"prac": "практика",
"lab": "лабораторна",
"con": "консультація"
}
color_code_mapping = {
"lec": "🔵",
"prac": "🟡",
"lab": "🔴",
"con": "🟢"
}
# global variables
module_path = ""
def get_preference_by_id(user_id, name):
if not os.path.exists(module_path + f"preference-db/{user_id}.json"):
return None
raw_prefs = readfile(f"preference-db/{user_id}.json")
try:
preferences = json.loads(raw_prefs)
except Exception as e:
return None
if not name in preferences:
return None
return preferences[name]
def get_all_preferences_by_id(user_id):
user_preferences = {
"output-style": "legacy-vibrant",
"output-style-lesson": "None",
"output-style-lookup": "None"
}
# label defaults as defaults and let custom settings override these labels
for i in user_preferences:
user_preferences[i] += " <i>(default)</i>"
for i in user_preferences:
override = get_preference_by_id(user_id, i)
if override != None:
user_preferences[i] = override
return user_preferences
def set_preference_by_id(user_id, name, value):
if not os.path.exists(module_path + "preference-db/"):
os.mkdir(module_path + "preference-db/")
preferences = {}
if os.path.exists(module_path + f"preference-db/{user_id}.json"):
try:
raw_prefs = readfile(f"preference-db/{user_id}.json")
preferences = json.loads(raw_prefs)
except Exception as e:
preferences = {}
else:
preferences = {}
preferences[name] = value
final_data = json.dumps(preferences)
writefile(f"preference-db/{user_id}.json", final_data)
def clear_preference_by_id(user_id, name):
if not os.path.exists(module_path + "preference-db/"):
os.mkdir(module_path + "preference-db/")
preferences = {}
if os.path.exists(module_path + f"preference-db/{user_id}.json"):
try:
raw_prefs = readfile(f"preference-db/{user_id}.json")
preferences = json.loads(raw_prefs)
except Exception as e:
preferences = {}
else:
preferences = {}
if name in preferences:
del preferences[name]
final_data = json.dumps(preferences)
writefile(f"preference-db/{user_id}.json", final_data)
def load_template(template, part):
return readfile(f"templates/{template}/{part}.msg")
def get_color_code(lesson_type):
return color_code_mapping[lesson_type]
def escaped_string_markdownV2(input_string):
result_string = input_string
symbols_to_escape = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for symbol in symbols_to_escape:
result_string = result_string.replace(symbol, f"\\{symbol}")
return result_string
def escaped_string_html(input_string):
result_string = input_string
symbols_to_escape = ['<', '>']
for symbol in symbols_to_escape:
result_string = result_string.replace(symbol, f"\\{symbol}")
return result_string
def get_human_readable_date(start_datetime, end_datetime,
current_day, current_week):
human_readable_date = ""
if ((current_day + 2) == int(start_datetime.strftime("%u"))) or \
((current_day == 6) and (start_datetime.strftime("%u") == "1")):
human_readable_date += "завтра "
elif current_week != int(start_datetime.strftime("%W")) % 2:
human_readable_date += f"{WEEKDAYS_GENITIVE_NEXT[int(start_datetime.strftime('%u')) - 1]} "
elif current_day != (int(start_datetime.strftime("%u")) - 1):
human_readable_date += f"{WEEKDAYS_GENITIVE_THIS[int(start_datetime.strftime('%u')) - 1]} "
else:
human_readable_date += "сьогодні "
human_readable_date += "з "
human_readable_date += start_datetime.strftime("%H:%M")
human_readable_date += " до "
human_readable_date += end_datetime.strftime("%H:%M")
return human_readable_date
def get_name_of_lesson_type(lesson_type):
if lesson_type in lesson_types_to_strings:
return lesson_types_to_strings[lesson_type]
def generate_lesson_description(lesson, start_datetime, end_datetime, current_day, current_week, overrides={},
custom_name_prefix="Назва", template="legacy-vibrant", force_date_at_top=False):
if lesson.__class__ == dict:
if force_date_at_top:
total_result = load_template(template, "date")
human_readable_date = get_human_readable_date(start_datetime, end_datetime,
current_day, current_week)
total_result = total_result.replace("%DATE%", human_readable_date)
if "full" in overrides and overrides["full"]:
total_result += load_template(template, "multiple")
else:
total_result += load_template(template, "multiple-short")
for i in ['name', 'teacher']:
total_result = total_result.replace(f"%{i.upper()}%", lesson[i])
total_result = total_result.replace("%DATE%", human_readable_date)
total_result = total_result.replace("%TYPE%", get_name_of_lesson_type(lesson['type']))
total_result = total_result.replace("%NAME_PREFIX%", custom_name_prefix)
total_result = total_result.replace("%COLOR_CODE%", get_color_code(lesson['type']))
if ('nolink' not in lesson) or (not lesson['nolink']):
total_result = total_result.replace("%LINK%", lesson['link'])
else:
total_result = total_result.replace("%LINK%", "#")
if 'comment' in lesson:
total_result += load_template(template, "comment")
total_result = total_result.replace("%COMMENT%", lesson["comment"])
if "full" in overrides and overrides["full"]:
total_result += "\n"
return total_result
else:
active_template = load_template(template, "single")
for i in ['name', 'teacher']:
active_template = active_template.replace(f"%{i.upper()}%", lesson[i])
human_readable_date = get_human_readable_date(start_datetime, end_datetime,
current_day, current_week)
active_template = active_template.replace("%DATE%", human_readable_date)
active_template = active_template.replace("%TYPE%", get_name_of_lesson_type(lesson['type']))
active_template = active_template.replace("%NAME_PREFIX%", custom_name_prefix)
active_template = active_template.replace("%COLOR_CODE%", get_color_code(lesson['type']))
if ('nolink' not in lesson) or (not lesson['nolink']):
active_template = active_template.replace("%LINK%", lesson['link'])
else:
active_template = active_template.replace("%LINK%", "#")
if 'comment' in lesson:
active_template += load_template(template, "comment")
active_template = active_template.replace("%COMMENT%", lesson["comment"])
return active_template
elif lesson.__class__ == list:
total_result = load_template(template, "date")
human_readable_date = get_human_readable_date(start_datetime, end_datetime,
current_day, current_week)
total_result = total_result.replace("%DATE%", human_readable_date)
for l in lesson:
if "full" in overrides and overrides["full"]:
active_template = load_template(template, "multiple")
else:
active_template = load_template(template, "multiple-short")
for i in ['name', 'teacher']:
active_template = active_template.replace(f"%{i.upper()}%", l[i])
active_template = active_template.replace("%DATE%", human_readable_date)
active_template = active_template.replace("%TYPE%", get_name_of_lesson_type(l['type']))
active_template = active_template.replace("%NAME_PREFIX%", custom_name_prefix)
active_template = active_template.replace("%COLOR_CODE%", get_color_code(l['type']))
if ('nolink' not in lesson) or (not lesson['nolink']):
active_template = active_template.replace("%LINK%", l['link'])
else:
active_template = active_template.replace("%LINK%", "#")
if 'comment' in lesson:
active_template += load_template(template, "comment")
active_template = active_template.replace("%COMMENT%", lesson["comment"])
if "full" in overrides and overrides["full"]:
active_template += "\n"
total_result += active_template
return total_result
def get_schedule_data_from(filename):
raw_schedule = json.loads(readfile(filename))
baked_schedule = {}
for day_number, lesson_times in enumerate(raw_schedule):
for lesson_time in lesson_times:
timestamp = day_number * 86400 + int(lesson_time.split(":")[0]) * 3600 \
+ int(lesson_time.split(":")[1]) * 60
new_record = raw_schedule[day_number][lesson_time]
item_source = filename.split(".json")[0]
if new_record.__class__ == list:
for item in new_record:
item["source"] = item_source
else:
new_record["source"] = item_source
baked_schedule[timestamp] = new_record
return baked_schedule
def process_arguments(args, base_day):
selected_day = int(base_day)
preferences = {}
for arg in args:
if arg[0] == "-":
if arg[1:].isdigit():
selected_day -= int(arg[1:])
else:
preferences[arg[1:]] = False
elif arg[0] == "+":
if arg[1:].isdigit():
selected_day += int(arg[1:])
else:
preferences[arg[1:]] = True
selected_day = selected_day % 14
return preferences, selected_day
def get_lesson_description(schedule, reference_time, lesson_time, current_day, current_week, overrides={},
custom_name_prefix="Назва", template="legacy-vibrant", force_date_at_top=False):
lesson_record = schedule[lesson_time]
lesson_start_datetime = datetime.fromtimestamp(reference_time + lesson_time)
lesson_end_datetime = datetime.fromtimestamp(reference_time + lesson_time + 5400)
return generate_lesson_description(lesson_record, lesson_start_datetime, lesson_end_datetime, current_day,
current_week, overrides=overrides, custom_name_prefix=custom_name_prefix, template=template,
force_date_at_top=force_date_at_top)
def process(message, path):
message_text = message["text"]
full_command = message_text.split()
# there is no need to check if the full_command list if empty as it
# never will be - Telegram requires all messages to have at least
# one printable symbol, so this is already protected
base_command = full_command[0].lower()
if base_command not in ["!пара", "!пари", "!schedule-ctl",
"!para", "!pary"]:
return None, None
global module_path
module_path = path
schedule = get_schedule_data_from("schedule-v2.json")
schedule.update(get_schedule_data_from("additions-v2.json"))
current_time = datetime.now()
current_week = current_time.isocalendar()[1] % 2
current_day = current_time.weekday()
current_seconds = current_week * 604800 + current_day * 86400 + current_time.hour * 3600 + current_time.minute \
* 60 + current_time.second
reference_time = int(current_time.strftime("%s")) - current_seconds
if base_command in ["!пара", "!para"]:
# easter egg
study_begin_ts = int(datetime(year=2024, month=9, day=2).strftime("%s"))
current_ts = int(datetime.now().strftime("%s"))
until_study_day = study_begin_ts - current_ts
if -3600*4 < until_study_day < 0:
return "Навчання от-от розпочнеться!", None
elif 0 <= until_study_day < 3600*24*28:
return f"До навчання залишилося {until_study_day} секунд..." \
f" ({round(until_study_day/3600, 4)} годин," \
f" {round(until_study_day/3600/24, 4)} діб)", None
elif until_study_day >= 3600*24*14:
return "Ви маєте законне право відпочити, пари почнуться не скоро", None
# actual lesson finding code
upcoming_lessons = [timestamp for timestamp in schedule if timestamp > current_seconds - 5400]
if len(upcoming_lessons) > 0:
closest_lesson_time = min(upcoming_lessons)
else:
closest_lesson_time = min(schedule)
# shifting lesson pointer if requested to do so
if len(full_command) >= 2:
possible_times = list(schedule.keys())
current_pointer_position = possible_times.index(closest_lesson_time)
total_list_length = len(possible_times)
if len(full_command[1]) > 1 and full_command[1][1:].isdigit():
if full_command[1][0] == "+":
current_pointer_position = (current_pointer_position + int(full_command[1][1:])) % total_list_length
else:
current_pointer_position = (current_pointer_position - int(full_command[1][1:])) % total_list_length
closest_lesson_time = possible_times[current_pointer_position]
# getting corrent style
output_style_preference = "legacy-vibrant"
general_output_style_preference = get_preference_by_id(message.from_user.id, "output-style")
if general_output_style_preference != None:
output_style_preference = general_output_style_preference
specific_output_style_preference = get_preference_by_id(message.from_user.id, "output-style-lesson")
if specific_output_style_preference != None:
output_style_preference = specific_output_style_preference
# returning generated pair description
return get_lesson_description(schedule, reference_time, closest_lesson_time, current_day,
current_week, custom_name_prefix="Актуальна пара", template=output_style_preference), "HTML"
elif base_command in ["!пари", "!pary"]:
base_day = current_week * 7 + current_day
if len(full_command) >= 2:
args = [i for i in full_command[1:] if len(i) > 1]
preferences, selected_day = process_arguments(args, base_day)
else:
preferences = {}
selected_day = base_day
lesson_list = [i for i in schedule if selected_day * 86400 <= i < (selected_day + 1) * 86400]
output_style_preference = "legacy-vibrant"
general_output_style_preference = get_preference_by_id(message.from_user.id, "output-style")
if general_output_style_preference != None:
output_style_preference = general_output_style_preference
specific_output_style_preference = get_preference_by_id(message.from_user.id, "output-style-lookup")
if specific_output_style_preference != None:
output_style_preference = specific_output_style_preference
lesson_descriptions_list = [get_lesson_description(schedule, reference_time, lesson_time, current_day,
current_week, overrides=preferences, custom_name_prefix="Назва", template=output_style_preference, force_date_at_top=True)
for lesson_time in lesson_list]
return f"<b><u>Пари у {WEEKDAYS_ACCUSATIVE[selected_day % 7]}</u></b>:\n\n\n" + "\n".join(lesson_descriptions_list), "HTML"
elif base_command == "!schedule-ctl" and len(full_command) >= 2:
if full_command[1] == "list":
prefs = get_all_preferences_by_id(message.from_user.id)
return "Ваші персональні налаштування:\n" + '\n'.join([f"- {k} = {v}" for k, v in prefs.items()]), "HTML"
elif full_command[1] == "set" and len(full_command) == 4:
prefs = get_all_preferences_by_id(message.from_user.id)
if full_command[2] in prefs:
if full_command[2] in ["output-style", "output-style-lesson", "output-style-lookup"]:
if full_command[3] not in os.listdir(module_path + "templates/"):
return f"Стилю {full_command[3]} не існує; доступні варіанти: " \
+ ', '.join(os.listdir(module_path + "templates/")), "HTML"
previous_value = prefs[full_command[2]]
prefs[full_command[2]] = full_command[3]
set_preference_by_id(message.from_user.id, full_command[2], full_command[3])
return f"Змінено значення {full_command[2]}: {previous_value} -> {full_command[3]}", "HTML"
else:
return f"Такого налаштування не існує; переглянути наявні налаштування можна за допомогою команди <u>!schedule-ctl list</u>", "HTML"
elif full_command[1] == "get" and len(full_command) == 3:
requested_preference = get_preference_by_id(message.from_user.id, full_command[2])
return f"Налаштування {full_command[2]} має значення {requested_preference}", "HTML"
elif full_command[1] == "clear" and len(full_command) == 3:
clear_preference_by_id(message.from_user.id, full_command[2])
return f"Очищено значення для налаштування {full_command[2]}, надалі для нього використовуватиметься стандартне значення", "HTML"
else:
return "Такої команди не існує (або був використаний помилковий синтаксис)", "HTML"