diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cb1e206 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.env +**\__pycache__ +*.json +**.jpg +Dockerfile +venv +dev*.txt +ressources +.gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6128871 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +venv +**/__pycache__ +*.json +dev*.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6666cbd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12.5-alpine3.20 + +COPY requirements.txt /app/ + +WORKDIR /app + +RUN apk update && \ + apk add gcc musl-dev linux-headers libc-dev libffi-dev openssl-dev make && \ + pip install --default-timeout=100 --upgrade pip && \ + pip install --default-timeout=100 -r requirements.txt + +ENV LANG=fr_FR.UTF-8 +ENV LC_ALL=fr_FR.UTF-8 + +COPY . /app/ + +CMD ["python", "-u", "main.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5a3383 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +
+ +
++A Discord bot that provides information on LAN Play servers, players, and games played. +
+
+# Table of Contents
+- [Get API Key](#get-api-key)
+- [Create a Bot Account](#create-a-bot-account)
+- [Invite the bot into your server](#invite-the-bot-into-your-server)
+- [Run the Bot](#run-the-bot)
+- [Commands Available](#commands-available)
+- [Contributing](#contributing)
+- [License](#license)
+- [Acknowledgements](#acknowledgements)
+- [Star History](#star-history)
+
+
+## Get API Key
+
+To use the LanPlay package you will need a specific, non-transferable API key that can be retrieved from LanPlay.
+
+Please follow these steps :
+
+1. Open a Web Browser and go to http://www.lan-play.com
+
+2. Open a Console Mode (Ctrl + Shift + C or F12 should work, or Google is your friend to find how to access the Console Mode đź« )
+
+3. Go to `Network` tab and refresh the current page
+
+4. Search a line named `getMonitors` and click on it
+
+5. Open the `Payload` tab
+
+6. Copy the `api_key` value and store it in safe location for the [Run the Bot](#run-the-bot) step.
+
+## Create a Bot Account
+
+1. Make sure you’re logged on to the [Discord website](https://discord.com/).
+
+2. Navigate to the [Discord Application](https://discord.com/developers/applications) for developers.
+
+3. Click on the `New Application` button.
+
+4. Give the application a name and click `Create`.
+
+5. Navigate to the `Bot` tab to configure it.
+
+6. Make sure that `Public Bot` is ticked if you want others to invite your bot.
+
+ - You should also make sure that `Require OAuth2 Code Grant` is unchecked.
+
+7. Copy the token using the `Copy` button and store it in safe location for the [Run the Bot](#run-the-bot) step.
+
+ - This is not the Client Secret at the General Information page.
+
+ > It should be worth noting that this token is essentially your bot’s password. You should never share this with someone else. In doing so, someone can log in to your bot and do malicious things, such as leaving servers, ban all members inside a server, or pinging everyone maliciously.
+ >
+ > The possibilities are endless, so do not share this token.
+ >
+ > If you accidentally leaked your token, click the `Regenerate` button as soon as possible. This revokes your old token and re-generates a new one. Now you need to use the new token to login.
+
+## Invite the bot into your server
+
+1. Navigate to the [Discord Application](https://discord.com/developers/applications) for developers.
+
+2. Open the App you previously created for the Bot.
+
+3. Navigate to `OAuth2` tab.
+
+4. Scroll down and in `OAuth2 URL Generator` -> `SCOPES`, tick these boxes:
+
+ - `bot`
+ - `applications.commands`
+
+5. Scroll down and in `BOT PERMISSIONS`, tick these boxes:
+
+ - `Manage Expressions`
+ - `Create Expressions`
+ - `Send Messages`
+ - `Send Messages in Threads`
+ - `Embed Links`
+ - `Read Message History`
+
+6. Make sure that `INTEGRATION TYPE` value is `Guild Install`.
+
+7. Copy/Paste the `GENERATED URL` at bottom to a new Browser Tab and add it to your Discord Server.
+
+## Run the Bot
+
+- Download [Docker](https://www.docker.com) and install it on your computer.
+
+- Make sure you got both [api_key](#get-api-key) and [token](#create-a-bot-account) values, and [invited the bot to your server](#invite-the-bot-into-your-server).
+
+- Open a Windows Terminal and execute the following command to run the Discord Bot:
+
+```docker
+docker run -e API_LAN_KEY=
e.g. 'tekn0.net:11451' | ![Status of server addition](https://github.com/LeGeRyChEeSe/LanPlay-DiscordBot/blob/main/ressources/add.png?raw=true) | `Admin` |
+| `/delete` | `server`
e.g. 'tekn0.net:11451' | ![Status of server deletion](https://github.com/LeGeRyChEeSe/LanPlay-DiscordBot/blob/main/ressources/delete.png?raw=true) | `Admin` |
+
+## Contributing
+
+Any contributions you make are **greatly appreciated**.
+
+1. Fork the Project
+2. Create your Feature Branch (`git checkout -b feature/NewFeature`)
+3. Commit your Changes (`git commit -m 'Add some NewFeature'`)
+4. Push to the Branch (`git push origin feature/NewFeature`)
+5. Open a Pull Request
+
+
+Thanks to every [contributors](https://github.com/LeGeRyChEeSe/LanPlay-DiscordBot/graphs/contributors) who have contributed in this project.
+
+## License
+
+Distributed under the MIT License. See [LICENSE](https://github.com/LeGeRyChEeSe/LanPlay-DiscordBot/blob/main/LICENSE) for more information.
+
+## Acknowledgements
+
+Shoutout to LizardByte for the Sunshine repo: https://github.com/LizardByte/Sunshine
+
+Shoutout to itsmikethetech for the Virtual Display Driver repo: https://github.com/itsmikethetech/Virtual-Display-Driver
+
+Thanks to Cynary for the Sunshine Virtual Monitor scripts: https://github.com/Cynary/sunshine-virtual-monitor
+
+Shoutout to JosefNemec for Playnite: https://github.com/JosefNemec/Playnite
+
+Shoutout to Nonary for the PlayNiteWatcher script: https://github.com/Nonary/PlayNiteWatcher
+
+## Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=LeGeRyChEeSe/LanPlay-DiscordBot&type=Date)](https://star-history.com/#LeGeRyChEeSe/LanPlay-DiscordBot&Date)
+
+----
+
+Author/Maintainer: [Garoh](https://github.com/LeGeRyChEeSe/) | Discord: garohrl
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..17852bc
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,170 @@
+import requests
+import disnake
+import json
+import os
+import typing
+from datetime import datetime
+from decouple import config
+from disnake.ext import commands
+from gql import gql, Client
+from gql.transport.aiohttp import AIOHTTPTransport
+
+list_all_games_url = "https://tinfoil.media/Title/ApiJson/"
+monitors_url = "https://api.uptimerobot.com/v2/getMonitors"
+API_LAN_KEY = config('API_LAN_KEY')
+lan_menu_url = "http://lan-play.com"
+lan_config_url = "http://lan-play.com/install-switch"
+image_lanplay_url = "http://lan-play.com/img/logo.f64272e3.png"
+
+
+async def getLanPlayInfos(lan_server_url: str) -> typing.Optional[typing.Dict]:
+ transport = AIOHTTPTransport(url=lan_server_url)
+ async with Client(
+ transport=transport,
+ ) as session:
+
+ query = gql("""
+ query getUsers {
+ room {
+ contentId
+ hostPlayerName
+ nodeCountMax
+ nodeCount
+ advertiseData
+ nodes {
+ playerName
+ }
+ }
+ serverInfo {
+ online
+ idle
+ }
+ }
+ """)
+
+ try:
+ result = await session.execute(query)
+ except:
+ return
+ else:
+ if hasRooms(result):
+ list_games = matchGame(result["room"])
+ if list_games and len(list_games) > 0:
+ for room in result["room"]:
+ for game in list_games:
+ if room["contentId"].lower() == game["id"].lower():
+ gameName = game["name"][game["name"].find(
+ '\"\u003e') + 2: game["name"].find('\u003c/a\u003e')]
+ iconUrl = game["icon"][game["icon"].find(
+ 'url') + 4: game["icon"].find(')\"')]
+ room["gameName"] = gameName
+ room["iconUrl"] = iconUrl
+ return result
+
+
+async def getNumberOfRooms(lan_server_url: str) -> int:
+ transport = AIOHTTPTransport(url=lan_server_url)
+ async with Client(
+ transport=transport,
+ ) as session:
+
+ query = gql("""
+ query getUsers {
+ room {
+ nodeCount
+ }
+ }
+ """)
+
+ result = await session.execute(query)
+ return len(result["room"])
+
+
+def hasRooms(server: typing.Dict) -> bool:
+ return isinstance(server.get("room", []), list) and bool(server["room"])
+
+
+def matchGame(rooms: list) -> list:
+ list_all_games = requests.get(list_all_games_url)
+ if not list_all_games.ok:
+ return []
+
+ list_all_games = list_all_games.json()
+ content_ids = set([room["contentId"].lower() for room in rooms])
+ list_games = [game for game in list_all_games["data"]
+ if game["id"].lower() in content_ids]
+
+ for game in list_games:
+ if game["id"].lower() == "ffffffffffffffff":
+ game["id"] = "0100B04011742000"
+
+ return list_games
+
+
+def getLanServers() -> typing.Dict:
+ response = requests.post(monitors_url, json={
+ "api_key": API_LAN_KEY, "format": "json", "all_time_uptime_ratio": 1})
+ return response.json()
+
+
+def getLocalization(bot: commands.InteractionBot, key: str, locale: disnake.Locale, **kwargs) -> typing.Optional[str]:
+ text_localized = bot.i18n.get(key).get(str(locale))
+
+ for k, value in kwargs.items():
+ k_formatted = "{" + k + "}"
+ text_localized = text_localized.replace(k_formatted, str(value))
+
+ return text_localized if text_localized else None
+
+
+def load_lan_servers(filename: str = 'lan_servers.json') -> typing.Optional[typing.Union[typing.List[typing.Dict[str, typing.Union[str, int]]], list]]:
+ if os.path.exists(filename):
+ with open(filename, 'r') as file:
+ return json.load(file)
+ return []
+
+
+def save_lan_servers(lan_servers: typing.List[typing.Dict[str, typing.Union[str, int]]], filename: str = 'lan_servers.json') -> None:
+ with open(filename, 'w') as file:
+ json.dump(lan_servers, file, indent=4)
+
+
+def create_custom_server(friendly_name: str) -> typing.Dict[str, typing.Union[str, int]]:
+ return {
+ "id": friendly_name.split(':')[0], # Utiliser la partie avant les ':'
+ "friendly_name": friendly_name,
+ "url": f"http://{friendly_name}/info",
+ "type": 1,
+ "sub_type": "",
+ "keyword_type": "None",
+ "keyword_case_type": 0,
+ "keyword_value": "",
+ "http_username": "",
+ "http_password": "",
+ "port": "",
+ "interval": 300,
+ "timeout": 30,
+ "status": 9,
+ "create_datetime": int(datetime.now().timestamp()), # Timestamp actuel
+ "all_time_uptime_ratio": "99.999"
+ }
+
+
+def add_custom_server(temp_lan_servers: typing.List[typing.Dict[str, typing.Union[str, int]]], friendly_name, lan_servers=None) -> bool:
+ custom_server = create_custom_server(friendly_name)
+
+ for server in temp_lan_servers:
+ if custom_server["id"] == server["id"]:
+ return False
+
+ if lan_servers:
+ for server in lan_servers["monitors"]:
+ if custom_server["friendly_name"] == server["friendly_name"]:
+ return False
+
+ temp_lan_servers.append(custom_server)
+ return True
+
+
+def delete_custom_server(lan_servers: typing.List[typing.Dict[str, typing.Union[str, int]]], friendly_name: str) -> typing.List[typing.Dict[str, typing.Union[str, int]]]:
+ return [server for server in lan_servers if server.get("friendly_name") != friendly_name]
diff --git a/lansbot.jpg b/lansbot.jpg
new file mode 100644
index 0000000..c675493
Binary files /dev/null and b/lansbot.jpg differ
diff --git a/locale/en_US.json b/locale/en_US.json
new file mode 100644
index 0000000..4d6143a
--- /dev/null
+++ b/locale/en_US.json
@@ -0,0 +1,29 @@
+{
+ "LAN_DESCRIPTION": "Display current games of any Lan Play server.",
+ "ADD_DESCRIPTION": "Add a Lan Play server to the list.",
+ "ADD_PARAMETER": "The server to add. E.g.: 'tekn0.net:11451'",
+ "ADD_ERROR": "The server format is incorrect. Use the 'domain:port' format.",
+ "ADD_SUCCESS": "Server `{server}` added successfully.",
+ "ADD_EXISTS": "The `{server}` server has already been added to the list.",
+ "DELETE_DESCRIPTION": "Remove a custom Lan Play server from the list.",
+ "DELETE_SUCCESS": "Custom server `{server}` successfully removed.",
+ "DELETE_ERROR": "The {server} server no longer exists.",
+ "HELP_DESCRIPTION": "Display the help menu for Lan'sBot commands.",
+ "HELP_TITLE": "Lan'sBot Help Menu",
+ "UPTIME": "% uptime",
+ "ONE_GAME": "active game.",
+ "MULTIPLE_GAME": "active games.",
+ "NO_GAME": "No active games.",
+ "GAME_HOST": "Host of the game:",
+ "SERVER_SELECT": "Please select one of the servers below",
+ "SITE_LANPLAY": "Lan Play site",
+ "CONFIG_LANPLAY": "Setup Lan Play",
+ "SERVER_SELECT_BUTTON": "Select a server",
+ "PLAYERS": "Players",
+ "EMBED_TITLE": "Research in progress...",
+ "EMBED_DESCRIPTION": "The search can take more or less time depending on the number of current games on the server. Thank you for your patience and understanding.",
+ "EMBED_ERROR_DESCRIPTION": "An error occurred with the server.",
+ "NO_PERMS_TITLE": "You cannot interact with this drop-down list!",
+ "NO_PERMS_FOOTER": "Drop-down list created by {member}",
+ "GAME_PLAYING": "A game is playing but I don't know which one..."
+}
\ No newline at end of file
diff --git a/locale/fr.json b/locale/fr.json
new file mode 100644
index 0000000..9ada882
--- /dev/null
+++ b/locale/fr.json
@@ -0,0 +1,29 @@
+{
+ "LAN_DESCRIPTION": "Afficher les parties actuelles de n'importe quel serveur Lan Play.",
+ "ADD_DESCRIPTION": "Ajouter un serveur custom Lan Play Ă la liste.",
+ "ADD_PARAMETER": "Le serveur custom Ă ajouter. Par exemple: 'tekn0.net:11451'",
+ "ADD_ERROR": "Le format du serveur custom est incorrect. Utilisez le format 'domaine:port'.",
+ "ADD_SUCCESS": "Serveur custom `{server}` ajouté avec succès.",
+ "ADD_EXISTS": "Le serveur custom `{server}` a déjà été ajouté à la liste.",
+ "DELETE_DESCRIPTION": "Supprimer un serveur custom Lan Play de la liste.",
+ "DELETE_SUCCESS": "Serveur custom `{server}` correctement supprimé.",
+ "DELETE_ERROR": "Le serveur {server} n'existe plus.",
+ "HELP_DESCRIPTION": "Afficher le menu d'aide pour les commandes de Lan'sBot.",
+ "HELP_TITLE": "Menu d'aide Lan'sBot",
+ "UPTIME": "% de disponibilité",
+ "ONE_GAME": "partie en cours.",
+ "MULTIPLE_GAME": "parties en cours.",
+ "NO_GAME": "Aucune partie en cours.",
+ "GAME_HOST": "HĂ´te de la partie:",
+ "SERVER_SELECT": "Veuillez sélectionner un des serveurs ci-dessous",
+ "SITE_LANPLAY": "Site Lan Play",
+ "CONFIG_LANPLAY": "Configurer Lan Play",
+ "SERVER_SELECT_BUTTON": "Choisir un serveur",
+ "PLAYERS": "Joueurs",
+ "EMBED_TITLE": "Recherche en cours...",
+ "EMBED_DESCRIPTION": "La recherche peut durer plus ou moins longtemps selon le nombre de parties actuelles sur le serveur.\nMerci de votre patience et de votre compréhension.",
+ "EMBED_ERROR_DESCRIPTION": "Une erreur est survenue avec le serveur.",
+ "NO_PERMS_TITLE": "Vous ne pouvez pas intéragir avec cette liste déroulante !",
+ "NO_PERMS_FOOTER": "Liste déroulante créée par {member}",
+ "GAME_PLAYING": "Un jeu est en cours mais je ne sais pas lequel..."
+}
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..3af83bc
--- /dev/null
+++ b/main.py
@@ -0,0 +1,225 @@
+import functions
+import os
+import disnake
+import locale
+import datetime
+import requests
+import typing
+import re
+from disnake.ui import Button, Select
+from disnake import SelectOption
+from dotenv import load_dotenv
+from disnake.ext import commands
+
+load_dotenv()
+
+
+locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8')
+os.environ['TZ'] = 'Europe/Paris'
+lan_servers = functions.getLanServers()
+custom_servers = functions.load_lan_servers()
+lan_servers["monitors"].extend(custom_servers)
+
+intents = disnake.Intents.all()
+client = commands.InteractionBot(intents=intents)
+client.i18n.load("locale/")
+
+
+def getTimeStamp() -> datetime.datetime:
+ return datetime.datetime.now(datetime.timezone.utc)
+
+
+@client.listen()
+async def on_dropdown(inter: disnake.MessageInteraction):
+ """
+ Fonction appelée lors de l'interaction avec un menu de sélection.
+
+ Paramètres
+ ----------
+ inter: :class:`disnake.MessageInteraction`
+ L'interaction avec le message.
+ """
+ if not inter.values:
+ return
+
+ custom_id = inter.component.custom_id
+ lan_server = f"http://{inter.values[0]}/"
+
+ if custom_id == f"lan_servers_{inter.author.id}":
+ await inter.response.defer(with_message=False)
+ embed = disnake.Embed(color=disnake.Color.blue())
+ embed.title = functions.getLocalization(
+ client, 'EMBED_TITLE', inter.locale)
+ embed.description = functions.getLocalization(
+ client, 'EMBED_DESCRIPTION', inter.locale)
+ await inter.edit_original_message(embed=embed)
+
+ embed.title = inter.values[0]
+ embed.url = functions.lan_menu_url
+ embed.set_thumbnail(url=functions.image_lanplay_url)
+ embed.set_footer(text=f"{[s['all_time_uptime_ratio'] for s in lan_servers['monitors'] if s['friendly_name'] in lan_server][0]}{functions.getLocalization(client, 'UPTIME', inter.locale)}", icon_url=inter.guild.icon.url)
+
+ server = await functions.getLanPlayInfos(lan_server)
+ emojis = []
+
+ if not server:
+ embed.description = functions.getLocalization(
+ client, 'EMBED_ERROR_DESCRIPTION', inter.locale)
+ else:
+ if len(server['room']) > 1:
+ embed.description = f"{server['serverInfo']['online']-server['serverInfo']['idle']} :video_game: / {server['serverInfo']['idle']} :zzz:"
+ embed.set_author(name=f"{len(server['room'])} {functions.getLocalization(client, 'MULTIPLE_GAME', inter.locale)}", icon_url=inter.author.display_avatar.url)
+ elif len(server['room']) == 1:
+ embed.description = f"{server['serverInfo']['online']-server['serverInfo']['idle']} :video_game: / {server['serverInfo']['idle']} :zzz:"
+ embed.set_author(name=f"{len(server['room'])} {functions.getLocalization(client, 'ONE_GAME', inter.locale)}", icon_url=inter.author.display_avatar.url)
+ else:
+ embed.description = f"{server['serverInfo']['online']-server['serverInfo']['idle']} :video_game: / {server['serverInfo']['idle']} :zzz:"
+ embed.set_author(name=f"{functions.getLocalization(client, 'NO_GAME', inter.locale)}", icon_url=inter.author.display_avatar.url)
+
+ emojis: typing.List[disnake.Emoji] = []
+ for room in server["room"]:
+ nb_players_room = f"({room['nodeCount']}/{room['nodeCountMax']})"
+ players = ',\n'.join(player['playerName']
+ for player in room['nodes'])
+
+ if room['nodeCount'] == room['nodeCountMax']:
+ nb_players_room += " :x:"
+ else:
+ nb_players_room += " :white_check_mark:"
+
+ if 'iconUrl' in room.keys():
+ response = requests.get(room["iconUrl"])
+ if inter.guild:
+ emojis.append(await inter.guild.create_custom_emoji(name=room["contentId"], image=response.content))
+ embed.add_field(name=f"{emojis[-1]} {room['gameName']} {nb_players_room}", value=f"{functions.getLocalization(client, 'GAME_HOST', inter.locale)} {bytearray.fromhex(room['advertiseData'][56:94].replace('00', '')).decode()} ({room['hostPlayerName']})\n**{functions.getLocalization(client, 'PLAYERS', inter.locale)}:**\n{players}", inline=False)
+ else:
+ embed.add_field(name=functions.getLocalization(client, 'GAME_PLAYING', inter.locale), value=f"{functions.getLocalization(client, 'GAME_HOST', inter.locale)} {bytearray.fromhex(room['advertiseData'][56:94].replace('00', '')).decode()} ({room['hostPlayerName']})\n**{functions.getLocalization(client, 'PLAYERS', inter.locale)}:**\n{players}", inline=False)
+
+ embed.timestamp = getTimeStamp()
+
+ await inter.edit_original_message(embed=embed)
+ else:
+ embed = disnake.Embed(title=functions.getLocalization(
+ client, 'NO_PERMS_TITLE', inter.locale), colour=disnake.Colour.red())
+ embed.set_author(name=inter.author.display_name,
+ icon_url=inter.author.display_avatar)
+ if inter.guild and custom_id:
+ member = await inter.guild.get_or_fetch_member(re.search('[0-5]+', custom_id).group())
+ if member:
+ embed.set_footer(text=functions.getLocalization(client, 'NO_PERMS_FOOTER', inter.locale, member=await inter.guild.get_or_fetch_member(re.search('[0-9]+', custom_id).group())))
+ await inter.response.send_message(embed=embed, ephemeral=True)
+
+
+@client.slash_command(name="lan")
+async def lan(inter: disnake.ApplicationCommandInteraction):
+ """
+ Afficher les parties actuelles de n'importe quel serveur LAN-Play. {{LAN_DESCRIPTION}}
+ """
+ embed = disnake.Embed(color=disnake.Color.blue())
+ embed.set_thumbnail(url=functions.image_lanplay_url)
+
+ embed.title = functions.getLocalization(
+ client, "SERVER_SELECT", inter.locale)
+
+ components = []
+
+ components.append(Button(style=disnake.ButtonStyle.url, label=functions.getLocalization(
+ client, "SITE_LANPLAY", inter.locale), url=functions.lan_menu_url))
+ components.append(Button(style=disnake.ButtonStyle.url, label=functions.getLocalization(
+ client, "CONFIG_LANPLAY", inter.locale), url=functions.lan_config_url))
+ components.append(Select(placeholder=functions.getLocalization(client, "SERVER_SELECT_BUTTON", inter.locale), custom_id=f"lan_servers_{inter.author.id}", options=[SelectOption(label=server["friendly_name"], value=server["friendly_name"], description=f"{server['all_time_uptime_ratio']}{functions.getLocalization(client, 'UPTIME', inter.locale)}") for server in sorted(lan_servers["monitors"], key=lambda x: x["all_time_uptime_ratio"], reverse=True)][0:25]))
+
+ await inter.response.send_message(embed=embed, components=components)
+
+
+@client.slash_command(name="help", dm_permission=True)
+async def help(inter: disnake.ApplicationCommandInteraction):
+ """
+ Afficher le menu d'aide pour les commandes de Lan'sBot. {{HELP_DESCRIPTION}}
+ """
+ embed = disnake.Embed(title=functions.getLocalization(
+ client, "HELP_TITLE", inter.locale), color=disnake.Color.blue(), timestamp=getTimeStamp())
+ embed.set_thumbnail(client.user.display_avatar.url)
+ embed.description = ""
+
+ for command in client.slash_commands:
+ localized_command = functions.getLocalization(client, f"{command.name.upper()}_DESCRIPTION", inter.locale)
+ embed.description += f"`/{command.qualified_name}`: {localized_command}\n"
+
+ await inter.response.send_message(embed=embed, ephemeral=True)
+
+
+@client.slash_command(name="add")
+@commands.default_member_permissions(administrator=True)
+async def add(inter: disnake.ApplicationCommandInteraction, server: str):
+ """
+ Ajouter un serveur custom Lan Play Ă la liste. {{ADD_DESCRIPTION}}
+
+ Parameters
+ ----------
+ server: :class:`str`
+ Le serveur custom Ă ajouter. Par exemple 'tekn0.net:11451' {{ADD_PARAMETER}}
+ """
+ pattern = r'^[a-zA-Z0-9.-]+:\d+$'
+
+ if not re.match(pattern, server):
+ await inter.response.send_message(functions.getLocalization(client, "ADD_ERROR", inter.locale), ephemeral=True)
+ return
+
+ temp_lan_custom_servers = functions.load_lan_servers()
+ custom_server = functions.create_custom_server(server)
+
+ if not functions.add_custom_server(temp_lan_custom_servers, server, lan_servers):
+ await inter.response.send_message(functions.getLocalization(client, "ADD_EXISTS", inter.locale, server=server), ephemeral=True)
+ return
+
+ functions.save_lan_servers(temp_lan_custom_servers)
+ lan_servers["monitors"].extend([custom_server])
+
+ await inter.response.send_message(functions.getLocalization(client, "ADD_SUCCESS", inter.locale, server=server), ephemeral=True)
+
+
+@client.slash_command(name="delete")
+@commands.default_member_permissions(administrator=True)
+async def delete(inter: disnake.ApplicationCommandInteraction, server: str):
+ """
+ Supprimer un serveur custom Lan Play de la liste. {{DELETE_DESCRIPTION}}
+
+ Parameters
+ ----------
+ server: :class:`str`
+ Le serveur custom Ă supprimer. Par exemple 'tekn0.net:11451' {{ADD_PARAMETER}}
+ """
+ pattern = r'^[a-zA-Z0-9.-]+:\d+$'
+
+ if not re.match(pattern, server):
+ await inter.response.send_message(functions.getLocalization(client, "DELETE_ERROR", inter.locale), ephemeral=True)
+ return
+
+ custom_servers = functions.load_lan_servers()
+ custom_server = [c for c in custom_servers if server ==
+ c.get("friendly_name")][0] if custom_servers else None
+ new_lan_custom_servers = functions.delete_custom_server(
+ custom_servers, server) if custom_servers else None
+
+ if new_lan_custom_servers:
+ functions.save_lan_servers(new_lan_custom_servers)
+ lan_servers["monitors"].remove(custom_server)
+
+ await inter.response.send_message(functions.getLocalization(client, "DELETE_SUCCESS", inter.locale, server=server), ephemeral=True)
+
+
+@delete.autocomplete("server")
+async def server_autocomplete(inter: disnake.ApplicationCommandInteraction, server: str):
+ custom_servers = functions.load_lan_servers()
+ return [server["friendly_name"] for server in custom_servers]
+
+# Events
+
+
+@client.event
+async def on_ready():
+ await client.change_presence(activity=disnake.Activity(type=disnake.ActivityType.watching, name="/help"))
+ print(f"{client.user.display_name}#{client.user.discriminator} is ready.")
+
+client.run(os.getenv("TOKEN"))
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..6a248d4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,23 @@
+aiohappyeyeballs==2.4.0
+aiohttp==3.10.5
+aiosignal==1.3.1
+async-timeout==4.0.3
+attrs==24.2.0
+backoff==2.2.1
+certifi==2024.7.4
+charset-normalizer==3.3.2
+disnake==2.9.2
+frozenlist==1.4.1
+gql==3.5.0
+graphql-core==3.2.3
+idna==3.8
+multidict==6.0.4
+pipdeptree==2.23.1
+pycodestyle==2.12.1
+python-decouple==3.8
+python-dotenv==1.0.1
+requests==2.32.3
+tomli==2.0.1
+urllib3==2.2.2
+websocket-client==1.8.0
+yarl==1.9.4
diff --git a/ressources/add.png b/ressources/add.png
new file mode 100644
index 0000000..a895751
Binary files /dev/null and b/ressources/add.png differ
diff --git a/ressources/delete.png b/ressources/delete.png
new file mode 100644
index 0000000..f869bdd
Binary files /dev/null and b/ressources/delete.png differ
diff --git a/ressources/help.png b/ressources/help.png
new file mode 100644
index 0000000..b7ac732
Binary files /dev/null and b/ressources/help.png differ
diff --git a/ressources/lan.png b/ressources/lan.png
new file mode 100644
index 0000000..cee4184
Binary files /dev/null and b/ressources/lan.png differ