Merge branch 'master' of http://10.1.1.1:3000/dymik739/modular-bot-framework-for-telegram
This commit is contained in:
commit
184fd25c0f
|
@ -0,0 +1,2 @@
|
||||||
|
config/*
|
||||||
|
modules/irc-bridge/error.log
|
4
main.py
4
main.py
|
@ -180,11 +180,13 @@ def queue_processor():
|
||||||
if mod.version == 1:
|
if mod.version == 1:
|
||||||
responce = mod.process(msg)
|
responce = mod.process(msg)
|
||||||
if len(responce) > 0:
|
if len(responce) > 0:
|
||||||
updater.bot.send_message(chat_id = msg.chat.id, text = responce)
|
updater.bot.send_message(chat_id = msg.chat.id, text = responce, disable_web_page_preview = True)
|
||||||
print("Responded using module {} ({}) with text: {}".format(mod.path, mod.alias, responce))
|
print("Responded using module {} ({}) with text: {}".format(mod.path, mod.alias, responce))
|
||||||
break
|
break
|
||||||
|
|
||||||
del message_queue[0]
|
del message_queue[0]
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
else:
|
else:
|
||||||
if STOP_REQUESTED:
|
if STOP_REQUESTED:
|
||||||
break
|
break
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"12:20": {"name": "Культура мовлення та ділове мовлення", "teacher": "Кушлаба М. П.", "link": "https://bbb.comsys.kpi.ua/b/myk-0iw-red-p01"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
|
@ -0,0 +1,234 @@
|
||||||
|
## env approximation ##
|
||||||
|
'''
|
||||||
|
import datetime, json, time
|
||||||
|
|
||||||
|
class Self:
|
||||||
|
def __init__(self):
|
||||||
|
self.path = ""
|
||||||
|
self.WEEKDAY_NAMES_ROD = ["понеділка", "вівторка", "середи", "четверга", "п'ятниці", "суботи", "неділі"]
|
||||||
|
self.WEEKDAY_NAMES_ROD_WITH_NEXT = ["наступного понеділка", "наступного вівторка", "наступної середи", "наступного четверга", "наступної п'ятниці", "наступної суботи", "наступної неділі"]
|
||||||
|
self.WEEKDAY_NAMES_ROD_WITH_THIS = ["цього понеділка", "цього вівторка", "цієї середи", "цього четверга", "цієї п'ятниці", "цієї суботи", "цієї неділі"]
|
||||||
|
|
||||||
|
|
||||||
|
def readfile(path):
|
||||||
|
return open(path).read()
|
||||||
|
|
||||||
|
self = Self()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## code ##
|
||||||
|
if self.MESSAGE["text"].lower() == "!пара":
|
||||||
|
current_time = datetime.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
|
||||||
|
|
||||||
|
# baking defined schedule
|
||||||
|
raw_schedule = json.loads( readfile(self.path + "schedule.json") )
|
||||||
|
schedule = {}
|
||||||
|
for day in range(len(raw_schedule)):
|
||||||
|
for i in raw_schedule[day]:
|
||||||
|
ts = day*86400 + int(i.split(":")[0])*3600 + int(i.split(":")[1])*60
|
||||||
|
new_item = dict(raw_schedule[day][i])
|
||||||
|
new_item["source"] = "schedule"
|
||||||
|
schedule[ts] = new_item
|
||||||
|
|
||||||
|
# baking additions (extra pairs)
|
||||||
|
raw_additions = json.loads( readfile(self.path + "additions.json") )
|
||||||
|
additions = {}
|
||||||
|
for day in range(len(raw_additions)):
|
||||||
|
for i in raw_additions[day]:
|
||||||
|
ts = day*86400 + int(i.split(":")[0])*3600 + int(i.split(":")[1])*60
|
||||||
|
new_item = dict(raw_additions[day][i])
|
||||||
|
new_item["source"] = "additions"
|
||||||
|
schedule[ts] = new_item
|
||||||
|
|
||||||
|
full_schedule = dict(list(schedule.items()) + list(additions.items()))
|
||||||
|
|
||||||
|
print("test1")
|
||||||
|
print(f"Full schedule printout: {full_schedule}")
|
||||||
|
print(f"Current delta_time: {current_seconds}")
|
||||||
|
p = None
|
||||||
|
next_pair_time = None
|
||||||
|
|
||||||
|
key_list = list(full_schedule.keys())
|
||||||
|
key_list.sort()
|
||||||
|
for i in key_list:
|
||||||
|
if i > current_seconds - 5400:
|
||||||
|
p = full_schedule[i]
|
||||||
|
next_pair_time = i
|
||||||
|
break
|
||||||
|
|
||||||
|
print("test2")
|
||||||
|
if next_pair_time == None:
|
||||||
|
if len(full_schedule.keys()) > 0:
|
||||||
|
print("test3.1")
|
||||||
|
actual_pair_ts = reference_time + min(full_schedule.keys())
|
||||||
|
dt_pair = datetime.datetime.fromtimestamp(actual_pair_ts)
|
||||||
|
dt_pair_finish = datetime.datetime.fromtimestamp(actual_pair_ts + 5400)
|
||||||
|
p = full_schedule[min(full_schedule.keys())]
|
||||||
|
print("test3.1.1")
|
||||||
|
|
||||||
|
print("{} == 6 && {} == 1, {}".format(current_day, dt_pair.strftime('%u'), str( ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")) )))
|
||||||
|
|
||||||
|
human_readable_date = ""
|
||||||
|
if ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")):
|
||||||
|
human_readable_date += "завтра "
|
||||||
|
elif current_week != int(dt_pair.strftime("%W")) % 2:
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_NEXT[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
elif current_day != (int(dt_pair.strftime("%u")) - 1):
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_THIS[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
else:
|
||||||
|
human_readable_date += "сьогодні "
|
||||||
|
|
||||||
|
print("test3.1.2")
|
||||||
|
|
||||||
|
human_readable_date += "з "
|
||||||
|
|
||||||
|
print("test3.1.3")
|
||||||
|
human_readable_date += dt_pair.strftime("%H:%M")
|
||||||
|
print("test3.1.4")
|
||||||
|
|
||||||
|
human_readable_date += " до "
|
||||||
|
human_readable_date += dt_pair_finish.strftime("%H:%M")
|
||||||
|
|
||||||
|
self.RESPONCE = "Актуальна пара: {}\nДата: {}\nВикладач: {}\nПосилання на пару: {}".format(p['name'], human_readable_date, p['teacher'], p['link'])
|
||||||
|
print("test3.1.5")
|
||||||
|
else:
|
||||||
|
self.RESPONCE = "Пар немає взагалі. Ми вільні!"
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("test3.2")
|
||||||
|
actual_pair_ts = reference_time + next_pair_time
|
||||||
|
dt_pair = datetime.datetime.fromtimestamp(actual_pair_ts)
|
||||||
|
dt_pair_finish = datetime.datetime.fromtimestamp(actual_pair_ts + 5400)
|
||||||
|
|
||||||
|
human_readable_date = ""
|
||||||
|
if ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")):
|
||||||
|
human_readable_date += "завтра "
|
||||||
|
elif current_week != int(dt_pair.strftime("%W")) % 2:
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_NEXT[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
elif current_day != (int(dt_pair.strftime("%u")) - 1):
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_THIS[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
else:
|
||||||
|
human_readable_date += "сьогодні "
|
||||||
|
|
||||||
|
human_readable_date += "з "
|
||||||
|
human_readable_date += dt_pair.strftime("%H:%M")
|
||||||
|
|
||||||
|
human_readable_date += " до "
|
||||||
|
human_readable_date += dt_pair_finish.strftime("%H:%M")
|
||||||
|
|
||||||
|
self.RESPONCE = "Актуальна пара: {}\nДата: {}\nВикладач: {}\nПосилання на пару: {}".format(p['name'], human_readable_date, p['teacher'], p['link'])
|
||||||
|
|
||||||
|
if self.MESSAGE["text"].lower().split()[0] == "!пари":
|
||||||
|
command = self.MESSAGE["text"].lower().split()
|
||||||
|
|
||||||
|
current_time = datetime.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
|
||||||
|
|
||||||
|
# baking defined schedule
|
||||||
|
raw_schedule = json.loads( readfile(self.path + "schedule.json") )
|
||||||
|
schedule = {}
|
||||||
|
for day in range(len(raw_schedule)):
|
||||||
|
for i in raw_schedule[day]:
|
||||||
|
ts = day*86400 + int(i.split(":")[0])*3600 + int(i.split(":")[1])*60
|
||||||
|
new_item = dict(raw_schedule[day][i])
|
||||||
|
new_item["source"] = "schedule"
|
||||||
|
schedule[ts] = new_item
|
||||||
|
|
||||||
|
# baking additions (extra pairs)
|
||||||
|
raw_additions = json.loads( readfile(self.path + "additions.json") )
|
||||||
|
additions = {}
|
||||||
|
for day in range(len(raw_additions)):
|
||||||
|
for i in raw_additions[day]:
|
||||||
|
ts = day*86400 + int(i.split(":")[0])*3600 + int(i.split(":")[1])*60
|
||||||
|
new_item = dict(raw_additions[day][i])
|
||||||
|
new_item["source"] = "additions"
|
||||||
|
schedule[ts] = new_item
|
||||||
|
|
||||||
|
full_schedule = dict(list(schedule.items()) + list(additions.items()))
|
||||||
|
|
||||||
|
preferences = {"name": True, "date": True, "teacher": True, "link": True}
|
||||||
|
selected_day = current_week*7 + current_day
|
||||||
|
|
||||||
|
if len(command) >= 2 and len(command[1]) > 0:
|
||||||
|
if command[1][0] == "+":
|
||||||
|
try:
|
||||||
|
selected_day += int(command[1][1:])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[auto-schedule-pro:error] Got exception '{e}' while parsing {command[1]}")
|
||||||
|
elif command[1][0] == "-":
|
||||||
|
try:
|
||||||
|
selected_day -= int(command[1][1:])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[auto-schedule-pro:error] Got exception '{e}' while parsing {command[1]}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
selected_day = int(command[1])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[auto-schedule-pro:error] Got exception '{e}' while parsing {command[1]}")
|
||||||
|
|
||||||
|
# keeping day in bounds
|
||||||
|
while selected_day > 13:
|
||||||
|
selected_day -= 14
|
||||||
|
while selected_day < 0:
|
||||||
|
selected_day += 14
|
||||||
|
|
||||||
|
if len(command) > 2:
|
||||||
|
for i in command[2:]:
|
||||||
|
if len(i) >= 2:
|
||||||
|
if i[1:] in preferences:
|
||||||
|
if i[0] == "+":
|
||||||
|
preferences[i[1:]] = True
|
||||||
|
elif i[0] == "-":
|
||||||
|
preferences[i[1:]] = False
|
||||||
|
|
||||||
|
found_pairs = {}
|
||||||
|
for i in full_schedule:
|
||||||
|
if selected_day*86400 <= i < (selected_day+1)*86400:
|
||||||
|
found_pairs[i] = dict(full_schedule[i])
|
||||||
|
|
||||||
|
result_text = f"Пари у {self.WEEKDAY_NAMES_ZNAH[selected_day%7]}:\n\n"
|
||||||
|
for i in found_pairs:
|
||||||
|
actual_pair_ts = reference_time + i
|
||||||
|
dt_pair = datetime.datetime.fromtimestamp(actual_pair_ts)
|
||||||
|
dt_pair_finish = datetime.datetime.fromtimestamp(actual_pair_ts + 5400)
|
||||||
|
p = found_pairs[i]
|
||||||
|
|
||||||
|
human_readable_date = ""
|
||||||
|
if ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")):
|
||||||
|
human_readable_date += "завтра "
|
||||||
|
elif current_week != int(dt_pair.strftime("%W")) % 2:
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_NEXT[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
elif current_day != (int(dt_pair.strftime("%u")) - 1):
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_THIS[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
else:
|
||||||
|
human_readable_date += "сьогодні "
|
||||||
|
|
||||||
|
human_readable_date += "з "
|
||||||
|
human_readable_date += dt_pair.strftime("%H:%M")
|
||||||
|
|
||||||
|
human_readable_date += " до "
|
||||||
|
human_readable_date += dt_pair_finish.strftime("%H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
if preferences['name']:
|
||||||
|
result_text += f"Назва: {p['name']}\n"
|
||||||
|
if preferences['date']:
|
||||||
|
result_text += f"Дата: {human_readable_date}\n"
|
||||||
|
if preferences['teacher']:
|
||||||
|
result_text += f"Викладач: {p['teacher']}\n"
|
||||||
|
if preferences['link']:
|
||||||
|
result_text += f"Посилання на пару: {p['link']}\n"
|
||||||
|
|
||||||
|
result_text += "\n"
|
||||||
|
|
||||||
|
self.RESPONCE = result_text
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"start_on_boot": true,
|
||||||
|
"alias": "auto-schedule-pro",
|
||||||
|
"version": 1,
|
||||||
|
"index_file": "index.py",
|
||||||
|
"predefine": "predefine.py"
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
self.WEEKDAY_NAMES_ZNAH = ["понедулок", "вівторок", "середу", "четвер", "п'ятницю", "суботу", "неділю"]
|
||||||
|
self.WEEKDAY_NAMES_ROD_WITH_NEXT = ["наступного понеділка", "наступного вівторка", "наступної середи", "наступного четверга", "наступної п'ятниці", "наступної суботи", "наступної неділі"]
|
||||||
|
self.WEEKDAY_NAMES_ROD_WITH_THIS = ["цього понеділка", "цього вівторка", "цієї середи", "цього четверга", "цієї п'ятниці", "цієї суботи", "цієї неділі"]
|
||||||
|
self.current_seconds = 0
|
|
@ -0,0 +1,53 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"8:30": {"name": "Дискретна математика", "teacher": "Новотарський М. А.", "link": "https://us02web.zoom.us/j/87578307057?pwd=UGwyVGlwc3M4Q0Q0Q0NLWUt6bmVpUT09"},
|
||||||
|
"10:25": {"name": "Комп'ютерна логіка", "teacher": "Жабін В. І.", "link": "https://bbb.comsys.kpi.ua/b/val-2vb-o7w-y5y АБО https://bbb.ugrid.org/b/val-osi-lup-ou8"},
|
||||||
|
"12:20": {"name": "Культура мовлення та ділове мовлення", "teacher": "Онуфрієнко О. П.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"12:20": {"name": "Практичний курс іноземної мови. Частина 1", "teacher": "Шевченко О. М.", "link": "https://meet.google.com/bwg-pdnr-evh"},
|
||||||
|
"14:15": {"name": "Фізика", "teacher": "Федотов В. В. & Іванова І. М.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"8:30": {"name": "Програмування. Частина 2. Об'єктно-орієнтоване програмування", "teacher": "Алещенко О. В.", "link": "https://us02web.zoom.us/j/2711546637?pwd=Ry82RHp3SjV6WTZRMXl6WUNod25hUT09"},
|
||||||
|
"10:25": {"name": "Вища математика", "teacher": "Ординська З. П.", "link": "https://us04web.zoom.us/j/2684350438?pwd=kiOi3BrgbJHeYvkrx7qaSxa08J8m8O.1"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"10:25": {"name": "Вища математика", "teacher": "Ординська З. П.", "link": "https://us04web.zoom.us/j/2684350438?pwd=kiOi3BrgbJHeYvkrx7qaSxa08J8m8O.1"},
|
||||||
|
"12:20": {"name": "Фізика", "teacher": "Русаков В. Ф.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!", "container_id": "1"},
|
||||||
|
"14:15": {"name": "Програмування. Частина 2. Об'єктно-орієнтоване програмування", "teacher": "Алещенко О. В.", "link": "https://us02web.zoom.us/j/2711546637?pwd=Ry82RHp3SjV6WTZRMXl6WUNod25hUT09"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"10:25": {"name": "Фізика", "teacher": "Русаков В. Ф.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!", "container_id": "1"},
|
||||||
|
"12:20": {"name": "Дискретна математика", "teacher": "Пономаренко А. М.", "link": "https://us05web.zoom.us/j/7089075754?pwd=TWRlZmxyVlFiTWU1UGlVVU1XcFE0Zz09"},
|
||||||
|
"14:15": {"name": "Основи здорового способу життя", "teacher": "Соболенко А. І.", "link": "https://zoom.us/j/2035574145?pwd=bk1wTVhGbjJsQTR4WmVQMlROWFBCZz09"}
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"8:30": {"name": "Дискретна математика", "teacher": "Новотарський М. А.", "link": "https://us02web.zoom.us/j/87578307057?pwd=UGwyVGlwc3M4Q0Q0Q0NLWUt6bmVpUT09"},
|
||||||
|
"10:25": {"name": "Комп'ютерна логіка", "teacher": "Жабін В. І.", "link": "https://bbb.comsys.kpi.ua/b/val-2vb-o7w-y5y АБО https://bbb.ugrid.org/b/val-osi-lup-ou8"},
|
||||||
|
"12:20": {"name": "Вища математика", "teacher": "Ординська З. П.", "link": "https://us04web.zoom.us/j/2684350438?pwd=kiOi3BrgbJHeYvkrx7qaSxa08J8m8O.1"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"8:30": {"name": "Комп'ютерна логіка", "teacher": "Верба О. А.", "link": "https://us04web.zoom.us/j/7382214783?pwd=RnZ3SWgwK1JoVkZtNndnKzdPZjFGdz09"},
|
||||||
|
"10:25": {"name": "Вища математика", "teacher": "Ординська З. П.", "link": "https://us04web.zoom.us/j/2684350438?pwd=kiOi3BrgbJHeYvkrx7qaSxa08J8m8O.1"},
|
||||||
|
"12:20": {"name": "Практичний курс іноземної мови. Частина 1", "teacher": "Шевченко О. М.", "link": "https://meet.google.com/bwg-pdnr-evh"},
|
||||||
|
"14:15": {"name": "Фізика", "teacher": "Федотов В. В. & Іванова І. М.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"10:25": {"name": "Вища математика", "teacher": "Ординська З. П.", "link": "https://us04web.zoom.us/j/2684350438?pwd=kiOi3BrgbJHeYvkrx7qaSxa08J8m8O.1"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"10:25": {"name": "Вища математика", "teacher": "Ординська З. П.", "link": "https://us04web.zoom.us/j/2684350438?pwd=kiOi3BrgbJHeYvkrx7qaSxa08J8m8O.1"},
|
||||||
|
"12:20": {"name": "Фізика", "teacher": "Русаков В. Ф.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!", "container_id": "1"},
|
||||||
|
"14:15": {"name": "Програмування. Частина 2. Об'єктно-орієнтоване програмування", "teacher": "Алещенко О. В.", "link": "https://us02web.zoom.us/j/2711546637?pwd=Ry82RHp3SjV6WTZRMXl6WUNod25hUT09"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"10:25": {"name": "Фізика", "teacher": "Русаков В. Ф.", "link": "В житті не буває нічого вічного. Життя мінливе, як і посилання на кожну нову пару. Щасти вам його віднайти!", "container_id": "1"},
|
||||||
|
"12:20": {"name": "Культура мовлення та ділове мовлення", "teacher": "Кушлаба М. П.", "link": "https://bbb.comsys.kpi.ua/b/myk-0iw-red-p01"},
|
||||||
|
"14:15": {"name": "Основи здорового способу життя", "teacher": "Соболенко А. І.", "link": "https://zoom.us/j/2035574145?pwd=bk1wTVhGbjJsQTR4WmVQMlROWFBCZz09"}
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
|
@ -0,0 +1,165 @@
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
current_time = datetime.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
|
||||||
|
|
||||||
|
# baking defined schedule
|
||||||
|
raw_schedule = json.loads( open("../schedule.json").read() )
|
||||||
|
schedule = {}
|
||||||
|
for day in range(len(raw_schedule)):
|
||||||
|
for i in raw_schedule[day]:
|
||||||
|
ts = day*86400 + int(i.split(":")[0])*3600 + int(i.split(":")[1])*60
|
||||||
|
new_item = dict(raw_schedule[day][i])
|
||||||
|
new_item["source"] = "schedule"
|
||||||
|
schedule[ts] = new_item
|
||||||
|
|
||||||
|
# baking additions (extra pairs)
|
||||||
|
raw_additions = json.loads( open("../additions.json").read() )
|
||||||
|
additions = {}
|
||||||
|
for day in range(len(raw_additions)):
|
||||||
|
for i in raw_additions[day]:
|
||||||
|
ts = day*86400 + int(i.split(":")[0])*3600 + int(i.split(":")[1])*60
|
||||||
|
new_item = dict(raw_additions[day][i])
|
||||||
|
new_item["source"] = "additions"
|
||||||
|
schedule[ts] = new_item
|
||||||
|
|
||||||
|
full_schedule = dict(list(schedule.items()) + list(additions.items()))
|
||||||
|
|
||||||
|
#print("test1")
|
||||||
|
#print(f"Full schedule #printout: {full_schedule}")
|
||||||
|
#print(f"Current delta_time: {current_seconds}")
|
||||||
|
p = None
|
||||||
|
next_pair_time = None
|
||||||
|
|
||||||
|
key_list = list(full_schedule.keys())
|
||||||
|
key_list.sort()
|
||||||
|
for i in key_list:
|
||||||
|
if i > current_seconds - 5400:
|
||||||
|
p = full_schedule[i]
|
||||||
|
next_pair_time = i
|
||||||
|
break
|
||||||
|
|
||||||
|
#print("test2")
|
||||||
|
if next_pair_time == None:
|
||||||
|
if len(full_schedule.keys()) > 0:
|
||||||
|
#print("test3.1")
|
||||||
|
|
||||||
|
#actual_pair_ts = reference_time + min(full_schedule.keys())
|
||||||
|
#dt_pair = datetime.datetime.fromtimestamp(actual_pair_ts)
|
||||||
|
#dt_pair_finish = datetime.datetime.fromtimestamp(actual_pair_ts + 5400)
|
||||||
|
p = full_schedule[min(full_schedule.keys())]
|
||||||
|
#print("test3.1.1")
|
||||||
|
|
||||||
|
#print("{} == 6 && {} == 1, {}".format(current_day, dt_pair.strftime('%u'), str( ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")) )))
|
||||||
|
'''
|
||||||
|
human_readable_date = ""
|
||||||
|
if ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")):
|
||||||
|
human_readable_date += "завтра "
|
||||||
|
elif current_week != int(dt_pair.strftime("%W")) % 2:
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_NEXT[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
elif current_day != (int(dt_pair.strftime("%u")) - 1):
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_THIS[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
else:
|
||||||
|
human_readable_date += "сьогодні "
|
||||||
|
|
||||||
|
#print("test3.1.2")
|
||||||
|
|
||||||
|
human_readable_date += "з "
|
||||||
|
|
||||||
|
#print("test3.1.3")
|
||||||
|
human_readable_date += dt_pair.strftime("%H:%M")
|
||||||
|
#print("test3.1.4")
|
||||||
|
|
||||||
|
human_readable_date += " до "
|
||||||
|
human_readable_date += dt_pair_finish.strftime("%H:%M")
|
||||||
|
'''
|
||||||
|
|
||||||
|
#self.RESPONCE = "Актуальна пара: {}\nДата: {}\nВикладач: {}\nПосилання на пару: {}".format(p['name'], human_readable_date, p['teacher'], p['link'])
|
||||||
|
#print("test3.1.5")
|
||||||
|
if 'container_id' in p:
|
||||||
|
try:
|
||||||
|
cont = json.decode(open(f"../containers/{p['container_id']}", 'r').read())
|
||||||
|
if (time.time() - cont['update_ts']) > 43200:
|
||||||
|
if ("QUERY_STRING" in os.environ) and ("force" in os.environ['QUERY_STRING'].lower()):
|
||||||
|
print(f"Location: {cont['link']}\n\n", end = '')
|
||||||
|
else:
|
||||||
|
import random
|
||||||
|
new_seed = os.environ['REMOTE_ADDR'] + datetime.datetime.now().replace(minute = 0, second = 0).strftime("%s")
|
||||||
|
random.seed(new_seed)
|
||||||
|
surprise_pool = ["Йой!", "От халепа!", "Ой лишенько!"]
|
||||||
|
print(f"Content-Type: text/html; charset=UTF-8\n\n<h2>{random.choice(surprise_pool)}</h2><br><p>Посилання на пару {p['name']}, яке зберігається у сховищі, було отримане більш ніж 12 годин тому (рівно {time.time() - cont['update_ts']} секунд тому), тому, скоріш за все, не є дійсним.</p><p>На жаль, нового посилання ще не надходило, тому Ви можете або чекати на нього і оновлювати цю сторінку (перенаправлення станеться, щойно з'явиться нове посилання), або перейти вручну за старим посиланням (не рекомендується):</p><a href=\"{cont['link']}\">{cont['link']}</a><br><p>PS: щоб обійти цю сторінку та завжди автоматично переходити за будь-яким наявним посиланням, можна додати у рядок URL в кінці напис: ?force</p>")
|
||||||
|
else:
|
||||||
|
print(f"Location: {cont['link']}\n\n", end = '')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import random
|
||||||
|
new_seed = os.environ['REMOTE_ADDR'] + datetime.datetime.now().replace(minute = 0, second = 0).strftime("%s")
|
||||||
|
random.seed(new_seed)
|
||||||
|
surprise_pool = ["Йой!", "От халепа!", "Ой лишенько!"]
|
||||||
|
print(f"Content-Type: text/html; charset=UTF-8\n\n<h2>{random.choice(surprise_pool)}</h2><br><p>Під час спроби отримання посилання на пару {p['name']} сталася непередбачена помилка. Ви можете оновлювати сторінку, поки проблема не зникне (перенаправлення відбудеться, щойно все запрацює), або пошукати посилання де-інде.</p><p>Вибачте за тимчасові незручності(</p>")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Location: {p['link'].split()[0]}\n\n", end = '')
|
||||||
|
|
||||||
|
else:
|
||||||
|
#self.RESPONCE = "Пар немає взагалі. Ми вільні!"
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
#print("test3.2")
|
||||||
|
'''
|
||||||
|
actual_pair_ts = reference_time + next_pair_time
|
||||||
|
dt_pair = datetime.datetime.fromtimestamp(actual_pair_ts)
|
||||||
|
dt_pair_finish = datetime.datetime.fromtimestamp(actual_pair_ts + 5400)
|
||||||
|
|
||||||
|
human_readable_date = ""
|
||||||
|
if ((current_day + 2) == int(dt_pair.strftime("%u"))) or ((str(current_day) == "6") and (dt_pair.strftime("%u") == "1")):
|
||||||
|
human_readable_date += "завтра "
|
||||||
|
elif current_week != int(dt_pair.strftime("%W")) % 2:
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_NEXT[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
elif current_day != (int(dt_pair.strftime("%u")) - 1):
|
||||||
|
human_readable_date += "{} ".format(self.WEEKDAY_NAMES_ROD_WITH_THIS[int(dt_pair.strftime("%u")) - 1])
|
||||||
|
else:
|
||||||
|
human_readable_date += "сьогодні "
|
||||||
|
|
||||||
|
human_readable_date += "з "
|
||||||
|
human_readable_date += dt_pair.strftime("%H:%M")
|
||||||
|
|
||||||
|
human_readable_date += " до "
|
||||||
|
human_readable_date += dt_pair_finish.strftime("%H:%M")
|
||||||
|
'''
|
||||||
|
|
||||||
|
#self.RESPONCE = "Актуальна пара: {}\nДата: {}\nВикладач: {}\nПосилання на пару: {}".format(p['name'], human_readable_date, p['teacher'], p['link'])
|
||||||
|
|
||||||
|
if 'container_id' in p:
|
||||||
|
try:
|
||||||
|
cont = json.decode(open(f"../containers/{p['container_id']}", 'r').read())
|
||||||
|
if (time.time() - cont['update_ts']) > 43200:
|
||||||
|
if ("QUERY_STRING" in os.environ) and ("force" in os.environ['QUERY_STRING'].lower()):
|
||||||
|
print(f"Location: {cont['link']}\n\n", end = '')
|
||||||
|
else:
|
||||||
|
import random
|
||||||
|
new_seed = os.environ['REMOTE_ADDR'] + datetime.datetime.now().replace(minute = 0, second = 0).strftime("%s")
|
||||||
|
random.seed(new_seed)
|
||||||
|
surprise_pool = ["Йой!", "От халепа!", "Ой лишенько!"]
|
||||||
|
print(f"Content-Type: text/html; charset=UTF-8\n\n<h2>{random.choice(surprise_pool)}</h2><br><p>Посилання на пару {p['name']}, яке зберігається у сховищі, було отримане більш ніж 12 годин тому (рівно {time.time() - cont['update_ts']} секунд тому), тому, скоріш за все, не є дійсним.</p><p>На жаль, нового посилання ще не надходило, тому Ви можете або чекати на нього і оновлювати цю сторінку (перенаправлення станеться, щойно з'явиться нове посилання), або перейти вручну за старим посиланням (не рекомендується):</p><a href=\"{cont['link']}\">{cont['link']}</a><br><p>PS: щоб обійти цю сторінку та завжди автоматично переходити за будь-яким наявним посиланням, можна додати у рядок URL в кінці напис: ?force</p>")
|
||||||
|
else:
|
||||||
|
print(f"Location: {cont['link']}\n\n", end = '')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import random
|
||||||
|
new_seed = os.environ['REMOTE_ADDR'] + datetime.datetime.now().replace(minute = 0, second = 0).strftime("%s")
|
||||||
|
random.seed(new_seed)
|
||||||
|
surprise_pool = ["Йой!", "От халепа!", "Ой лишенько!"]
|
||||||
|
print(f"Content-Type: text/html; charset=UTF-8\n\n<h2>{random.choice(surprise_pool)}</h2><br><p>Під час спроби отримання посилання на пару {p['name']} сталася непередбачена помилка. Ви можете оновлювати сторінку, поки проблема не зникне (перенаправлення відбудеться, щойно все запрацює), або пошукати посилання де-інде.</p><p>Вибачте за тимчасові незручності(</p>")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Location: {p['link'].split()[0]}\n\n", end = '')
|
||||||
|
|
||||||
|
#print(f"Location: {p['link'].split()[0]}\n\n")
|
|
@ -1,4 +1,4 @@
|
||||||
if self.MESSAGE["text"].lower() == "!пара":
|
if self.MESSAGE["text"].lower() == "!пара-old":
|
||||||
try:
|
try:
|
||||||
schedule = json.loads( readfile(self.path + "schedule.json") )
|
schedule = json.loads( readfile(self.path + "schedule.json") )
|
||||||
|
|
||||||
|
@ -42,4 +42,4 @@ if self.MESSAGE["text"].lower() == "!пара":
|
||||||
self.RESPONCE = "Сьогодні більше немає пар"
|
self.RESPONCE = "Сьогодні більше немає пар"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[WARN] module: auto-schedule: failed to process schedule.json ({e})")
|
print(f"[WARN] module: auto-schedule: failed to process schedule.json ({e})")
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
"lector": "Новотарський М. А."
|
"lector": "Новотарський М. А."
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
"link": "https://bbb.comsys.kpi.ua/b/val-zdp-vw0-dbr",
|
"link": "https://bbb.ugrid.org/b/val-zdp-vw0-dbr",
|
||||||
"subject": "Комп'ютерна логіка",
|
"subject": "Комп'ютерна логіка",
|
||||||
"lector": "Жабін В. І."
|
"lector": "Жабін В. І."
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
"lector": "Новотарський М. А."
|
"lector": "Новотарський М. А."
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
"link": "https://bbb.comsys.kpi.ua/b/val-zdp-vw0-dbr",
|
"link": "https://bbb.ugrid.org/b/val-zdp-vw0-dbr",
|
||||||
"subject": "Комп'ютерна логіка",
|
"subject": "Комп'ютерна логіка",
|
||||||
"lector": "Жабін В. І."
|
"lector": "Жабін В. І."
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
|
||||||
|
login_lines_raw = [
|
||||||
|
"NICK bridge",
|
||||||
|
"USER bridge bridge * !",
|
||||||
|
"JOIN #io23-bridge"
|
||||||
|
]
|
||||||
|
|
||||||
|
login_lines = map( lambda x: (x + "\n").encode("utf-8"), login_lines_raw )
|
||||||
|
|
||||||
|
recv_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
send_buffer = []
|
||||||
|
reconnecting = False
|
||||||
|
|
||||||
|
LINE_END = "\r\n".encode("utf-8")
|
||||||
|
|
||||||
|
recv_s.bind( ("127.0.0.1", 5001) )
|
||||||
|
|
||||||
|
def reconnect():
|
||||||
|
global s, reconnecting
|
||||||
|
reconnecting = True
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
s.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.connect( ("10.1.1.1", 6667) )
|
||||||
|
|
||||||
|
for i in login_lines:
|
||||||
|
s.send(i)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
reconnecting = False
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[reconnect:main] ERROR: got exception {e}")
|
||||||
|
t = time.time()
|
||||||
|
open("error.log", "a", encoding = "utf-8").write(f"[{t}] recv_thread:main ERROR: got exception {e}\n")
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def recv_thread():
|
||||||
|
global s, send_buffer, reconnecting
|
||||||
|
databuffer = bytes()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
new_data = s.recv(1024)
|
||||||
|
print(f"[DEBUG] new_data: {new_data}")
|
||||||
|
if len(new_data) == 0:
|
||||||
|
if not reconnecting:
|
||||||
|
reconnect() # bin the old socket and get a new one instead
|
||||||
|
continue
|
||||||
|
|
||||||
|
databuffer += new_data
|
||||||
|
|
||||||
|
if LINE_END in databuffer:
|
||||||
|
commands = map( lambda x: x.decode("UTF-8"), databuffer.split(LINE_END)[:-1])
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
if len(command) > 0:
|
||||||
|
if command.split(" ")[0] == "PING":
|
||||||
|
print("[DEBUG] recv_thread:command:ping: Responding with {}".format(command.split(" ")[1]).encode("UTF-8"))
|
||||||
|
send_buffer.append(("PONG {}\n".format( command.split(" ")[1]).encode("UTF-8") ))
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
databuffer = databuffer.split(LINE_END)[-1]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[recv_thread:main] ERROR: got exception {e}")
|
||||||
|
t = time.time()
|
||||||
|
open("error.log", "a", encoding = "utf-8").write(f"[{t}] recv_thread:main ERROR: got exception {e}\n")
|
||||||
|
try:
|
||||||
|
if not reconnecting:
|
||||||
|
reconnect()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[recv_thread:except:reconnect] ERROR: got exception {e}")
|
||||||
|
t = time.time()
|
||||||
|
open("error.log", "a", encoding = "utf-8").write(f"[{t}] recv_thread:except:reconnect ERROR: got exception {e}\n")
|
||||||
|
|
||||||
|
def send_thread():
|
||||||
|
global s, send_buffer, reconnecting
|
||||||
|
while True:
|
||||||
|
if len(send_buffer) > 0:
|
||||||
|
try:
|
||||||
|
print(f"[DEBUG] send_thread: sending {send_buffer[0]}")
|
||||||
|
s.send(send_buffer[0])
|
||||||
|
del send_buffer[0]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[send_thread:main] ERROR: got exception {e}")
|
||||||
|
t = time.time()
|
||||||
|
open("error.log", "a", encoding = "utf-8").write(f"[{t}] send_thread:main ERROR: got exception {e}\n")
|
||||||
|
|
||||||
|
if not reconnecting:
|
||||||
|
reconnect()
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
else:
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
def msg_thread():
|
||||||
|
global recv_s, send_buffer
|
||||||
|
while True:
|
||||||
|
d = recv_s.recvfrom(16384)
|
||||||
|
print(f"[DEBUG] msg_thread:d: {d}")
|
||||||
|
send_buffer.append(d[0])
|
||||||
|
|
||||||
|
st = threading.Thread(target = send_thread, args = [])
|
||||||
|
rt = threading.Thread(target = recv_thread, args = [])
|
||||||
|
mt = threading.Thread(target = msg_thread, args = [])
|
||||||
|
|
||||||
|
reconnect()
|
||||||
|
|
||||||
|
st.start()
|
||||||
|
rt.start()
|
||||||
|
mt.start()
|
|
@ -0,0 +1,10 @@
|
||||||
|
if self.MESSAGE["chat"]["id"] == -1001570221452:
|
||||||
|
l = min( len(self.MESSAGE["text"]), 300 )
|
||||||
|
new_msg = "{} {}: {}".format(self.MESSAGE.from_user.first_name,\
|
||||||
|
self.MESSAGE.from_user.last_name,\
|
||||||
|
self.MESSAGE.text.replace("\n", " | ")[:l])
|
||||||
|
|
||||||
|
import socket
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.sendto(f"PRIVMSG #io23-bridge :{new_msg}\n".encode("utf-8"), ("127.0.0.1", 5001))
|
||||||
|
del s
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"start_on_boot": true,
|
||||||
|
"alias": "irc-bridge",
|
||||||
|
"version": 1,
|
||||||
|
"index_file": "index.py",
|
||||||
|
"predefine": "predefine.py"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import socket
|
||||||
|
|
||||||
|
SEND_ADDR = ( "127.0.0.1", 5001 )
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
@ -0,0 +1,22 @@
|
||||||
|
if self.MESSAGE['text'].split()[0] == "!decode" and (2 <= len(self.MESSAGE['text'].split(" ", 2)) <= 3):
|
||||||
|
try:
|
||||||
|
models = json.loads(readfile(self.path + "translate_models.json"))
|
||||||
|
chosen_model = self.MESSAGE['text'].split(" ", 2)[1]
|
||||||
|
|
||||||
|
if len(self.MESSAGE['text'].split(" ", 2)) == 3:
|
||||||
|
t = self.MESSAGE['text'].split(" ", 2)[2]
|
||||||
|
elif len(self.MESSAGE['text'].split(" ", 2)) == 2:
|
||||||
|
t = self.MESSAGE['reply_to_message']['text']
|
||||||
|
|
||||||
|
translated_t = t
|
||||||
|
if chosen_model not in models:
|
||||||
|
self.RESPONCE = f"Такого варіанту транслітерації не існує. Доступні варіанти: {', '.join(list(models.keys()))}"
|
||||||
|
else:
|
||||||
|
for i in models[chosen_model]:
|
||||||
|
translated_t = translated_t.replace(i[0], i[1])
|
||||||
|
translated_t = translated_t.replace(i[0].capitalize(), i[1].capitalize())
|
||||||
|
translated_t = translated_t.replace(i[0].upper(), i[1].upper())
|
||||||
|
|
||||||
|
self.RESPONCE = f"Результат: {translated_t}"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[translit-decoder] Got exception: {e}")
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"index_file": "index.py",
|
||||||
|
"start_on_boot": true,
|
||||||
|
"alias": "translit-decoder"
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"cz-ua": [
|
||||||
|
["šč", "щ"],
|
||||||
|
["ja", "я"],
|
||||||
|
["ju", "ю"],
|
||||||
|
["ji", "ї"],
|
||||||
|
["je", "є"],
|
||||||
|
["f", "ф"],
|
||||||
|
["b", "б"],
|
||||||
|
["ž", "ж"],
|
||||||
|
["q", "к"],
|
||||||
|
["š", "ш"],
|
||||||
|
["n", "н"],
|
||||||
|
["p", "п"],
|
||||||
|
["r", "р"],
|
||||||
|
["s", "с"],
|
||||||
|
["t", "т"],
|
||||||
|
["h", "х"],
|
||||||
|
["č", "ч"],
|
||||||
|
["'", "ь"],
|
||||||
|
["y", "и"],
|
||||||
|
["z", "з"],
|
||||||
|
["l", "л"],
|
||||||
|
["k", "к"],
|
||||||
|
["d", "д"],
|
||||||
|
["u", "у"],
|
||||||
|
["m", "м"],
|
||||||
|
["v", "в"],
|
||||||
|
["j", "й"],
|
||||||
|
["s", "с"],
|
||||||
|
["a", "а"],
|
||||||
|
["i", "і"],
|
||||||
|
["g", "г"]
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue