diff --git a/.gitignore b/.gitignore index a3068b3..268d9f2 100644 --- a/.gitignore +++ b/.gitignore @@ -565,6 +565,5 @@ FodyWeavers.xsd # Hide internal database and it's backups *.db -# Hide token file -token -token.devel \ No newline at end of file +# Hide config file +config.ini \ No newline at end of file diff --git a/bot.py b/bot.py index 2561d8c..cf9fbea 100644 --- a/bot.py +++ b/bot.py @@ -3,90 +3,31 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import telebot -import re import time import datetime as dt import sys import signal import os import subprocess -import pickle as p +import configparser from os import path from sys import stderr, stdout, stdin from threading import Thread +filename = "config.ini" +config = configparser.ConfigParser() +config.read(filename) -db = {} - -def save_db(dbfo = ".db"): - global db - try: - with open(dbfo, "wb") as fo: - p.dump(db, fo) - except: - stderr.write("Не удалось записать базу!") - sys.exit(1) - -def load_db(dbfi = ".db"): - global db - if not path.exists(dbfi): - save_db() - return - try: - with open(dbfi, "rb") as fi: - db = p.load(fi) - except: - stderr.write("Не удалось прочитать базу, пробуем сохранить…") - save_db() - -def write_db(field: str, value: any, data = None): - global db - head = False - if data == None: - data = db - head = True - field = field.split(".") - if len(field) == 1: - data[field[0]] = value - else: - if field[0] not in data or data[field[0]] is None: - data[field[0]] = {} - if type(data[field[0]]) is not dict: - raise ValueError("Поле не является группой.") - data[field[0]] = write_db('.'.join(field[1:]), value, data[field[0]]) - save_db() - if head: - db = data - return data - -def read_db(field: str, default = None, data = None) -> any: - global db - if data == None: - data = db - field = field.split(".") - if field[0] not in data: - return default - if len(field) == 1: - return data[field[0]] - else: - return read_db('.'.join(field[1:]), default, data[field[0]]) - -def pop_db(field: str): - global db - ret = read_db(field) - field = field.split(".") - val = read_db('.'.join(field[:-1])) - if type(val) is not dict: - return - val.pop(field[-1]) - write_db('.'.join(field[:-1]), val) - return ret - - +# TODO more backends +from pickle_db import * load_db() -write_db("about.version", "v1.0rc4") +__cur_version = "v1.0rc5" +__version = read_db("about.version", __cur_version) +write_db("about.updatedfrom", __version) +write_db("about.version", __cur_version) write_db("about.author", "electromagneticcyclone") write_db("about.tester", "angelbeautifull") +write_db("about.source", "https://git.disroot.org/electromagneticcyclone/duty-board-dog") if (read_db("about.host") is None) and __debug__: stdout.write("Введите username хоста: ") stdout.flush() @@ -95,15 +36,32 @@ if (read_db("about.host") is None) and __debug__: try: bot = None - filename = "token.devel" if __debug__ else "token" - with open(filename, encoding = "utf-8") as fi: - for i in fi: - i = i.strip() - pattern = re.compile("^[\\d]{10}:[\\w\\d\\-\\+\\*]{35}$") - matches = pattern.fullmatch(i) is not None - if matches: - bot = telebot.TeleBot(i, parse_mode = "MarkdownV2") - break + if __version != __cur_version: + import re + if path.exists("token"): + with open("token", encoding = "utf-8") as fi: + for i in fi: + i = i.strip() + pattern = re.compile("^[\\d]{10}:[\\w\\d\\-\\+\\*]{35}$") + matches = pattern.fullmatch(i) is not None + if matches: + config['tokens']['prod'] = i + config.write(filename) + break + os.remove("token") + if path.exists("token.devel"): + with open("token.devel", encoding = "utf-8") as fi: + for i in fi: + i = i.strip() + pattern = re.compile("^[\\d]{10}:[\\w\\d\\-\\+\\*]{35}$") + matches = pattern.fullmatch(i) is not None + if matches: + config['tokens']['devel'] = i + config.write(filename) + break + os.remove("token.devel") + bot = telebot.TeleBot( config['tokens']['devel' if __debug__ else 'prod'], + parse_mode = "MarkdownV2") if bot is None: stderr.write("В файле нет токена\n") raise Exception @@ -111,6 +69,7 @@ except: stderr.write("Ошибка чтения файла токена\n") sys.exit(1) + def get_time(forum: int): return dt.datetime.now(dt.UTC) \ + dt.timedelta(hours = read_db(str(forum) + ".settings.timezone", 3)) @@ -251,6 +210,89 @@ def insert_user_in_current_order(forum: int, uid) -> bool: write_db(str(forum) + ".rookies.order", list(order.keys())[1:]) return True +def parse_dates(forum: int, args): + dates = [] + OK = True + for a in args: + if a.lower() == "сегодня": + dates.append(get_time(forum).date()) + continue + if a.lower() == "завтра": + dates.append(get_time(forum).date() + dt.timedelta(days=1)) + continue + if a.lower() == "послезавтра": + dates.append(get_time(forum).date() + dt.timedelta(days=2)) + continue + if a.lower() == "вчера": + dates.append(get_time(forum).date() - dt.timedelta(days=1)) + continue + if a.lower() == "позавчера": + dates.append(get_time(forum).date() - dt.timedelta(days=2)) + continue + d = a.split('.') + a_dates = [] + if len(d) in (2, 3): + try: + d = list(map(int, d)) + except: + print("Ne ok") + OK = False + if OK: + cur_date = get_time(forum).date() - dt.timedelta(days=1) + cur_year = cur_date.year + if len(d) == 2: + years = [cur_year, cur_year + 1] + else: + years = [(cur_year // 100 * 100) + d[2] if (d[2] < 100) else d[2]] + for y in years: + try: + a_dates.append(dt.datetime(y, d[1], d[0]).date()) + except: + pass + a_dates = sorted(filter(lambda x: cur_date + dt.timedelta(days=120) > x > cur_date, a_dates)) + if len(a_dates) == 0: + OK = False + else: + OK = False + if OK: + dates.append(a_dates[0]) + else: + return a + return dates + +def mod_days(message, target, neighbour): + forum = message.chat.id + chat = get_chat(message) + if chat is not None: + if antispam(message, chat, forum): + return + if check_if_admin(message): + args = message.text.split()[1:] + if len(args) == 0: + dates = [get_time(forum).date()] + else: + dates = parse_dates(forum, args) + if type(dates) is str: + bot.reply_to(chat, telebot.formatting.escape_markdown(dates) + + " — это точно дата из ближайшего будущего?") + return + if dates is None: + bot.reply_to(chat, "Нечего добавлять") + return + t = read_db(target) + if t is None: + t = [] + n = read_db(neighbour) + if n is None: + n = [] + write_db(neighbour, list(filter(lambda x: x not in dates, n))) + write_db(target, list(sorted(set(t + dates)))) + if read_db(str(forum) + ".settings.delete_messages"): + bot.delete_message(forum, message.id) + bot.reply_to(chat, + "Добавил " + telebot.formatting.escape_markdown( + ", ".join(['.'.join(map(str, (d.day, d.month, d.year))) for d in dates]))) + timeout = None caution = True def antispam(message, chat, forum): @@ -320,9 +362,26 @@ def help(message): + "/stop — остановить дежурства\n" + "/begin \\[@\\] — начать сначала с определённого студента") -@bot.message_handler(commands=['exec']) -def exec_bot(message): - if __debug__: +if __debug__: + def pretty(d, indent=0): + for key, value in d.items(): + print(' ' * indent + str(key)) + if isinstance(value, dict): + pretty(value, indent+1) + else: + print(' ' * (indent+1) + str(value)) + + @bot.message_handler(commands=['info']) + def info(message): + forum = message.chat.id + chat = get_chat(message, True) + if chat is not None: + bot.delete_message(forum, message.id) + if message.from_user.username == read_db("about.host"): + pretty(db) + + @bot.message_handler(commands=['exec']) + def exec_bot(message): forum = message.chat.id chat = get_chat(message, True) if chat is not None: @@ -332,24 +391,6 @@ def exec_bot(message): else: bot.delete_message(forum, message.id) -def pretty(d, indent=0): - for key, value in d.items(): - print(' ' * indent + str(key)) - if isinstance(value, dict): - pretty(value, indent+1) - else: - print(' ' * (indent+1) + str(value)) - -@bot.message_handler(commands=['info']) -def info(message): - if __debug__: - forum = message.chat.id - chat = get_chat(message, True) - if chat is not None: - bot.delete_message(forum, message.id) - if message.from_user.username == read_db("about.host"): - pretty(db) - @bot.message_handler(commands=['fuck', 'fuck_you', 'fuck-you', 'fuckyou']) def rude(message): forum = message.chat.id @@ -762,89 +803,6 @@ def calendar(message): + "*Рабочие:*\n" + telebot.formatting.escape_markdown( "\n".join(['.'.join(map(str, (d.day, d.month, d.year))) for d in work]))) -def parse_dates(forum: int, args): - dates = [] - OK = True - for a in args: - if a.lower() == "сегодня": - dates.append(get_time(forum).date()) - continue - if a.lower() == "завтра": - dates.append(get_time(forum).date() + dt.timedelta(days=1)) - continue - if a.lower() == "послезавтра": - dates.append(get_time(forum).date() + dt.timedelta(days=2)) - continue - if a.lower() == "вчера": - dates.append(get_time(forum).date() - dt.timedelta(days=1)) - continue - if a.lower() == "позавчера": - dates.append(get_time(forum).date() - dt.timedelta(days=2)) - continue - d = a.split('.') - a_dates = [] - if len(d) in (2, 3): - try: - d = list(map(int, d)) - except: - print("Ne ok") - OK = False - if OK: - cur_date = get_time(forum).date() - dt.timedelta(days=1) - cur_year = cur_date.year - if len(d) == 2: - years = [cur_year, cur_year + 1] - else: - years = [(cur_year // 100 * 100) + d[2] if (d[2] < 100) else d[2]] - for y in years: - try: - a_dates.append(dt.datetime(y, d[1], d[0]).date()) - except: - pass - a_dates = sorted(filter(lambda x: cur_date + dt.timedelta(days=120) > x > cur_date, a_dates)) - if len(a_dates) == 0: - OK = False - else: - OK = False - if OK: - dates.append(a_dates[0]) - else: - return a - return dates - -def mod_days(message, target, neighbour): - forum = message.chat.id - chat = get_chat(message) - if chat is not None: - if antispam(message, chat, forum): - return - if check_if_admin(message): - args = message.text.split()[1:] - if len(args) == 0: - dates = [get_time(forum).date()] - else: - dates = parse_dates(forum, args) - if type(dates) is str: - bot.reply_to(chat, telebot.formatting.escape_markdown(dates) - + " — это точно дата из ближайшего будущего?") - return - if dates is None: - bot.reply_to(chat, "Нечего добавлять") - return - t = read_db(target) - if t is None: - t = [] - n = read_db(neighbour) - if n is None: - n = [] - write_db(neighbour, list(filter(lambda x: x not in dates, n))) - write_db(target, list(sorted(set(t + dates)))) - if read_db(str(forum) + ".settings.delete_messages"): - bot.delete_message(forum, message.id) - bot.reply_to(chat, - "Добавил " + telebot.formatting.escape_markdown( - ", ".join(['.'.join(map(str, (d.day, d.month, d.year))) for d in dates]))) - @bot.message_handler(commands=['skip']) def skip_days(message): forum = message.chat.id @@ -1169,13 +1127,6 @@ def set_timezone(message): def get_hours(forum: int) -> list: utc_range = (8, 20) return utc_range - # Obsoleted by `get_time` - # tz = read_db(str(forum) + ".settings.timezone", 3) - # utc_range = tuple(map(lambda x: (x - tz) % 24, utc_range)) - # if utc_range[0] > utc_range[1]: - # return list(range(utc_range[0], 24)) + list(range(0, utc_range[1])) - # else: - # return list(range(utc_range[0], utc_range[1])) def stack_update(forum: int, force_reset = False): now = get_time(forum) @@ -1261,6 +1212,17 @@ def update(forum: int): write_db(str(forum) + ".schedule.last_notification_date", now_date) remind_users(forum) +def update_notify(forum: int): + bot.reply_to(get_chat(forum), + "Обновился до версии " + telebot.formatting.escape_markdown(__version)) + + +if __version != __cur_version: + for i in db.keys(): + try: + update_notify(int(i)) + except ValueError: + pass def process1(): bot.infinity_polling(none_stop=True) diff --git a/token.example b/config.example.ini similarity index 53% rename from token.example rename to config.example.ini index 8d335d4..21c71bb 100644 --- a/token.example +++ b/config.example.ini @@ -2,5 +2,6 @@ # # SPDX-License-Identifier: Unlicense -# Enter your token in the `token` file -0000000000:ooooooooooooooooooooooooooooooooooo \ No newline at end of file +[tokens] +# Enter your token in the `config.ini` file +prod = 0000000000:ooooooooooooooooooooooooooooooooooo \ No newline at end of file diff --git a/pickle_db.py b/pickle_db.py new file mode 100644 index 0000000..67a0d11 --- /dev/null +++ b/pickle_db.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2023 Egor Guslyancev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import sys +import pickle as p +from os import path +from sys import stderr, stdout, stdin + +db = {} + +def save_db(dbfo = ".db"): + global db + try: + with open(dbfo, "wb") as fo: + p.dump(db, fo) + except: + stderr.write("Не удалось записать базу!") + sys.exit(1) + +def load_db(dbfi = ".db"): + global db + if not path.exists(dbfi): + save_db() + return + try: + with open(dbfi, "rb") as fi: + db = p.load(fi) + except: + stderr.write("Не удалось прочитать базу, пробуем сохранить…") + save_db() + +def write_db(field: str, value: any, data = None): + global db + head = False + if data == None: + data = db + head = True + field = field.split(".") + if len(field) == 1: + data[field[0]] = value + else: + if field[0] not in data or data[field[0]] is None: + data[field[0]] = {} + if type(data[field[0]]) is not dict: + raise ValueError("Поле не является группой.") + data[field[0]] = write_db('.'.join(field[1:]), value, data[field[0]]) + save_db() + if head: + db = data + return data + +def read_db(field: str, default = None, data = None) -> any: + global db + if data == None: + data = db + field = field.split(".") + if field[0] not in data: + return default + if len(field) == 1: + return data[field[0]] + else: + return read_db('.'.join(field[1:]), default, data[field[0]]) + +def pop_db(field: str): + global db + ret = read_db(field) + field = field.split(".") + val = read_db('.'.join(field[:-1])) + if type(val) is not dict: + return + val.pop(field[-1]) + write_db('.'.join(field[:-1]), val) + return ret \ No newline at end of file