Multilingual support, custom paths

This commit is contained in:
Profitroll 2022-09-15 13:05:44 +02:00
parent c4481c8baa
commit 5a523c9b04
6 changed files with 355 additions and 61 deletions

122
bot.py
View File

@ -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)):
logWrite(f'Got command meeting from {ctx.author.id}')
@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}', 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()

View File

@ -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"
}

View File

@ -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"
}
}
}
}
}

62
locale/uk.json Normal file
View File

@ -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": "Записувати конференцію"
}
}
}
}
}

View File

@ -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
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()

View File

@ -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
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