Skip to content

Commit

Permalink
feat(mc): convert command to slash cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
ZRunner committed Jan 27, 2024
1 parent a58dcf7 commit 75076d3
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 81 deletions.
39 changes: 14 additions & 25 deletions docs/minecraft.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,27 @@

At the very beginning, Axobot was a single server bot, and focused on the world-famous Minecraft game.

Even after diversifying, the bot has not forgotten its origins and remains very open to this cubic world, offering several commands related to the game. You will find a huge database on all the blocks, entities, items, commands, progress, potion effects, enchantments, and more. As well as a command to obtain the status of a Minecraft server (it is possible to display it permanently so that the information is refreshed regularly). And another one for the state of Mojang's servers. If you find this content is very low, don't worry: other orders are in preparation!
Even after diversifying, the bot has not forgotten its origins and remains very open to this cubic world, offering several commands related to the game. You will still find some cool commands to get information about Minecraft Java servers, player skins, or mods. There's even a command to get the status of a Minecraft Java server in real time, right into your Discord server!

.. note:: The whole database comes from a single Minecraft site (French, like Axobot): `fr-minecraft.net <https://fr-minecraft.net>`__ . The search engine and the information collected are therefore those appearing on this site. If you observe any error in this database, do not hesitate to contact me so that I relay it to the administrator of the site!
--------------------------
Get a server/skin/mod info
--------------------------

.. warning:: Most of these commands are reserved for certain roles only. To allow roles to use a command, see the `config` command
**Syntax:** :code:`minecraft (server|skin|mod) <name>`

Mods info come from either the `CurseForge API <https://docs.curseforge.com>`__ or the `Modrinth API <https://docs.modrinth.com/#tag/projects>`__ (depending on which one offers the closest result to your query), so Axobot may not be able to find some mods. Please also note that their search engines sometimes behave very strangely and may not give the best results. Player and server searches use the official Mojang API and tools.

---
MC
---
.. warning:: The bot needs the "`Embed links <perms.html#embed-links>`__" permission to send its search query, as well as "`Read message history <perms.html#read-message-history>`__" and to display the status of a server (enabled with `add` subcommand)

**Syntax:** :code:`mc <type> <name>`

This command is the main command of this module: the one that allows to search the information in the database, or to get those from a Minecraft server. To ask the bot to send the status of a server and to refresh this message regularly, use the `add` subcommand followed by the server ip. The bot will then try to edit the last message about this server, and if it can't, it will send a new one.
--------------------------
Subscribe to a server info
--------------------------

To search in the database, the command is disconcertingly simple: you just have to write the type of your search (entity, block, mod, etc.) followed by its name (partial or total, French or English) or its identifier (numerical or textual). The rest does itself!
**Syntax:** :code:`minecraft follow-server <server> [port] [channel]`

To see the list of available types, enter the help mc command in the chat. If you don't find what you're looking for, don't worry: this type is probably planned for later!
This command allows you to follow a Minecraft Java server right into your channel. Axobot will post a simple embed containing some information about the server (its version, number of connected players, motd, etc.), and update it on a regular basis. You can also specify a port if the server is not running on the default port (25565), and a channel if you want to post the embed in another channel than the one where you typed the command.

Mods info come from the `CurseForge API <https://twitchappapi.docs.apiary.io/>`__ (currently managed by Twitch), so Axobot may not be able to find some mods. Please also note that their search engine is very weird, and may not have the best results. Players search use the official Mojang API, and other data come from the french `fr-minecraft.net <https://fr-minecraft.net>`__ website.

.. warning::
.. note::
* The bot needs the "`Embed links <perms.html#embed-links>`__" permission to send its search query, as well as "`Read message history <perms.html#read-message-history>`__" and to display the status of a server (enabled with `add` subcommand)
* Adding server tracking automatically with `add` is considered the same way as an rss feed, which means that it takes a place in your feed list (limited to a certain number, except for a few special cases).


------
Mojang
------

**Syntax:** :code:`mojang` or :code:`mojang_status`

This command, much more basic, uses the Mojang API to get the status of its servers. For each server you will thus have its state, its url, as well as a short description.

.. note:: The bot does not need any specific permission for this command, however note that the appearance will look better if ""`Embed links <perms.html#embed-links>`__" permission is enabled.
* Adding server tracking automatically with `follow-server` is considered the same way as an rss feed, which means that it takes a place in your feeds list (limited to a certain number).
2 changes: 1 addition & 1 deletion docs/perms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Allows the bot to send a TTS (text-to-speech) message, i.e. a message that will
Embed Links
-----------

Allows the bot the bot to send an embed. Some commands will need that permissions, some others will only look worse. Examples of use for a better display: `membercount <infos.html#membercount>`__ , `mojang <minecraft.html#mojang>`__, `XP system <user.html#xp-system>`__ . Examples of required permission: `infos <infos.html#info>`__ , `mc <minecraft.html#mc>`__ , `config see <server.html#watch>`__, `embeds generator <miscellaneous.html#embed>`__
Allows the bot the bot to send an embed. Some commands will need that permissions, some others will only look worse. Examples of use for a better display: `membercount <infos.html#membercount>`__ , `mojang <minecraft.html#mojang>`__, `XP system <user.html#xp-system>`__ . Examples of required permission: `infos <infos.html#info>`__ , `minecraft <minecraft.html#mc>`__ , `config see <server.html#watch>`__, `embeds generator <miscellaneous.html#embed>`__

Attach Files
------------
Expand Down
117 changes: 68 additions & 49 deletions fcts/minecraft.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
from typing import Any, Optional, Union

import aiohttp
from asyncache import cached
from cachetools import TTLCache
import discord
from dateutil.parser import isoparse
from discord import app_commands
from discord.ext import commands
from fcts.rss import can_use_rss

from libs.bot_classes import Axobot, MyContext
from libs.checks import checks
Expand All @@ -21,8 +25,7 @@ def _similar(input_1: str, input_2: str):
return SequenceMatcher(None, input_1, input_2).ratio()

class Minecraft(commands.Cog):
"""Cog gathering all commands related to the Minecraft® game.
Every information come from the website www.fr-minecraft.net"""
"""Cog gathering all commands related to the Minecraft® game"""

def __init__(self, bot: Axobot):
self.bot = bot
Expand All @@ -45,10 +48,9 @@ async def cog_unload(self):
self._session = None


@commands.group(name="minecraft", aliases=["mc"])
@commands.cooldown(5, 30, commands.BucketType.user)
@commands.hybrid_group(name="minecraft")
async def mc_main(self, ctx: MyContext):
"""Search for Minecraft game items/servers
"""Search for info about servers/players/mods from Minecraft
..Doc minecraft.html#mc"""
if ctx.subcommand_passed is None:
Expand All @@ -62,28 +64,23 @@ async def send_embed(self, ctx: MyContext, embed: discord.Embed):
self.bot.dispatch("error", err, ctx)
await ctx.send(await self.bot._(ctx.channel, "minecraft.serv-error"))

@mc_main.command(name="mod", aliases=["mods"])
async def mc_mod(self, ctx: MyContext, *, value: str = 'help'):
"""Get info about any mod registered on CurseForge
@mc_main.command(name="mod")
@app_commands.checks.cooldown(5, 20)
async def mc_mod(self, ctx: MyContext, *, mod_name: str):
"""Get info about any mod registered on CurseForge or Modrinth
..Example mc mod Minecolonies
..Example minecraft mod Minecolonies
..Doc minecraft.html#mc"""
if value == 'help':
await ctx.send(await self.bot._(ctx.channel, "minecraft.mod-help", p=ctx.prefix))
return
if not ctx.can_send_embed:
await ctx.send(await self.bot._(ctx.channel, "minecraft.no-embed"))
return
await ctx.defer()
if cf_result := await self.get_mod_from_curseforge(ctx, value):
if cf_result := await self.get_mod_from_curseforge(ctx, mod_name):
cf_embed, cf_pertinence = cf_result
if cf_pertinence > 0.9:
await self.send_embed(ctx, cf_embed)
return
else:
cf_embed, cf_pertinence = None, 0
if mr_result := await self.get_mod_from_modrinth(ctx, value):
if mr_result := await self.get_mod_from_modrinth(ctx, mod_name):
mr_embed, mr_pertinence = mr_result
if mr_pertinence > 0.9:
await self.send_embed(ctx, mr_embed)
Expand Down Expand Up @@ -217,16 +214,15 @@ async def get_mod_from_modrinth(self, ctx: MyContext, search_value: str):
return embed, pertinence

@mc_main.command(name="skin")
@app_commands.checks.cooldown(5, 20)
@commands.check(checks.bot_can_embed)
async def mc_skin(self, ctx: MyContext, username):
"""Get the skin of any Java player
async def mc_skin(self, ctx: MyContext, username: str):
"""Get the skin of any Minecraft Java player
..Example mc skin Notch
..Example minecraft skin Notch
..Doc minecraft.html#mc"""
if not ctx.can_send_embed:
await ctx.send(await self.bot._(ctx.channel, "minecraft.no-embed"))
return
await ctx.defer()
uuid = await self.username_to_uuid(username)
if uuid is None:
await ctx.send(await self.bot._(ctx.channel, "minecraft.player-not-found"))
Expand All @@ -239,50 +235,72 @@ async def mc_skin(self, ctx: MyContext, username):
await self.send_embed(ctx, emb)

@mc_main.command(name="server")
async def mc_server(self, ctx: MyContext, ip: str, port: int = None):
"""Get infos about any Minecraft server
@app_commands.checks.cooldown(5, 20)
async def mc_server(self, ctx: MyContext, ip: str, port: Optional[int] = None):
"""Get infos about any Minecraft Java server
..Example mc server play.gunivers.net
..Example minecraft server play.gunivers.net
..Doc minecraft.html#mc"""
if ":" in ip and port is None:
i = ip.split(":")
ip, port = i[0], i[1]
obj = await self.create_server_1(ctx.guild, ip, port)
await self.send_msg_server(obj, ctx.channel, (ip, port))
ip, port_str = ip.split(":")
if not port_str.isnumeric():
await ctx.send(await self.bot._(ctx.guild, "minecraft.invalid-port"))
return
port = int(port_str)
elif port is None:
port_str = None
else:
port_str = str(port)
await ctx.defer()
obj = await self.create_server_1(ctx.guild, ip, port_str)
embed = await self.form_msg_server(obj, ctx.guild, (ip, port_str))
await ctx.send(embed=embed)

@mc_main.command(name="add")
@mc_main.command(name="follow-server")
@commands.guild_only()
async def mc_add_server(self, ctx: MyContext, ip, port: int = None):
"""Follow a server's info (regularly displayed on this channel)
@commands.check(can_use_rss)
@app_commands.checks.cooldown(5, 20)
async def mc_follow_server(self, ctx: MyContext, ip: str, port: Optional[int] = None,
channel: Optional[discord.TextChannel] = None):
"""Follow a server's info in real time in your channel
..Example mc add mc.hypixel.net
..Example minecraft follow-server mc.hypixel.net
..Doc minecraft.html#mc"""
if not ctx.bot.database_online:
return await ctx.send(await self.bot._(ctx.guild.id, "cases.no_database"))
await ctx.send(await self.bot._(ctx.guild.id, "cases.no_database"))
return
if ":" in ip and port is None:
i = ip.split(":")
ip, port = i[0], i[1]
elif port is None:
port = ''
ip, port_str = ip.split(":")
if not port_str.isnumeric():
await ctx.send(await self.bot._(ctx.guild, "minecraft.invalid-port"))
return
port = int(port_str)
# TODO: add proper ip validation and testing
await ctx.defer()
is_over, flow_limit = await self.bot.get_cog('Rss').is_overflow(ctx.guild)
if is_over:
await ctx.send(await self.bot._(ctx.guild.id, "rss.flow-limit", limit=flow_limit))
return
if channel is None:
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).send_messages or channel.permissions_for(ctx.guild.me).embed_links:
await ctx.send(await self.bot._(ctx.guild.id, "minecraft.serv-follow.missing-perms"))
return
try:
if port is None:
display_ip = ip
else:
display_ip = f"{ip}:{port}"
await self.bot.get_cog('Rss').db_add_feed(ctx.guild.id, ctx.channel.id, 'mc', f"{ip}:{port}")
await ctx.send(await self.bot._(ctx.guild, "minecraft.success-add", ip=display_ip, channel=ctx.channel.mention))
await self.bot.get_cog('Rss').db_add_feed(ctx.guild.id, channel.id, 'mc', f"{ip}:{port or ''}")
await ctx.send(await self.bot._(ctx.guild, "minecraft.serv-follow.success", ip=display_ip, channel=channel.mention))
except Exception as err:
cmd = await self.bot.get_command_mention("about")
await ctx.send(await self.bot._(ctx.guild, "errors.unknown2", about=cmd))
self.bot.dispatch("error", err, ctx)

async def create_server_1(self, guild: discord.Guild, ip: str, port=None) -> Union[str, 'MCServer']:
async def create_server_1(self, guild: discord.Guild, ip: str, port: Optional[str]=None) -> Union[str, 'MCServer']:
"Collect and serialize server data from a given IP, using minetools.eu"
if port is None:
url = "https://api.minetools.eu/ping/"+str(ip)
Expand Down Expand Up @@ -326,7 +344,7 @@ async def create_server_1(self, guild: discord.Guild, ip: str, port=None) -> Uni
ping=l, desc=data['description'], api='api.minetools.eu'
).clear_desc()

async def create_server_2(self, guild: discord.Guild, ip: str, port: str):
async def create_server_2(self, guild: discord.Guild, ip: str, port: Optional[str]):
"Collect and serialize server data from a given IP, using mcsrvstat.us"
if port is None:
url = "https://api.mcsrvstat.us/1/"+str(ip)
Expand All @@ -335,7 +353,7 @@ async def create_server_2(self, guild: discord.Guild, ip: str, port: str):
try:
async with self.session.get(url, timeout=5) as resp:
data: dict = await resp.json()
except aiohttp.ClientConnectorError:
except (aiohttp.ClientConnectorError, aiohttp.ContentTypeError):
return await self.bot._(guild, "minecraft.no-api")
except json.decoder.JSONDecodeError:
return await self.bot._(guild, "minecraft.serv-error")
Expand Down Expand Up @@ -368,6 +386,7 @@ async def create_server_2(self, guild: discord.Guild, ip: str, port: str):
ping=l, desc=desc, api="api.mcsrvstat.us"
).clear_desc()

@cached(TTLCache(maxsize=1_000, ttl=60*60*24*7)) # 1 week
async def username_to_uuid(self, username: str) -> str:
"""Convert a minecraft username to its uuid"""
if username in self.uuid_cache:
Expand Down Expand Up @@ -431,19 +450,19 @@ async def create_msg(self, guild: discord.Guild, translate):
else:
embed.add_field(name=await translate(guild, "minecraft.serv-2"), value=", ".join(p))
if self.ping is not None:
embed.add_field(name=await translate(guild, "minecraft.serv-3"), value=f"{self.ping} ms")
embed.add_field(name=await translate(guild, "minecraft.serv-3"), value=f"{self.ping:.0f} ms")
if self.desc:
embed.add_field(name="Description", value="```\n" + self.desc + "\n```", inline=False)
return embed

async def send_msg_server(self, obj: Union[str, "MCServer"], channel: discord.abc.Messageable, ip: str):
async def send_msg_server(self, obj: Union[str, "MCServer"], channel: discord.abc.Messageable, ip: tuple[str, Optional[str]]):
"Send the message into a Discord channel"
guild = None if isinstance(channel, discord.DMChannel) else channel.guild
e = await self.form_msg_server(obj, guild, ip)
embed = await self.form_msg_server(obj, guild, ip)
if self.bot.zombie_mode:
return
if isinstance(channel, discord.DMChannel) or channel.permissions_for(channel.guild.me).embed_links:
msg = await channel.send(embed=e)
msg = await channel.send(embed=embed)
else:
try:
await channel.send(await self.bot._(guild, "minecraft.cant-embed"))
Expand All @@ -452,7 +471,7 @@ async def send_msg_server(self, obj: Union[str, "MCServer"], channel: discord.ab
msg = None
return msg

async def form_msg_server(self, obj: Union[str, "MCServer"], guild: discord.Guild, ip: str):
async def form_msg_server(self, obj: Union[str, "MCServer"], guild: discord.Guild, ip: tuple[str, Optional[str]]):
"Create the embed from the saved data"
if isinstance(obj, str):
if ip[1] is None:
Expand Down
Loading

0 comments on commit 75076d3

Please sign in to comment.