From 5a523c9b0447bfa50ad6a51342f5f417ae9d34f7 Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 15 Sep 2022 13:05:44 +0200 Subject: [PATCH] Multilingual support, custom paths --- bot.py | 122 +++++++++++++++++++++++++++++------ config.json | 8 ++- locale/en.json | 47 ++++++++++++++ locale/uk.json | 62 ++++++++++++++++++ modules/functions.py | 41 ++++++++---- modules/functions_bot.py | 136 +++++++++++++++++++++++++++++++-------- 6 files changed, 355 insertions(+), 61 deletions(-) create mode 100644 locale/uk.json diff --git a/bot.py b/bot.py index 80774b7..82c5878 100644 --- a/bot.py +++ b/bot.py @@ -11,69 +11,151 @@ intents = discord.Intents().all() client = discord.Bot(intents=intents) -def makeEmbed(title="", description="", footer="", color=0xffffff): +def makeEmbed(title="", description="", footer="", color=0xffffff) -> discord.Embed: embed=Embed(title=title, description=description, color=color) if footer is not None: embed.set_footer(text=footer) return embed + @client.event async def on_ready(): print(f"Logged in as {client.user}") - await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="end-play.xyz/autozoom")) + await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=configGet("activity"))) -@client.slash_command(name="link", description="Connect to your AutoZoom") -async def link(ctx: discord.ApplicationContext, code: discord.Option(str, "Code you got in AutoZoom app", required=True)): - logWrite(f'Got command start/link from {ctx.author.id}') +@client.slash_command(name="link", + description=locale("description", "cmd", "link", locale=configGet("locale")), + name_localizations=localeName("link", None), + description_localizations=localeDescription("link", None) +) +async def link(ctx: discord.ApplicationContext, + code: + discord.Option(str, + locale("description", "cmd", "link", "options", "code", locale=configGet("locale")), + required=True, + name_localizations=localeName("link", "code"), + description_localizations=localeDescription("link", "code"), + ) + ): - if f"{ctx.author.id}.json" not in listdir(f"data{sep}users{sep}"): - logWrite(f'Creating blank data file for {ctx.author.id}') - jsonSave( f"data{sep}users{sep}{ctx.author.id}.json", {"api_key": None, "linked": False, "context": {"action": None, "data": None}} ) + logWrite(f'Got command start/link from {ctx.author.id}', logs_folder=configGet("logs")) + + if isinstance(ctx.channel, discord.DMChannel) or isinstance(ctx.channel, discord.channel.PartialMessageable): + await ctx.defer() + else: + await ctx.defer(ephemeral=True) + + data = configGet("data") + + if f"{ctx.author.id}.json" not in listdir(f"{data}{sep}users{sep}"): + logWrite(f'Creating blank data file for {ctx.author.id}', logs_folder=configGet("logs")) + jsonSave( f"{data}{sep}users{sep}{ctx.author.id}.json", {"api_key": None, "linked": False, "context": {"action": None, "data": None}} ) if not userGet(ctx.author.id, "linked"): if code in jsonLoad(configGet("api_keys"))["autozoom"]: await ctx.respond(embed=makeEmbed(title=locale("key_correct", "msg"), description=locale("key_correct_text", "msg"), color=0x45d352)) userSet(ctx.author.id, "api_key", code) userSet(ctx.author.id, "linked", True) - keys_storage = jsonLoad(f"data{sep}keys_storage.json") + keys_storage = jsonLoad(f"{data}{sep}keys_storage.json") keys_storage[code] = ctx.author.id - jsonSave(f"data{sep}keys_storage.json", keys_storage) - logWrite(f"Added apikey {code} for user {ctx.author.id}") + jsonSave(f"{data}{sep}keys_storage.json", keys_storage) + logWrite(f"Added apikey {code} for user {ctx.author.id}", logs_folder=configGet("logs")) else: - logWrite(f"User {ctx.author.id} tried to pair with invalid apikey {code}") + logWrite(f"User {ctx.author.id} tried to pair with invalid apikey {code}", logs_folder=configGet("logs")) await ctx.respond(embed=makeEmbed(title=locale("key_wrong", "msg"), description=locale("key_wrong_text", "msg"), color=0xe06044)) else: await ctx.respond(embed=makeEmbed(title=locale("already_linked", "msg"), description=locale("already_linked_text", "msg"), color=0xe06044)) -@client.slash_command(name="unlink", description="Disconnect from your AutoZoom") + +@client.slash_command(name="unlink", + description=locale("description", "cmd", "unlink", locale=configGet("locale")), + name_localizations=localeName("unlink", None), + description_localizations=localeDescription("unlink", None) +) async def unlink(ctx: discord.ApplicationContext): - logWrite(f'Got command ulink from {ctx.author.id}') + logWrite(f'Got command unlink from {ctx.author.id}', logs_folder=configGet("logs")) + + if isinstance(ctx.channel, discord.DMChannel) or isinstance(ctx.channel, discord.channel.PartialMessageable): + await ctx.defer() + else: + await ctx.defer(ephemeral=True) + + data = configGet("data") if not userGet(ctx.author.id, "linked"): await ctx.respond(embed=makeEmbed(title=locale("not_linked", "msg"), description=locale("not_linked_text", "msg"), color=0xe06044)) else: try: - keys_storage = jsonLoad(f"data{sep}keys_storage.json") + keys_storage = jsonLoad(f"{data}{sep}keys_storage.json") del keys_storage[userGet(ctx.author.id, "api_key")] - jsonSave(f"data{sep}keys_storage.json", keys_storage) + jsonSave(f"{data}{sep}keys_storage.json", keys_storage) except: pass userClear(ctx.author.id, "api_key") userSet(ctx.author.id, "linked", False) await ctx.respond(embed=makeEmbed(title=locale("unlinked", "msg"), description=locale("unlinked_text", "msg"), color=0x45d352)) + -@client.slash_command(name="meeting", description="Add new Zoom meeting") -async def meeting(ctx: discord.ApplicationContext, title: discord.Option(str, "Meeting title", required=True), day: discord.Option(str, "Day formatted as dd.mm.yyyy", required=True), time: discord.Option(str, "Time formatted as hh:mm", required=True), link: discord.Option(str, "Direct meeting link", required=True), repeat: discord.Option(bool, "Repeat meeting this weekday", required=True), record: discord.Option(bool, "Record meeting using app", required=True)): +@client.slash_command(name="meeting", + description=locale("description", "cmd", "meeting", locale=configGet("locale")), + name_localizations=localeName("meeting", None), + description_localizations=localeDescription("meeting", None) +) +async def meeting(ctx: discord.ApplicationContext, + title: discord.Option(str, + locale("description", "cmd", "meeting", "options", "title", locale=configGet("locale")), + required=True, + name_localizations=localeName("meeting", "title"), + description_localizations=localeDescription("meeting", "title") + ), + day: discord.Option(str, + locale("description", "cmd", "meeting", "options", "day", locale=configGet("locale")), + required=True, + name_localizations=localeName("meeting", "day"), + description_localizations=localeDescription("meeting", "day") + ), + time: discord.Option(str, + locale("description", "cmd", "meeting", "options", "time", locale=configGet("locale")), + required=True, + name_localizations=localeName("meeting", "time"), + description_localizations=localeDescription("meeting", "time") + ), + link: discord.Option(str, + locale("description", "cmd", "meeting", "options", "link", locale=configGet("locale")), + required=True, + name_localizations=localeName("meeting", "link"), + description_localizations=localeDescription("meeting", "link") + ), + repeat: discord.Option(bool, + locale("description", "cmd", "meeting", "options", "repeat", locale=configGet("locale")), + required=True, + name_localizations=localeName("meeting", "repeat"), + description_localizations=localeDescription("meeting", "repeat") + ), + record: discord.Option(bool, + locale("description", "cmd", "meeting", "options", "record", locale=configGet("locale")), + required=True, + name_localizations=localeName("meeting", "record"), + description_localizations=localeDescription("meeting", "record") + ) + ): - logWrite(f'Got command meeting from {ctx.author.id}') + logWrite(f'Got command meeting from {ctx.author.id}', logs_folder=configGet("logs")) + + if isinstance(ctx.channel, discord.DMChannel) or isinstance(ctx.channel, discord.channel.PartialMessageable): + await ctx.defer() + else: + await ctx.defer(ephemeral=True) + + data = configGet("data") return if configGet("token") != "INSERT-TOKEN": client.run(configGet("token")) else: - logWrite("Could not start the bot. Please, configure token in config.json") + logWrite("Could not start the bot. Please, configure token in config.json", logs_folder=configGet("logs")) exit() \ No newline at end of file diff --git a/config.json b/config.json index dec8477..6b2f4a8 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,9 @@ { "locale": "en", - "api_keys": "data/api_keys.json", - "token": "INSERT-TOKEN" + "token": "INSERT-TOKEN", + "activity": "end-play.xyz/autozoom", + "api_keys": "/home/user/AutoZoomAPI/data/api_keys.json", + "data": "data", + "locales": "locale", + "logs": "logs" } diff --git a/locale/en.json b/locale/en.json index 87f3850..613970d 100644 --- a/locale/en.json +++ b/locale/en.json @@ -11,5 +11,52 @@ "not_linked_text": "You need to `/link` your account to AutoZoom application first.", "unlinked": "Account successfully unlinked", "unlinked_text": "You can now `/link` it to another AutoZoom application if you want." + }, + "cmd": { + "link": { + "name": "link", + "description": "Connect to your AutoZoom", + "options": { + "code": { + "name": "code", + "description": "Code you got in AutoZoom app" + } + } + }, + "unlink": { + "name": "unlink", + "description": "Disconnect from your AutoZoom", + "options": {} + }, + "meeting": { + "name": "meeting", + "description": "Add new Zoom meeting", + "options": { + "title": { + "name": "title", + "description": "Meeting title" + }, + "day": { + "name": "day", + "description": "Day formatted as dd.mm.yyyy" + }, + "time": { + "name": "time", + "description": "Time formatted as hh:mm" + }, + "link": { + "name": "link", + "description": "Direct meeting link" + }, + "repeat": { + "name": "repeat", + "description": "Repeat meeting this weekday" + }, + "record": { + "name": "record", + "description": "Record meeting using app" + } + } + } } } \ No newline at end of file diff --git a/locale/uk.json b/locale/uk.json new file mode 100644 index 0000000..9f402b6 --- /dev/null +++ b/locale/uk.json @@ -0,0 +1,62 @@ +{ + "btn": {}, + "msg": { + "key_correct": "Обліковий запис успішно прив’язано", + "key_correct_text": "Тепер ви отримуватимете сповіщення про всі зустрічі. Ви також можете додавати нові зустрічі до програми AutoZoom за допомогою команди `/meeting`.", + "key_wrong": "Особистий ключ неправильний", + "key_wrong_text": "Отримайте ключ зв’язування за допомогою програми AutoZoom і повторіть спробу за допомогою команди бота `/link`.\n\n**Потрібна допомога?**\nДізнайтеся, як працює зв’язування, на нашому веб-сайті (https://www.end-play.xyz/autozoom/bot) або зверніться до нашої служби підтримки (https://support.end-play.xyz), якщо сторінка посібника вам не допомогла.", + "already_linked": "Обліковий запис уже прив’язано", + "already_linked_text": "Якщо ви хочете змінити свій особистий ключ, вам потрібно спочатку `/unlink` свій обліковий запис і спробувати `/link` його ще раз.", + "not_linked": "Обліковий запис не прив'язано", + "not_linked_text": "Спершу вам потрібно `/link` свій обліковий запис із програмою AutoZoom.", + "unlinked": "Обліковий запис успішно прив’язано", + "unlinked_text": "Тепер ви можете `/link` його з іншим AutoZoom, якщо хочете." + }, + "cmd": { + "link": { + "name": "link", + "description": "Прив’язати до AutoZoom", + "options": { + "code": { + "name": "код", + "description": "Код отриманий в додатку AutoZoom" + } + } + }, + "unlink": { + "name": "unlink", + "description": "Відв’язати від AutoZoom", + "options": {} + }, + "meeting": { + "name": "meeting", + "description": "Додати нову конференцію", + "options": { + "title": { + "name": "назва", + "description": "Назва конференції" + }, + "day": { + "name": "дата", + "description": "Дата у форматі дд.мм.рррр" + }, + "time": { + "name": "час", + "description": "Час у форматі гг:хх" + }, + "link": { + "name": "посилання", + "description": "Пряме посилання на конференцію" + }, + "repeat": { + "name": "повторювати", + "description": "Повторювати конференцію в цей день тижня" + }, + "record": { + "name": "записувати", + "description": "Записувати конференцію" + } + } + } + } +} \ No newline at end of file diff --git a/modules/functions.py b/modules/functions.py index 55eb072..243b96c 100644 --- a/modules/functions.py +++ b/modules/functions.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- """Some set of functions needed for discord/telegram bots and other types of apps""" -from os import sep, stat, makedirs +from os import sep, stat, makedirs, kill +from os import name as osname from datetime import datetime +from typing import Union from ujson import loads, dumps from shutil import copyfileobj from gzip import open as gzipopen +from psutil import Process -def nowtimeGet(format="%H:%M:%S | %d.%m.%Y"): +def nowtimeGet(format="%H:%M:%S | %d.%m.%Y") -> str: """Return current local time formatted as arg. ### Args: @@ -20,7 +23,7 @@ def nowtimeGet(format="%H:%M:%S | %d.%m.%Y"): return datetime.now().strftime(format) -def checkSize(logs_folder=f"logs{sep}", log_size=1024): +def checkSize(logs_folder=f"logs{sep}", log_size=1024) -> None: """Checks latest log file size and rotates it if needed. ### Args: @@ -51,7 +54,7 @@ def checkSize(logs_folder=f"logs{sep}", log_size=1024): i += 1 -def logWrite(message, logs_folder=f"logs{sep}", level="INFO"): +def logWrite(message: str, logs_folder=f"logs{sep}", level="INFO") -> None: """Append some message to latest log file. ### Args: @@ -79,28 +82,42 @@ def logWrite(message, logs_folder=f"logs{sep}", level="INFO"): log.close() -def jsonSave(filename, value): +def jsonSave(filename: str, value: Union[list, dict]) -> None: """Save some list or dict as json file. - Args: + ### Args: * filename (str): File to which value will be written. - * value (list or dict): Some object that will be written to filename. + * value (Union[list, dict]): Some object that will be written to filename. """ with open(filename, 'w', encoding="utf-8") as f: f.write(dumps(value, indent=4, ensure_ascii=False)) f.close() -def jsonLoad(filename): +def jsonLoad(filename: str) -> any: """Load json file and return python dict or list. - Args: + ### Args: * filename (str): File which should be loaded. - Returns: - * list or dict: Content of json file provided. + ### Returns: + * any: Content of json file provided. """ with open(filename, 'r', encoding="utf-8") as f: value = loads(f.read()) f.close() - return value \ No newline at end of file + return value + + +def killProc(pid: int) -> None: + """Kill the process by its PID + + ### Args: + * pid (int): Process ID to be killed + """ + if osname == "posix": + from signal import SIGKILL + kill(pid, SIGKILL) + else: + p = Process(pid) + p.kill() \ No newline at end of file diff --git a/modules/functions_bot.py b/modules/functions_bot.py index efac870..2422354 100644 --- a/modules/functions_bot.py +++ b/modules/functions_bot.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- """Essential set of functions needed for discord/telegram bots and other types of apps""" +from typing import Union from modules.functions import jsonLoad, jsonSave -from os import sep +from os import sep, listdir -def configGet(key: str, *args: str): + +def configGet(key: str, *args: str) -> any: """Get value of the config key - Args: + ### Args: * key (str): The last key of the keys path. * *args (str): Path to key like: dict[args][key]. - Returns: + ### Returns: * any: Value of provided key """ this_dict = jsonLoad("config.json") @@ -18,11 +20,12 @@ def configGet(key: str, *args: str): this_key = this_key[dict_key] return this_key[key] -def configAppend(key: str, value, *args: str): + +def configAppend(key: str, value: Union[str, float, int, bool, dict, list, None], *args: str) -> None: """Set key to a value - Args: + ### Args: * key (str): The last key of the keys path. - * value (str/int/float/list/dict/None): Some needed value. + * value (Union[str, float, int, bool, dict, list, NoneType]): Some needed value. * *args (str): Path to key like: dict[args][key]. """ this_dict = jsonLoad("config.json") @@ -37,34 +40,40 @@ def configAppend(key: str, value, *args: str): jsonSave(this_dict, "config.json") return -def configRemove(key, value): + +def configRemove(key: str, value: Union[str, float, int, bool, dict, list, None]) -> None: + """Remove value from config's list key + + ### Args: + * key (str): The last key of the keys path. + * value (Union[str, float, int, bool, dict, list, NoneType]): Some needed value. + """ config = jsonLoad("config.json") config[key].remove(value) jsonSave("config.json", config) -def locale(key: str, *args: str, locale=configGet("locale")): +def locale(key: str, *args: str, locale=configGet("locale")) -> str: """Get value of locale string - Args: + ### Args: * key (str): The last key of the locale's keys path. * *args (list): Path to key like: dict[args][key]. * locale (str): Locale to looked up in. Defaults to config's locale value. - Returns: + ### Returns: * any: Value of provided locale key """ if (locale == None): locale = configGet("locale") + + locales = configGet("locales") try: - this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{locale}.json') + this_dict = jsonLoad(f'{locales}{sep}{locale}.json') except FileNotFoundError: try: - this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{configGet("locale")}.json') + this_dict = jsonLoad(f'{locales}{sep}{configGet("locale")}.json') except FileNotFoundError: - try: - this_dict = jsonLoad(f'{configGet("locale_fallback", "locations")}{sep}{configGet("locale")}.json') - except: - return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' + return f'⚠ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' this_key = this_dict for dict_key in args: @@ -73,26 +82,99 @@ def locale(key: str, *args: str, locale=configGet("locale")): try: return this_key[key] except KeyError: - return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' + return f'⚠ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' -def userSet(userid, key, value): - user = jsonLoad(f"data{sep}users{sep}{userid}.json") +def userSet(userid: Union[str, int], key: str, value: Union[str, float, int, bool, dict, list, None]) -> None: + """Set user's variable + + ### Args: + * userid (Union[str, int]): ID of a user. + * key (str): Key of a user's variable. + * value (Union[str, float, int, bool, dict, list, NoneType]): Some needed value. + """ + data = configGet("data") + user = jsonLoad(f"{data}{sep}users{sep}{userid}.json") user[key] = value - jsonSave(f"data{sep}users{sep}{userid}.json", user) + jsonSave(f"{data}{sep}users{sep}{userid}.json", user) -def userGet(userid, key): + +def userGet(userid: Union[str, int], key: str) -> any: + """Get user's variable + + ### Args: + * userid (Union[str, int]): ID of a user. + * key (str): Key of a user's variable. + + ### Returns: + * any: Value of requested key or None + """ + data = configGet("data") try: - return jsonLoad(f"data{sep}users{sep}{userid}.json")[key] + return jsonLoad(f"{data}{sep}users{sep}{userid}.json")[key] except KeyError: return None except FileNotFoundError: return None -def userClear(userid, key): + +def userClear(userid: Union[str, int], key: str) -> None: + """Clear user's variable + + ### Args: + * userid (Union[str, int]): ID of a user. + * key (str): Key of a user's variable. + """ + data = configGet("data") try: - user = jsonLoad(f"data{sep}users{sep}{userid}.json") + user = jsonLoad(f"{data}{sep}users{sep}{userid}.json") del user[key] - jsonSave(f"data{sep}users{sep}{userid}.json", user) + jsonSave(f"{data}{sep}users{sep}{userid}.json", user) except KeyError: - pass \ No newline at end of file + pass + + +def localeName(command: str, option: Union[str, None]): + """Get name of a command or command's option + + ### Args: + * command (str): Command that is set in locale file + * option (Union[str, NoneType]): Option's name or None (if command's name requested) + + ### Returns: + * str: Name of a command or an option + """ + output = {} + locales = configGet("locales") + for entry in listdir(locales): + if entry.endswith(".json"): + if entry.replace(".json", "") != configGet("locale"): + all_commands = locale("cmd", locale=entry.replace(".json", "")) + if option != None: + output[entry.replace(".json", "")] = all_commands[command]["options"][option]["name"] + else: + output[entry.replace(".json", "")] = all_commands[command]["name"] + return output + + +def localeDescription(command: str, option: Union[str, None]): + """Get description of a command or command's option + + ### Args: + * command (str): Command that is set in locale file + * option (Union[str, NoneType]): Option's name or None (if command's description requested) + + ### Returns: + * str: Description of a command or an option + """ + output = {} + locales = configGet("locales") + for entry in listdir(locales): + if entry.endswith(".json"): + if entry.replace(".json", "") != configGet("locale"): + all_commands = locale("cmd", locale=entry.replace(".json", "")) + if option != None: + output[entry.replace(".json", "")] = all_commands[command]["options"][option]["description"] + else: + output[entry.replace(".json", "")] = all_commands[command]["description"] + return output \ No newline at end of file